agent-ecosystem/agent-teams-controller/test/crossTeam.test.js
iliya 210b59884e feat: implement cross-team message deduplication and improve handling
- Added deduplication logic to prevent recent equivalent cross-team messages from being sent multiple times.
- Introduced functions to normalize message content and build deduplication keys.
- Updated CrossTeamService and CrossTeamOutbox to utilize deduplication during message sending.
- Enhanced tests to verify deduplication behavior and ensure correct message handling in various scenarios.
2026-03-09 22:50:13 +02:00

361 lines
11 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_PREFIX_TAG } = 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.lead');
expect(inbox[0].text).toContain(`[${CROSS_TEAM_PREFIX_TAG} 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('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);
});
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([]);
});
});
});