moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View 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/);
});
});
});

View 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');
});
});

View 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);
});
});
});