agent-ecosystem/agent-teams-controller/test/crossTeam.test.js
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

277 lines
8.4 KiB
JavaScript

const fs = require('fs');
const os = require('os');
const path = require('path');
const { createController } = require('../src/index.js');
describe('crossTeam module', () => {
function makeClaudeDir(teams = {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'crossteam-test-'));
for (const [teamName, config] of Object.entries(teams)) {
const teamDir = path.join(dir, 'teams', teamName);
const taskDir = path.join(dir, 'tasks', teamName);
fs.mkdirSync(teamDir, { recursive: true });
fs.mkdirSync(taskDir, { recursive: true });
fs.mkdirSync(path.join(teamDir, 'inboxes'), { recursive: true });
fs.writeFileSync(
path.join(teamDir, 'config.json'),
JSON.stringify(config, null, 2)
);
}
return dir;
}
afterEach(() => {
// Reset cascade guard between tests
const cascadeGuard = require('../src/internal/cascadeGuard.js');
cascadeGuard.reset();
});
describe('sendCrossTeamMessage', () => {
it('delivers message to target team inbox', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
const result = controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
fromMember: 'lead',
text: 'Hello from team-a',
summary: 'Test message',
});
expect(result.deliveredToInbox).toBe(true);
expect(result.messageId).toBeDefined();
// Verify inbox was written
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'team-lead.json');
const inbox = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
expect(inbox).toHaveLength(1);
expect(inbox[0].source).toBe('cross_team');
expect(inbox[0].from).toBe('team-a.lead');
expect(inbox[0].text).toContain('[Cross-team from team-a.lead | depth:0]');
});
it('records outbox entry', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Hello',
});
const outbox = controller.crossTeam.getCrossTeamOutbox();
expect(outbox).toHaveLength(1);
expect(outbox[0].toTeam).toBe('team-b');
});
it('rejects self-send', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
expect(() =>
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-a',
text: 'Self',
})
).toThrow('same team');
});
it('rejects when target not found', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
expect(() =>
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-nonexistent',
text: 'Hello',
})
).toThrow('Target team not found');
});
it('rejects when target is deleted', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
deletedAt: '2024-01-01T00:00:00Z',
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
expect(() =>
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Hello',
})
).toThrow('Target team not found');
});
it('rejects excessive chain depth', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
expect(() =>
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Hello',
chainDepth: 5,
})
).toThrow('chain depth');
});
});
describe('resolveTargetLead', () => {
it('resolves lead by agentType', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'alpha-lead', agentType: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [{ name: 'beta-lead', agentType: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Hello',
});
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'beta-lead.json');
expect(fs.existsSync(inboxPath)).toBe(true);
});
it('resolves lead by name fallback', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [{ name: 'team-lead' }],
},
});
const controller = createController({ teamName: 'team-a', claudeDir });
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Hello',
});
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'team-lead.json');
expect(fs.existsSync(inboxPath)).toBe(true);
});
it('resolves lead from members.meta.json with normalization', () => {
const claudeDir = makeClaudeDir({
'team-a': {
name: 'team-a',
members: [{ name: 'team-lead' }],
},
'team-b': {
name: 'team-b',
members: [],
},
});
// Write meta with dirty data (leading spaces, duplicates)
const metaPath = path.join(claudeDir, 'teams', 'team-b', 'members.meta.json');
fs.writeFileSync(
metaPath,
JSON.stringify({
members: [
{ name: ' meta-lead ', agentType: 'team-lead' },
{ name: ' meta-lead ', agentType: 'team-lead' },
{ name: 'worker', agentType: 'worker' },
],
})
);
const controller = createController({ teamName: 'team-a', claudeDir });
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Hello',
});
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'meta-lead.json');
expect(fs.existsSync(inboxPath)).toBe(true);
});
});
describe('listCrossTeamTargets', () => {
it('lists valid teams excluding current', () => {
const claudeDir = makeClaudeDir({
'team-a': { name: 'Team A' },
'team-b': { name: 'Team B', description: 'B desc' },
'team-c': { name: 'Team C', deletedAt: '2024-01-01' },
});
const controller = createController({ teamName: 'team-a', claudeDir });
const targets = controller.crossTeam.listCrossTeamTargets();
expect(targets).toHaveLength(1);
expect(targets[0].teamName).toBe('team-b');
expect(targets[0].displayName).toBe('Team B');
expect(targets[0].description).toBe('B desc');
});
});
describe('getCrossTeamOutbox', () => {
it('returns empty for non-existent outbox', () => {
const claudeDir = makeClaudeDir({
'team-a': { name: 'Team A' },
});
const controller = createController({ teamName: 'team-a', claudeDir });
const outbox = controller.crossTeam.getCrossTeamOutbox();
expect(outbox).toEqual([]);
});
});
});