agent-orchestrator/ui-electrobun/tests/unit/hardening/channel-acl.test.ts
Hibryda 1995f03682 test: complete test suite — 166 new tests (stores + hardening + agent)
@agor/stores (37 tests):
- theme: 6 (17 themes, 3 groups, no duplicates)
- notifications: 11 (types, rate limiter, window expiry)
- health: 20 (scoring, burn rate, context pressure, tool tracking)

Electrobun stores (90 tests):
- agent-store: 27 (seqId, dedup, double-start guard, persistence)
- workspace-store: 17 (CRUD, derived state, aggregates)
- plugin-store: 14 (commands, events, permissions, meta)
- keybinding-store: 18 (defaults, chords, conflicts, capture)

Hardening (39 tests):
- durable-sequencing: 10 (monotonic, dedup, restore)
- file-conflict: 10 (mtime, atomic write, workflows)
- backpressure: 7 (paste 64KB, buffer 50MB, line 10MB)
- retention: 7 (count, age, running protected)
- channel-acl: 9 (join/leave, rejection, isolation)

Total across all suites: 1,020+ tests
2026-03-22 05:07:40 +01:00

165 lines
5.4 KiB
TypeScript

// Tests for channel ACL — membership-gated messaging.
// Uses bun:test. Tests the logic from btmsg-db.ts channel operations.
import { describe, it, expect, beforeEach } from 'bun:test';
// ── In-memory channel store (replicated logic from btmsg-db.ts) ─────────────
interface ChannelMessage {
id: string;
channelId: string;
fromAgent: string;
content: string;
}
function createChannelStore() {
const channels = new Map<string, { id: string; name: string }>();
const members = new Map<string, Set<string>>(); // channelId -> Set<agentId>
const messages: ChannelMessage[] = [];
let msgCounter = 0;
return {
createChannel(id: string, name: string): void {
channels.set(id, { id, name });
members.set(id, new Set());
},
joinChannel(channelId: string, agentId: string): void {
const ch = channels.get(channelId);
if (!ch) throw new Error(`Channel '${channelId}' not found`);
members.get(channelId)!.add(agentId);
},
leaveChannel(channelId: string, agentId: string): void {
members.get(channelId)?.delete(agentId);
},
sendChannelMessage(channelId: string, fromAgent: string, content: string): string {
const memberSet = members.get(channelId);
if (!memberSet || !memberSet.has(fromAgent)) {
throw new Error(`Agent '${fromAgent}' is not a member of channel '${channelId}'`);
}
const id = `msg-${++msgCounter}`;
messages.push({ id, channelId, fromAgent, content });
return id;
},
getChannelMembers(channelId: string): string[] {
return Array.from(members.get(channelId) ?? []);
},
getMessages(channelId: string): ChannelMessage[] {
return messages.filter(m => m.channelId === channelId);
},
};
}
// ── Tests ───────────────────────────────────────────────────────────────────
describe('channel membership', () => {
let store: ReturnType<typeof createChannelStore>;
beforeEach(() => {
store = createChannelStore();
store.createChannel('general', 'General');
});
it('joinChannel adds member', () => {
store.joinChannel('general', 'agent-1');
expect(store.getChannelMembers('general')).toContain('agent-1');
});
it('joinChannel to nonexistent channel throws', () => {
expect(() => store.joinChannel('nonexistent', 'agent-1')).toThrow('not found');
});
it('leaveChannel removes member', () => {
store.joinChannel('general', 'agent-1');
store.leaveChannel('general', 'agent-1');
expect(store.getChannelMembers('general')).not.toContain('agent-1');
});
it('leaveChannel is idempotent', () => {
store.leaveChannel('general', 'agent-1');
expect(store.getChannelMembers('general')).toHaveLength(0);
});
it('getChannelMembers returns all members', () => {
store.joinChannel('general', 'agent-1');
store.joinChannel('general', 'agent-2');
store.joinChannel('general', 'agent-3');
expect(store.getChannelMembers('general')).toHaveLength(3);
});
it('duplicate join is idempotent (Set semantics)', () => {
store.joinChannel('general', 'agent-1');
store.joinChannel('general', 'agent-1');
expect(store.getChannelMembers('general')).toHaveLength(1);
});
});
describe('channel message ACL', () => {
let store: ReturnType<typeof createChannelStore>;
beforeEach(() => {
store = createChannelStore();
store.createChannel('ops', 'Operations');
store.joinChannel('ops', 'manager');
});
it('member can send message', () => {
const id = store.sendChannelMessage('ops', 'manager', 'hello team');
expect(id).toBeTruthy();
const msgs = store.getMessages('ops');
expect(msgs).toHaveLength(1);
expect(msgs[0].content).toBe('hello team');
expect(msgs[0].fromAgent).toBe('manager');
});
it('non-member is rejected', () => {
expect(() => {
store.sendChannelMessage('ops', 'outsider', 'sneaky message');
}).toThrow("not a member");
});
it('former member is rejected after leaving', () => {
store.leaveChannel('ops', 'manager');
expect(() => {
store.sendChannelMessage('ops', 'manager', 'should fail');
}).toThrow("not a member");
});
it('rejoined member can send again', () => {
store.leaveChannel('ops', 'manager');
store.joinChannel('ops', 'manager');
const id = store.sendChannelMessage('ops', 'manager', 'back again');
expect(id).toBeTruthy();
});
});
describe('channel isolation', () => {
let store: ReturnType<typeof createChannelStore>;
beforeEach(() => {
store = createChannelStore();
store.createChannel('ch-a', 'Channel A');
store.createChannel('ch-b', 'Channel B');
store.joinChannel('ch-a', 'agent-1');
});
it('member of channel A cannot send to channel B', () => {
expect(() => {
store.sendChannelMessage('ch-b', 'agent-1', 'wrong channel');
}).toThrow("not a member");
});
it('messages are channel-scoped', () => {
store.joinChannel('ch-b', 'agent-2');
store.sendChannelMessage('ch-a', 'agent-1', 'msg in A');
store.sendChannelMessage('ch-b', 'agent-2', 'msg in B');
expect(store.getMessages('ch-a')).toHaveLength(1);
expect(store.getMessages('ch-b')).toHaveLength(1);
expect(store.getMessages('ch-a')[0].content).toBe('msg in A');
expect(store.getMessages('ch-b')[0].content).toBe('msg in B');
});
});