153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { AgentMessageBus } from '../src/agent-message-bus.js';
|
|
import { createAgentCommsTools } from '../src/agent-comms-tools.js';
|
|
|
|
describe('AgentMessageBus', () => {
|
|
it('send() delivers message to recipient workspace', () => {
|
|
const bus = new AgentMessageBus();
|
|
const id = bus.send({ from: 'ws-1', to: 'ws-2', content: 'Hello from ws-1' });
|
|
|
|
expect(id).toBeTruthy();
|
|
const messages = bus.receive('ws-2');
|
|
expect(messages).toHaveLength(1);
|
|
expect(messages[0].from).toBe('ws-1');
|
|
expect(messages[0].content).toBe('Hello from ws-1');
|
|
});
|
|
|
|
it('receive() clears messages after read (one-shot)', () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'Message 1' });
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'Message 2' });
|
|
|
|
const first = bus.receive('ws-2');
|
|
expect(first).toHaveLength(2);
|
|
|
|
const second = bus.receive('ws-2');
|
|
expect(second).toHaveLength(0);
|
|
});
|
|
|
|
it('messages have sender, recipient, content, timestamp', () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'Test' });
|
|
|
|
const messages = bus.receive('ws-2');
|
|
expect(messages[0].id).toBeTruthy();
|
|
expect(messages[0].from).toBe('ws-1');
|
|
expect(messages[0].to).toBe('ws-2');
|
|
expect(messages[0].content).toBe('Test');
|
|
expect(messages[0].timestamp).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('request/response pattern with correlationId', () => {
|
|
const bus = new AgentMessageBus();
|
|
const requestId = bus.send({ from: 'ws-1', to: 'ws-2', content: 'What is X?' });
|
|
const responseId = bus.reply(requestId, 'X is 42', 'ws-2', 'ws-1');
|
|
|
|
const responses = bus.receive('ws-1');
|
|
expect(responses).toHaveLength(1);
|
|
expect(responses[0].correlationId).toBe(requestId);
|
|
expect(responses[0].content).toBe('X is 42');
|
|
});
|
|
|
|
it('messages expire after TTL', () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'Expired', ttlMs: 1 });
|
|
|
|
// Wait for expiry
|
|
const start = Date.now();
|
|
while (Date.now() - start < 5) { /* spin */ }
|
|
|
|
const messages = bus.receive('ws-2');
|
|
expect(messages).toHaveLength(0);
|
|
});
|
|
|
|
it('peek() shows messages without consuming them', () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'Peek test' });
|
|
|
|
expect(bus.peek('ws-2')).toHaveLength(1);
|
|
expect(bus.peek('ws-2')).toHaveLength(1); // Still there
|
|
|
|
bus.receive('ws-2'); // Consume
|
|
expect(bus.peek('ws-2')).toHaveLength(0);
|
|
});
|
|
|
|
it('cleanup() removes expired messages', () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'Expired', ttlMs: 1 });
|
|
bus.send({ from: 'ws-1', to: 'ws-3', content: 'Still valid', ttlMs: 60000 });
|
|
|
|
const start = Date.now();
|
|
while (Date.now() - start < 5) { /* spin */ }
|
|
|
|
const removed = bus.cleanup();
|
|
expect(removed).toBe(1);
|
|
expect(bus.peek('ws-3')).toHaveLength(1);
|
|
});
|
|
|
|
it('pendingCount() returns message count', () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'A' });
|
|
bus.send({ from: 'ws-1', to: 'ws-2', content: 'B' });
|
|
|
|
expect(bus.pendingCount('ws-2')).toBe(2);
|
|
expect(bus.pendingCount('ws-3')).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('send_agent_message tool', () => {
|
|
it('sends message to another workspace agent', async () => {
|
|
const bus = new AgentMessageBus();
|
|
const tools = createAgentCommsTools(bus, 'ws-1');
|
|
const sendTool = tools.find(t => t.name === 'send_agent_message')!;
|
|
|
|
const result = JSON.parse(await sendTool.execute({ workspace: 'ws-2', message: 'Research findings' }));
|
|
expect(result.success).toBe(true);
|
|
expect(result.messageId).toBeTruthy();
|
|
|
|
// Verify message arrived
|
|
const messages = bus.receive('ws-2');
|
|
expect(messages).toHaveLength(1);
|
|
expect(messages[0].content).toBe('Research findings');
|
|
});
|
|
|
|
it('rejects sending to self', async () => {
|
|
const bus = new AgentMessageBus();
|
|
const tools = createAgentCommsTools(bus, 'ws-1');
|
|
const sendTool = tools.find(t => t.name === 'send_agent_message')!;
|
|
|
|
const result = JSON.parse(await sendTool.execute({ workspace: 'ws-1', message: 'Self' }));
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('yourself');
|
|
});
|
|
|
|
it('fails if target workspace is not active', async () => {
|
|
const bus = new AgentMessageBus();
|
|
const tools = createAgentCommsTools(bus, 'ws-1', (id) => id === 'ws-1');
|
|
const sendTool = tools.find(t => t.name === 'send_agent_message')!;
|
|
|
|
const result = JSON.parse(await sendTool.execute({ workspace: 'ws-2', message: 'Hello' }));
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('not active');
|
|
});
|
|
});
|
|
|
|
describe('check_agent_messages tool', () => {
|
|
it('returns pending messages and consumes them', async () => {
|
|
const bus = new AgentMessageBus();
|
|
bus.send({ from: 'ws-2', to: 'ws-1', content: 'Here are the findings' });
|
|
|
|
const tools = createAgentCommsTools(bus, 'ws-1');
|
|
const checkTool = tools.find(t => t.name === 'check_agent_messages')!;
|
|
|
|
const result = JSON.parse(await checkTool.execute({}));
|
|
expect(result.count).toBe(1);
|
|
expect(result.messages[0].from).toBe('ws-2');
|
|
expect(result.messages[0].content).toBe('Here are the findings');
|
|
|
|
// Second check should be empty (consumed)
|
|
const result2 = JSON.parse(await checkTool.execute({}));
|
|
expect(result2.count).toBe(0);
|
|
});
|
|
});
|