agent-ecosystem/agent-teams-controller/test/crossTeam.test.js
2026-04-24 22:41:16 +03:00

458 lines
15 KiB
JavaScript

const fs = require('fs');
const os = require('os');
const path = require('path');
const { createController } = require('../src/index.js');
const { CROSS_TEAM_SOURCE, CROSS_TEAM_TAG_NAME } = require('../src/internal/crossTeamProtocol.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_SOURCE);
expect(inbox[0].from).toBe('team-a.team-lead');
expect(inbox[0].text).toContain(`<${CROSS_TEAM_TAG_NAME} from="team-a.team-lead" depth="0"`);
expect(inbox[0].conversationId).toBeTruthy();
expect(inbox[0].text).toContain(`conversationId="${inbox[0].conversationId}"`);
});
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');
expect(outbox[0].conversationId).toBeTruthy();
const sentMessagesPath = path.join(claudeDir, 'teams', 'team-a', 'sentMessages.json');
const sentMessages = JSON.parse(fs.readFileSync(sentMessagesPath, 'utf8'));
expect(sentMessages).toHaveLength(1);
expect(sentMessages[0].from).toBe('team-lead');
expect(sentMessages[0].to).toBe('team-b.team-lead');
expect(sentMessages[0].text).toBe('Hello');
expect(sentMessages[0].source).toBe('cross_team_sent');
expect(sentMessages[0].messageId).toBe(outbox[0].messageId);
});
it('preserves taskRefs in target inbox, sender copy and outbox', () => {
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 taskRefs = [{ taskId: 'task-1', displayId: 'abcd1234', teamName: 'team-a' }];
const controller = createController({ teamName: 'team-a', claudeDir });
controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Please review the linked task',
taskRefs,
});
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'team-lead.json');
const inbox = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
expect(inbox[0].taskRefs).toEqual(taskRefs);
const sentMessagesPath = path.join(claudeDir, 'teams', 'team-a', 'sentMessages.json');
const sentMessages = JSON.parse(fs.readFileSync(sentMessagesPath, 'utf8'));
expect(sentMessages[0].taskRefs).toEqual(taskRefs);
const outbox = controller.crossTeam.getCrossTeamOutbox();
expect(outbox[0].taskRefs).toEqual(taskRefs);
});
it('rejects unknown source fromMember', () => {
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',
fromMember: 'ghost',
text: 'Hello from nowhere',
})
).toThrow('Unknown fromMember');
});
it('preserves reply conversation metadata for explicit replies', () => {
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: 'Answering the open question',
replyToConversationId: 'conv-123',
});
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'team-lead.json');
const inbox = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
expect(inbox[0].conversationId).toBe('conv-123');
expect(inbox[0].replyToConversationId).toBe('conv-123');
expect(inbox[0].text).toContain('conversationId="conv-123"');
expect(inbox[0].text).toContain('replyToConversationId="conv-123"');
});
it('deduplicates the same recent cross-team request', () => {
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 first = controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
fromMember: 'lead',
text: 'Please review the API contract',
summary: 'Review request',
});
const second = controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
fromMember: 'lead',
text: 'Please review the API contract',
summary: ' Review request ',
});
expect(second.deliveredToInbox).toBe(true);
expect(second.deduplicated).toBe(true);
expect(second.messageId).toBe(first.messageId);
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'team-lead.json');
const inbox = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
expect(inbox).toHaveLength(1);
const outbox = controller.crossTeam.getCrossTeamOutbox();
expect(outbox).toHaveLength(1);
const sentMessagesPath = path.join(claudeDir, 'teams', 'team-a', 'sentMessages.json');
const sentMessages = JSON.parse(fs.readFileSync(sentMessagesPath, 'utf8'));
expect(sentMessages).toHaveLength(1);
});
it('allows resending after dedupe window expires', () => {
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 originalNow = Date.now;
let now = originalNow();
Date.now = () => now;
try {
const first = controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Need a decision on the schema',
summary: 'Schema decision',
});
now += 6 * 60 * 1000;
const second = controller.crossTeam.sendCrossTeamMessage({
toTeam: 'team-b',
text: 'Need a decision on the schema',
summary: 'Schema decision',
});
expect(second.deduplicated).toBeUndefined();
expect(second.messageId).not.toBe(first.messageId);
const inboxPath = path.join(claudeDir, 'teams', 'team-b', 'inboxes', 'team-lead.json');
const inbox = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
expect(inbox).toHaveLength(2);
const updatedOutbox = controller.crossTeam.getCrossTeamOutbox();
expect(updatedOutbox).toHaveLength(2);
} finally {
Date.now = originalNow;
}
});
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([]);
});
});
});