This commit is contained in:
508
packages/waggle-dance/tests/dispatcher.test.ts
Normal file
508
packages/waggle-dance/tests/dispatcher.test.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { WaggleDanceDispatcher } from '../src/dispatcher.js';
|
||||
import type { DispatchDeps } from '../src/dispatcher.js';
|
||||
import type { WaggleMessage } from '@waggle/shared';
|
||||
|
||||
function makeDeps(overrides?: Partial<DispatchDeps>): DispatchDeps {
|
||||
return {
|
||||
searchMemory: vi.fn(async (q: string) => `Memory results for: ${q}`),
|
||||
resolveCapability: vi.fn((q: string) => [
|
||||
{ source: 'native', name: 'search_memory', description: 'Search memory', available: true },
|
||||
]),
|
||||
spawnWorker: vi.fn(async (task: string, role: string) => `Worker completed: ${task}`),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build deps that include the v2 emit/response/recommend callbacks.
|
||||
* Most v2 tests want to assert that the right callback was invoked
|
||||
* with the right message — use this helper to get spies.
|
||||
*/
|
||||
function makeV2Deps(overrides?: Partial<DispatchDeps>): DispatchDeps {
|
||||
return {
|
||||
...makeDeps(),
|
||||
emitSignal: vi.fn(async () => undefined),
|
||||
recordResponse: vi.fn(async () => undefined),
|
||||
recommendModel: vi.fn(async () => 'claude-haiku-4-5'),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMessage(overrides?: Partial<WaggleMessage>): WaggleMessage {
|
||||
return {
|
||||
id: 'msg-1',
|
||||
teamId: 'team-1',
|
||||
senderId: 'agent-1',
|
||||
type: 'request',
|
||||
subtype: 'task_delegation',
|
||||
content: { task: 'Research TypeScript', role: 'researcher' },
|
||||
referenceId: null,
|
||||
routing: null,
|
||||
createdAt: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('WaggleDanceDispatcher', () => {
|
||||
it('constructor creates dispatcher', () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
expect(dispatcher).toBeDefined();
|
||||
expect(dispatcher).toBeInstanceOf(WaggleDanceDispatcher);
|
||||
});
|
||||
|
||||
describe('task_delegation', () => {
|
||||
it('dispatches to spawnWorker with task and role', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage());
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.response).toBe('Worker completed: Research TypeScript');
|
||||
expect(deps.spawnWorker).toHaveBeenCalledWith('Research TypeScript', 'researcher', undefined);
|
||||
});
|
||||
|
||||
it('passes context when provided', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
content: { task: 'Analyze data', role: 'analyst', context: 'Q4 sales figures' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.spawnWorker).toHaveBeenCalledWith('Analyze data', 'analyst', 'Q4 sales figures');
|
||||
});
|
||||
|
||||
it('fails gracefully on missing task', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
content: { role: 'analyst' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toBe('task_delegation requires content.task');
|
||||
});
|
||||
|
||||
it('handles spawnWorker errors', async () => {
|
||||
const deps = makeDeps({
|
||||
spawnWorker: vi.fn(async () => { throw new Error('Worker crashed'); }),
|
||||
});
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage());
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toContain('Worker spawn failed');
|
||||
expect(result.error).toContain('Worker crashed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('knowledge_check', () => {
|
||||
it('dispatches to searchMemory', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'knowledge_check',
|
||||
content: { query: 'TypeScript patterns' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.response).toBe('Memory results for: TypeScript patterns');
|
||||
expect(deps.searchMemory).toHaveBeenCalledWith('TypeScript patterns');
|
||||
});
|
||||
|
||||
it('supports both query and topic fields', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
|
||||
// topic field
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'knowledge_check',
|
||||
content: { topic: 'Architecture decisions' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.searchMemory).toHaveBeenCalledWith('Architecture decisions');
|
||||
});
|
||||
|
||||
it('fails on empty query', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'knowledge_check',
|
||||
content: {},
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toBe('knowledge_check requires content.query or content.topic');
|
||||
});
|
||||
|
||||
it('handles searchMemory errors', async () => {
|
||||
const deps = makeDeps({
|
||||
searchMemory: vi.fn(async () => { throw new Error('DB connection lost'); }),
|
||||
});
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'knowledge_check',
|
||||
content: { query: 'something' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toContain('Memory search failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('skill_request', () => {
|
||||
it('dispatches to resolveCapability', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'skill_request',
|
||||
content: { skill: 'web_search' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.resolveCapability).toHaveBeenCalledWith('web_search');
|
||||
});
|
||||
|
||||
it('returns formatted capability routes', async () => {
|
||||
const deps = makeDeps({
|
||||
resolveCapability: vi.fn(() => [
|
||||
{ source: 'native', name: 'search_memory', description: 'Search memory', available: true },
|
||||
{ source: 'plugin', name: 'web_search', description: 'Web search', available: false },
|
||||
]),
|
||||
});
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'skill_request',
|
||||
content: { skill: 'search' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.response).toContain('Found 2 capabilities (1 available)');
|
||||
expect(result.response).toContain('[native] search_memory');
|
||||
expect(result.response).toContain('[plugin] web_search');
|
||||
expect(result.response).toContain('(available)');
|
||||
expect(result.response).toContain('(not available)');
|
||||
});
|
||||
|
||||
it('handles no results', async () => {
|
||||
const deps = makeDeps({
|
||||
resolveCapability: vi.fn(() => []),
|
||||
});
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'skill_request',
|
||||
content: { skill: 'nonexistent' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.response).toBe('No capability found for "nonexistent"');
|
||||
});
|
||||
|
||||
it('fails on empty skill query', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'skill_request',
|
||||
content: {},
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toBe('skill_request requires content.skill or content.query');
|
||||
});
|
||||
});
|
||||
|
||||
describe('skill_share', () => {
|
||||
it('returns parsed install data when name and content provided', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'skill_share',
|
||||
content: { name: 'code_review', content: '#!/bin/bash\necho review', sharedBy: 'agent-2' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
const parsed = JSON.parse(result.response!);
|
||||
expect(parsed.action).toBe('install_shared_skill');
|
||||
expect(parsed.skillName).toBe('code_review');
|
||||
expect(parsed.skillContent).toBe('#!/bin/bash\necho review');
|
||||
expect(parsed.sharedBy).toBe('agent-2');
|
||||
});
|
||||
|
||||
it('defaults sharedBy to unknown when not provided', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'skill_share',
|
||||
content: { name: 'deploy', content: 'deploy script body' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
const parsed = JSON.parse(result.response!);
|
||||
expect(parsed.sharedBy).toBe('unknown');
|
||||
});
|
||||
|
||||
it('accepts skill field as fallback for name', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'skill_share',
|
||||
content: { skill: 'lint_fix', content: 'lint script' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(true);
|
||||
const parsed = JSON.parse(result.response!);
|
||||
expect(parsed.skillName).toBe('lint_fix');
|
||||
});
|
||||
|
||||
it('fails when name is missing', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'skill_share',
|
||||
content: { content: 'some content' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toBe('skill_share requires content.name and content.content');
|
||||
});
|
||||
|
||||
it('fails when content is missing', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'skill_share',
|
||||
content: { name: 'some_skill' },
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toBe('skill_share requires content.name and content.content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('rejects invalid message type-subtype combo', async () => {
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'task_delegation',
|
||||
}));
|
||||
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toBe('Invalid message: broadcast/task_delegation');
|
||||
});
|
||||
|
||||
it('rejects unknown subtypes (runtime defense against malformed messages)', async () => {
|
||||
// TypeScript prevents this in-process, but network messages may
|
||||
// arrive with garbage subtypes. The dispatcher's switch must have
|
||||
// a default branch that fails gracefully.
|
||||
const deps = makeDeps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch({
|
||||
...makeMessage(),
|
||||
type: 'broadcast',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
subtype: 'nonexistent_subtype' as any,
|
||||
content: {},
|
||||
});
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toMatch(/Invalid message|Unhandled subtype/);
|
||||
});
|
||||
});
|
||||
|
||||
// ── v2 — cross-tool activity bus ──────────────────────────────────
|
||||
|
||||
describe('discovery (v2 broadcast)', () => {
|
||||
it('forwards to emitSignal and reports handled=true', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const message = makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
content: { topic: 'webhook secret rotation', tool: 'claude-code', importance: 'normal' },
|
||||
});
|
||||
const result = await dispatcher.dispatch(message);
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.emitSignal).toHaveBeenCalledWith(message);
|
||||
});
|
||||
|
||||
it('handles missing emitSignal gracefully (Phase 1A behavior)', async () => {
|
||||
// emitSignal is optional. When absent, the dispatcher still
|
||||
// reports handled=true so callers can persist the message
|
||||
// without an emitter wired.
|
||||
const deps = makeDeps(); // no emitSignal
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
content: { topic: 'whatever' },
|
||||
}));
|
||||
expect(result.handled).toBe(true);
|
||||
});
|
||||
|
||||
it('propagates emitSignal errors', async () => {
|
||||
const deps = makeV2Deps({
|
||||
emitSignal: vi.fn(async () => { throw new Error('SSE pipe broken'); }),
|
||||
});
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
content: { topic: 'x' },
|
||||
}));
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toMatch(/Signal emit failed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('routed_share (v2 broadcast)', () => {
|
||||
it('forwards to emitSignal preserving the routing field', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const message = makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'routed_share',
|
||||
content: { payload: { docId: 'doc-42' } },
|
||||
routing: [
|
||||
{ userId: 'user-1', reason: 'subscribed to doc-42' },
|
||||
{ userId: 'user-3', reason: 'reviewer' },
|
||||
],
|
||||
});
|
||||
const result = await dispatcher.dispatch(message);
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.emitSignal).toHaveBeenCalled();
|
||||
const call = (deps.emitSignal as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(call.routing).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('model_recipe (v2 broadcast)', () => {
|
||||
it('forwards a shared model config via emitSignal', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'broadcast',
|
||||
subtype: 'model_recipe',
|
||||
content: {
|
||||
name: 'reasoning-bias-low',
|
||||
model: 'claude-opus-4-7',
|
||||
temperature: 0.2,
|
||||
system: 'You are concise.',
|
||||
},
|
||||
}));
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.emitSignal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('knowledge_match (v2 response)', () => {
|
||||
it('forwards to recordResponse', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const message = makeMessage({
|
||||
type: 'response',
|
||||
subtype: 'knowledge_match',
|
||||
content: { matchedEntities: ['ent-1', 'ent-2'], confidence: 0.91 },
|
||||
referenceId: 'request-msg-id-7',
|
||||
});
|
||||
const result = await dispatcher.dispatch(message);
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.recordResponse).toHaveBeenCalledWith(message);
|
||||
});
|
||||
|
||||
it('is handled even without recordResponse wired', async () => {
|
||||
const deps = makeDeps(); // no recordResponse
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'response',
|
||||
subtype: 'knowledge_match',
|
||||
content: {},
|
||||
referenceId: 'r-1',
|
||||
}));
|
||||
expect(result.handled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task_claim (v2 response)', () => {
|
||||
it('forwards to recordResponse', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const message = makeMessage({
|
||||
type: 'response',
|
||||
subtype: 'task_claim',
|
||||
content: { claimedBy: 'agent-9', eta: '10m' },
|
||||
referenceId: 'task-delegation-msg-3',
|
||||
});
|
||||
const result = await dispatcher.dispatch(message);
|
||||
expect(result.handled).toBe(true);
|
||||
expect(deps.recordResponse).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe('model_recommendation (v2 request)', () => {
|
||||
it('queries recommendModel and returns the recommendation', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'model_recommendation',
|
||||
content: { query: 'fast classifier' },
|
||||
}));
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.response).toContain('claude-haiku-4-5');
|
||||
expect(deps.recommendModel).toHaveBeenCalledWith('fast classifier', expect.any(Object));
|
||||
});
|
||||
|
||||
it('passes additional context fields to recommendModel', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'model_recommendation',
|
||||
content: { query: 'reason about a contract', maxLatencyMs: 4000, budget: 'normal' },
|
||||
}));
|
||||
const call = (deps.recommendModel as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(call[1]).toMatchObject({ maxLatencyMs: 4000, budget: 'normal' });
|
||||
});
|
||||
|
||||
it('handles missing recommendModel gracefully', async () => {
|
||||
const deps = makeDeps(); // no recommendModel
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'model_recommendation',
|
||||
content: { query: 'anything' },
|
||||
}));
|
||||
expect(result.handled).toBe(true);
|
||||
expect(result.response).toMatch(/no model recommender/i);
|
||||
});
|
||||
|
||||
it('fails when query is missing', async () => {
|
||||
const deps = makeV2Deps();
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
const result = await dispatcher.dispatch(makeMessage({
|
||||
type: 'request',
|
||||
subtype: 'model_recommendation',
|
||||
content: {},
|
||||
}));
|
||||
expect(result.handled).toBe(false);
|
||||
expect(result.error).toMatch(/model_recommendation requires content.query/);
|
||||
});
|
||||
});
|
||||
});
|
||||
72
packages/waggle-dance/tests/integration.test.ts
Normal file
72
packages/waggle-dance/tests/integration.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { WaggleDanceDispatcher } from '../src/dispatcher.js';
|
||||
import type { WaggleMessage } from '@waggle/shared';
|
||||
|
||||
describe('Waggle Dance Integration', () => {
|
||||
it('full workflow: task_delegation → knowledge_check → skill_request', async () => {
|
||||
const deps = {
|
||||
searchMemory: vi.fn(async (q: string) => `Found memories about: ${q}`),
|
||||
resolveCapability: vi.fn(() => [
|
||||
{ source: 'native', name: 'web_search', description: 'Web search', available: true },
|
||||
]),
|
||||
spawnWorker: vi.fn(async (task: string) => `Worker completed: ${task}`),
|
||||
};
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
|
||||
// 1. Delegate a task
|
||||
const taskResult = await dispatcher.dispatch({
|
||||
id: '1', teamId: 't1', senderId: 'a1', type: 'request', subtype: 'task_delegation',
|
||||
content: { task: 'Analyze market data', role: 'analyst' },
|
||||
referenceId: null, routing: null, createdAt: new Date(),
|
||||
});
|
||||
expect(taskResult.handled).toBe(true);
|
||||
expect(deps.spawnWorker).toHaveBeenCalledWith('Analyze market data', 'analyst', undefined);
|
||||
|
||||
// 2. Check knowledge
|
||||
const knowledgeResult = await dispatcher.dispatch({
|
||||
id: '2', teamId: 't1', senderId: 'a1', type: 'request', subtype: 'knowledge_check',
|
||||
content: { query: 'market trends' },
|
||||
referenceId: null, routing: null, createdAt: new Date(),
|
||||
});
|
||||
expect(knowledgeResult.handled).toBe(true);
|
||||
expect(knowledgeResult.response).toContain('market trends');
|
||||
|
||||
// 3. Request a skill
|
||||
const skillResult = await dispatcher.dispatch({
|
||||
id: '3', teamId: 't1', senderId: 'a1', type: 'request', subtype: 'skill_request',
|
||||
content: { skill: 'web_search' },
|
||||
referenceId: null, routing: null, createdAt: new Date(),
|
||||
});
|
||||
expect(skillResult.handled).toBe(true);
|
||||
expect(skillResult.response).toContain('web_search');
|
||||
});
|
||||
|
||||
it('round-trip: task delegation result feeds knowledge check', async () => {
|
||||
let lastWorkerResult = '';
|
||||
const deps = {
|
||||
searchMemory: vi.fn(async () => lastWorkerResult),
|
||||
resolveCapability: vi.fn(() => []),
|
||||
spawnWorker: vi.fn(async (task: string) => {
|
||||
lastWorkerResult = `Analysis complete for: ${task}`;
|
||||
return lastWorkerResult;
|
||||
}),
|
||||
};
|
||||
const dispatcher = new WaggleDanceDispatcher(deps);
|
||||
|
||||
// First: delegate
|
||||
await dispatcher.dispatch({
|
||||
id: '1', teamId: 't1', senderId: 'a1', type: 'request', subtype: 'task_delegation',
|
||||
content: { task: 'Analyze competitors', role: 'analyst' },
|
||||
referenceId: null, routing: null, createdAt: new Date(),
|
||||
});
|
||||
|
||||
// Then: check what was learned
|
||||
const result = await dispatcher.dispatch({
|
||||
id: '2', teamId: 't1', senderId: 'a1', type: 'request', subtype: 'knowledge_check',
|
||||
content: { query: 'competitors' },
|
||||
referenceId: null, routing: null, createdAt: new Date(),
|
||||
});
|
||||
|
||||
expect(result.response).toContain('Analyze competitors');
|
||||
});
|
||||
});
|
||||
55
packages/waggle-dance/tests/protocol.test.ts
Normal file
55
packages/waggle-dance/tests/protocol.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { MessageType, MessageSubtype } from '@waggle/shared';
|
||||
import { validateMessageTypeCombo, isRoutedMessage } from '../src/protocol.js';
|
||||
|
||||
describe('Waggle Dance protocol', () => {
|
||||
describe('validateMessageTypeCombo', () => {
|
||||
it('validates correct request subtypes', () => {
|
||||
expect(validateMessageTypeCombo('request', 'knowledge_check')).toBe(true);
|
||||
expect(validateMessageTypeCombo('request', 'task_delegation')).toBe(true);
|
||||
expect(validateMessageTypeCombo('request', 'skill_request')).toBe(true);
|
||||
expect(validateMessageTypeCombo('request', 'model_recommendation')).toBe(true);
|
||||
});
|
||||
|
||||
it('validates correct response subtypes', () => {
|
||||
expect(validateMessageTypeCombo('response', 'knowledge_match')).toBe(true);
|
||||
expect(validateMessageTypeCombo('response', 'task_claim')).toBe(true);
|
||||
});
|
||||
|
||||
it('validates correct broadcast subtypes', () => {
|
||||
expect(validateMessageTypeCombo('broadcast', 'discovery')).toBe(true);
|
||||
expect(validateMessageTypeCombo('broadcast', 'routed_share')).toBe(true);
|
||||
expect(validateMessageTypeCombo('broadcast', 'skill_share')).toBe(true);
|
||||
expect(validateMessageTypeCombo('broadcast', 'model_recipe')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid type-subtype combinations', () => {
|
||||
expect(validateMessageTypeCombo('request', 'discovery')).toBe(false);
|
||||
expect(validateMessageTypeCombo('broadcast', 'knowledge_check')).toBe(false);
|
||||
expect(validateMessageTypeCombo('response', 'skill_share')).toBe(false);
|
||||
expect(validateMessageTypeCombo('request', 'task_claim')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects completely invalid types', () => {
|
||||
expect(validateMessageTypeCombo('invalid' as MessageType, 'discovery')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects completely invalid subtypes', () => {
|
||||
expect(validateMessageTypeCombo('request', 'invalid' as MessageSubtype)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRoutedMessage', () => {
|
||||
it('identifies routed_share as routed', () => {
|
||||
expect(isRoutedMessage('routed_share')).toBe(true);
|
||||
});
|
||||
|
||||
it('identifies non-routed subtypes', () => {
|
||||
expect(isRoutedMessage('discovery')).toBe(false);
|
||||
expect(isRoutedMessage('knowledge_check')).toBe(false);
|
||||
expect(isRoutedMessage('task_claim')).toBe(false);
|
||||
expect(isRoutedMessage('skill_share')).toBe(false);
|
||||
expect(isRoutedMessage('model_recipe')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user