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
58 lines
1.7 KiB
JavaScript
58 lines
1.7 KiB
JavaScript
const cascadeGuard = require('../src/internal/cascadeGuard.js');
|
|
|
|
describe('cascadeGuard', () => {
|
|
beforeEach(() => {
|
|
cascadeGuard.reset();
|
|
});
|
|
|
|
describe('rate limit', () => {
|
|
it('allows up to 10 messages per minute', () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
cascadeGuard.check('team-a', `team-${i}`, 0);
|
|
cascadeGuard.record('team-a', `team-${i}`);
|
|
}
|
|
expect(() => cascadeGuard.check('team-a', 'team-x', 0)).toThrow('rate limit');
|
|
});
|
|
});
|
|
|
|
describe('chain depth', () => {
|
|
it('allows depth 0 through 4', () => {
|
|
for (let d = 0; d < 5; d++) {
|
|
expect(() => cascadeGuard.check('team-a', 'team-b', d)).not.toThrow();
|
|
}
|
|
});
|
|
|
|
it('rejects depth >= 5', () => {
|
|
expect(() => cascadeGuard.check('team-a', 'team-b', 5)).toThrow('chain depth');
|
|
});
|
|
});
|
|
|
|
describe('pair cooldown', () => {
|
|
it('rejects same pair within 3s', () => {
|
|
cascadeGuard.check('team-a', 'team-b', 0);
|
|
cascadeGuard.record('team-a', 'team-b');
|
|
|
|
expect(() => cascadeGuard.check('team-a', 'team-b', 0)).toThrow('cooldown');
|
|
});
|
|
|
|
it('allows different pairs simultaneously', () => {
|
|
cascadeGuard.check('team-a', 'team-b', 0);
|
|
cascadeGuard.record('team-a', 'team-b');
|
|
|
|
expect(() => cascadeGuard.check('team-a', 'team-c', 0)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('reset', () => {
|
|
it('clears all state', () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
cascadeGuard.check('team-a', `team-${i}`, 0);
|
|
cascadeGuard.record('team-a', `team-${i}`);
|
|
}
|
|
|
|
cascadeGuard.reset();
|
|
|
|
expect(() => cascadeGuard.check('team-a', 'team-0', 0)).not.toThrow();
|
|
});
|
|
});
|
|
});
|