This commit is contained in:
260
packages/server/tests/waggle-dance-bridge.test.ts
Normal file
260
packages/server/tests/waggle-dance-bridge.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* AI-OS Phase 1C — bridge tests.
|
||||
*
|
||||
* Two layers:
|
||||
* 1. Unit tests for the pure mapping functions (categorizeSubtype,
|
||||
* buildLegacyContent).
|
||||
* 2. End-to-end test: POST /api/waggle-dance/signal → GET
|
||||
* /api/waggle/signals (legacy UI endpoint) returns the bridged
|
||||
* signal.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { MindDB, FrameStore, SessionStore } from '@waggle/core';
|
||||
import { buildLocalServer } from '../src/local/index.js';
|
||||
import { injectWithAuth } from './test-utils.js';
|
||||
import {
|
||||
categorizeSubtype,
|
||||
buildLegacyContent,
|
||||
} from '../src/local/waggle-dance-bridge.js';
|
||||
import type { WaggleMessage, MessageSubtype } from '@waggle/shared';
|
||||
|
||||
// ── Unit: categorizeSubtype ─────────────────────────────────────────
|
||||
|
||||
describe('categorizeSubtype', () => {
|
||||
it.each<[MessageSubtype, string]>([
|
||||
['discovery', 'discovery'],
|
||||
['knowledge_check', 'discovery'],
|
||||
['skill_request', 'discovery'],
|
||||
['task_delegation', 'handoff'],
|
||||
['skill_share', 'handoff'],
|
||||
['routed_share', 'handoff'],
|
||||
['knowledge_match', 'insight'],
|
||||
['task_claim', 'coordination'],
|
||||
['model_recipe', 'coordination'],
|
||||
['model_recommendation', 'coordination'],
|
||||
])('maps %s → %s', (subtype, expected) => {
|
||||
expect(categorizeSubtype(subtype)).toBe(expected);
|
||||
});
|
||||
|
||||
it('falls back to coordination on unknown subtype (defense in depth)', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(categorizeSubtype('made_up' as any)).toBe('coordination');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Unit: buildLegacyContent ────────────────────────────────────────
|
||||
|
||||
function makeMsg(overrides: Partial<WaggleMessage>): WaggleMessage {
|
||||
return {
|
||||
id: 'm-1',
|
||||
teamId: 'personal::test',
|
||||
senderId: 'test',
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
content: {},
|
||||
referenceId: null,
|
||||
routing: null,
|
||||
createdAt: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildLegacyContent', () => {
|
||||
it('uses content.topic when present', () => {
|
||||
const out = buildLegacyContent(
|
||||
makeMsg({ subtype: 'discovery', content: { topic: 'webhook rotation' } }),
|
||||
);
|
||||
expect(out).toBe('discovery: webhook rotation');
|
||||
});
|
||||
|
||||
it('falls back to content.task', () => {
|
||||
const out = buildLegacyContent(
|
||||
makeMsg({
|
||||
type: 'request',
|
||||
subtype: 'task_delegation',
|
||||
content: { task: 'analyze Q4', role: 'analyst' },
|
||||
}),
|
||||
);
|
||||
expect(out).toBe('task_delegation: analyze Q4');
|
||||
});
|
||||
|
||||
it('falls back to content.query', () => {
|
||||
const out = buildLegacyContent(
|
||||
makeMsg({
|
||||
type: 'request',
|
||||
subtype: 'knowledge_check',
|
||||
content: { query: 'sql injection' },
|
||||
}),
|
||||
);
|
||||
expect(out).toBe('knowledge_check: sql injection');
|
||||
});
|
||||
|
||||
it('summarizes keys when no topic-like field exists', () => {
|
||||
const out = buildLegacyContent(
|
||||
makeMsg({
|
||||
type: 'response',
|
||||
subtype: 'knowledge_match',
|
||||
content: { matchedEntities: ['e1', 'e2'], confidence: 0.91 },
|
||||
}),
|
||||
);
|
||||
expect(out).toMatch(/knowledge_match \(/);
|
||||
expect(out).toContain('matchedEntities');
|
||||
expect(out).toContain('confidence');
|
||||
});
|
||||
|
||||
it('handles empty content gracefully', () => {
|
||||
const out = buildLegacyContent(
|
||||
makeMsg({ type: 'broadcast', subtype: 'discovery', content: {} }),
|
||||
);
|
||||
expect(out).toBe('discovery');
|
||||
});
|
||||
});
|
||||
|
||||
// ── End-to-end: POST v2 signal → GET legacy signals ─────────────────
|
||||
|
||||
function createTmpDir(prefix: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), `waggle-wd-bridge-${prefix}-`));
|
||||
}
|
||||
|
||||
function cleanupDir(dir: string): void {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
describe('WaggleDance bridge (v2 → legacy)', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDir('e2e');
|
||||
const personalPath = path.join(tmpDir, 'personal.mind');
|
||||
const mind = new MindDB(personalPath);
|
||||
const sessions = new SessionStore(mind);
|
||||
const frames = new FrameStore(mind);
|
||||
const s = sessions.create('bridge-test');
|
||||
frames.createIFrame(s.gop_id, 'bridge-test seed', 'normal');
|
||||
mind.close();
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
await server.ready();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (server) await server.close();
|
||||
if (tmpDir) cleanupDir(tmpDir);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (server?.signalBus) server.signalBus.clear();
|
||||
});
|
||||
|
||||
it('a discovery posted to /api/waggle-dance/signal appears at /api/waggle/signals', async () => {
|
||||
const postRes = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/waggle-dance/signal',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
payload: {
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
senderId: 'claude-code-hook',
|
||||
content: { tool: 'claude-code', topic: 'webhook secret rotation' },
|
||||
},
|
||||
});
|
||||
expect(postRes.statusCode).toBe(201);
|
||||
|
||||
const getRes = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/waggle/signals?limit=10',
|
||||
});
|
||||
expect(getRes.statusCode).toBe(200);
|
||||
const body = getRes.json();
|
||||
expect(body.signals).toBeDefined();
|
||||
expect(body.signals.length).toBeGreaterThan(0);
|
||||
const bridged = body.signals.find((s: { metadata?: { subtype?: string } }) =>
|
||||
s.metadata?.subtype === 'discovery'
|
||||
);
|
||||
expect(bridged).toBeTruthy();
|
||||
expect(bridged.type).toBe('waggle-dance:discovery');
|
||||
expect(bridged.workspaceId).toBe('personal::claude-code-hook');
|
||||
expect(bridged.content).toContain('webhook secret rotation');
|
||||
expect(bridged.metadata.tool).toBe('claude-code');
|
||||
expect(bridged.metadata.senderId).toBe('claude-code-hook');
|
||||
});
|
||||
|
||||
it('critical priority overrides to alert category', async () => {
|
||||
await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/waggle-dance/signal',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
payload: {
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
senderId: 'cursor-hook',
|
||||
content: {
|
||||
tool: 'cursor',
|
||||
topic: 'security finding',
|
||||
priority: 'critical',
|
||||
},
|
||||
},
|
||||
});
|
||||
const getRes = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/waggle/signals?limit=5',
|
||||
});
|
||||
const bridged = getRes.json().signals[0];
|
||||
expect(bridged.type).toBe('waggle-dance:alert');
|
||||
expect(bridged.metadata.priority).toBe('critical');
|
||||
});
|
||||
|
||||
it('preserves the full protocol message in metadata for drilldown', async () => {
|
||||
await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/waggle-dance/signal',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
payload: {
|
||||
type: 'broadcast',
|
||||
subtype: 'routed_share',
|
||||
senderId: 'hermes',
|
||||
content: { tool: 'hermes', payload: { docId: 'd-7' } },
|
||||
routing: [{ userId: 'u-1', reason: 'reviewer' }],
|
||||
},
|
||||
});
|
||||
const getRes = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/waggle/signals?limit=5',
|
||||
});
|
||||
const bridged = getRes.json().signals[0];
|
||||
expect(bridged.metadata.protocolMessage).toBeTruthy();
|
||||
expect(bridged.metadata.protocolMessage.subtype).toBe('routed_share');
|
||||
expect(bridged.metadata.protocolMessage.routing).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('a knowledge_match response routes through the bridge as insight', async () => {
|
||||
await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/waggle-dance/signal',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
payload: {
|
||||
type: 'response',
|
||||
subtype: 'knowledge_match',
|
||||
senderId: 'cursor-hook',
|
||||
referenceId: 'orig-request-id',
|
||||
content: { matchedEntities: ['e1'], confidence: 0.9 },
|
||||
},
|
||||
});
|
||||
const getRes = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/waggle/signals?limit=5',
|
||||
});
|
||||
const bridged = getRes.json().signals[0];
|
||||
expect(bridged.type).toBe('waggle-dance:insight');
|
||||
expect(bridged.metadata.referenceId).toBe('orig-request-id');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user