agent-ecosystem/test/main/ipc/crossTeam.test.ts
iliya c3eea4d6eb feat: add cross-team communication orchestrator
Autonomous message routing between Agent Teams via MCP with cascade
protection. Inbox-first canonical delivery with cross-process file
lock (O_CREAT|O_EXCL) and best-effort relay for online teams.

- CascadeGuard: rate limit (10/min), chain depth (max 5), pair cooldown (3s)
- FileLock: cross-process safe via kernel-level atomic lock files
- CrossTeamService: validate → cascade → file-lock → inbox write → relay → outbox
- Unified lead resolver with members.meta.json normalization (trim+dedup)
- 3 MCP tools: cross_team_send, cross_team_list_targets, cross_team_get_outbox
- Controller module with sync file lock, same protocol as main
- IPC adapter with 3 Electron handlers
- 64 new tests across 8 test files
2026-03-09 18:45:15 +02:00

163 lines
4.9 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest';
vi.mock('@shared/utils/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
}));
vi.mock('@preload/constants/ipcChannels', () => ({
CROSS_TEAM_SEND: 'cross-team:send',
CROSS_TEAM_LIST_TARGETS: 'cross-team:listTargets',
CROSS_TEAM_GET_OUTBOX: 'cross-team:getOutbox',
}));
import {
initializeCrossTeamHandlers,
registerCrossTeamHandlers,
removeCrossTeamHandlers,
} from '@main/ipc/crossTeam';
function createMockIpcMain() {
return {
handle: vi.fn(),
removeHandler: vi.fn(),
};
}
function createMockService() {
return {
send: vi.fn(),
listAvailableTargets: vi.fn(),
getOutbox: vi.fn(),
};
}
describe('crossTeam IPC handlers', () => {
let mockIpc: ReturnType<typeof createMockIpcMain>;
let mockService: ReturnType<typeof createMockService>;
beforeEach(() => {
mockIpc = createMockIpcMain();
mockService = createMockService();
// Re-initialize with fresh service for each test
initializeCrossTeamHandlers(mockService as never);
});
it('registers 3 IPC handlers', () => {
registerCrossTeamHandlers(mockIpc as never);
expect(mockIpc.handle).toHaveBeenCalledTimes(3);
expect(mockIpc.handle).toHaveBeenCalledWith('cross-team:send', expect.any(Function));
expect(mockIpc.handle).toHaveBeenCalledWith('cross-team:listTargets', expect.any(Function));
expect(mockIpc.handle).toHaveBeenCalledWith('cross-team:getOutbox', expect.any(Function));
});
it('removes all 3 handlers', () => {
removeCrossTeamHandlers(mockIpc as never);
expect(mockIpc.removeHandler).toHaveBeenCalledTimes(3);
expect(mockIpc.removeHandler).toHaveBeenCalledWith('cross-team:send');
expect(mockIpc.removeHandler).toHaveBeenCalledWith('cross-team:listTargets');
expect(mockIpc.removeHandler).toHaveBeenCalledWith('cross-team:getOutbox');
});
it('send handler returns success on valid request', async () => {
mockService.send.mockResolvedValue({ messageId: 'msg-1', deliveredToInbox: true });
registerCrossTeamHandlers(mockIpc as never);
const handler = mockIpc.handle.mock.calls.find((c) => c[0] === 'cross-team:send')![1];
const result = await handler({} as never, {
fromTeam: 'team-a',
fromMember: 'lead',
toTeam: 'team-b',
text: 'Hello',
});
expect(result).toEqual({
success: true,
data: { messageId: 'msg-1', deliveredToInbox: true },
});
expect(mockService.send).toHaveBeenCalledWith({
fromTeam: 'team-a',
fromMember: 'lead',
toTeam: 'team-b',
text: 'Hello',
summary: undefined,
chainDepth: undefined,
});
});
it('send handler returns error on service throw', async () => {
mockService.send.mockRejectedValue(new Error('Target team not found'));
registerCrossTeamHandlers(mockIpc as never);
const handler = mockIpc.handle.mock.calls.find((c) => c[0] === 'cross-team:send')![1];
const result = await handler({} as never, {
fromTeam: 'team-a',
fromMember: 'lead',
toTeam: 'nonexistent',
text: 'Hello',
});
expect(result).toEqual({ success: false, error: 'Target team not found' });
});
it('send handler rejects invalid request', async () => {
registerCrossTeamHandlers(mockIpc as never);
const handler = mockIpc.handle.mock.calls.find((c) => c[0] === 'cross-team:send')![1];
const result = await handler({} as never, null);
expect(result).toEqual({ success: false, error: 'Invalid request' });
});
it('listTargets handler calls service', async () => {
mockService.listAvailableTargets.mockResolvedValue([
{ teamName: 'team-b', displayName: 'Team B' },
]);
registerCrossTeamHandlers(mockIpc as never);
const handler = mockIpc.handle.mock.calls.find(
(c) => c[0] === 'cross-team:listTargets'
)![1];
const result = await handler({} as never, 'team-a');
expect(result).toEqual({
success: true,
data: [{ teamName: 'team-b', displayName: 'Team B' }],
});
expect(mockService.listAvailableTargets).toHaveBeenCalledWith('team-a');
});
it('getOutbox handler calls service', async () => {
mockService.getOutbox.mockResolvedValue([]);
registerCrossTeamHandlers(mockIpc as never);
const handler = mockIpc.handle.mock.calls.find(
(c) => c[0] === 'cross-team:getOutbox'
)![1];
const result = await handler({} as never, 'team-a');
expect(result).toEqual({ success: true, data: [] });
expect(mockService.getOutbox).toHaveBeenCalledWith('team-a');
});
it('getOutbox handler rejects empty teamName', async () => {
registerCrossTeamHandlers(mockIpc as never);
const handler = mockIpc.handle.mock.calls.find(
(c) => c[0] === 'cross-team:getOutbox'
)![1];
const result = await handler({} as never, '');
expect(result).toEqual({ success: false, error: 'teamName is required' });
});
});