Files
waggle-os/packages/server/tests/waggle-dance-routes.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

474 lines
18 KiB
TypeScript

/**
* AI-OS Phase 1B — waggle-dance routes integration tests.
*
* Boots a real local server, posts signals, queries the bus.
* Verifies the cross-tool activity bus end-to-end via REST.
*/
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';
function createTmpDir(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), `waggle-wd-${prefix}-`));
}
function cleanupDir(dir: string): void {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
/* best-effort */
}
}
describe('Waggle-Dance routes (Phase 1B)', () => {
let server: FastifyInstance;
let tmpDir: string;
beforeAll(async () => {
tmpDir = createTmpDir('routes');
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('wd-routes-test');
frames.createIFrame(s.gop_id, 'wd-routes-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(() => {
// Reset the bus between tests so we don't have cross-test pollution.
if (server?.signalBus) server.signalBus.clear();
});
// ── POST /api/waggle-dance/signal ────────────────────────────────
it('POST returns 201 and persists a discovery signal', async () => {
const res = 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 rotation' },
},
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.dispatched).toBe(true);
expect(body.message.subtype).toBe('discovery');
expect(body.message.teamId).toBe('personal::claude-code-hook');
expect(body.message.id).toBeTruthy();
});
it('POST defaults senderId to "local" and derives a personal team', async () => {
const res = await injectWithAuth(server, {
method: 'POST',
url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast',
subtype: 'discovery',
content: { topic: 'no-sender case' },
},
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.message.senderId).toBe('local');
expect(body.message.teamId).toBe('personal::local');
});
it('POST rejects invalid type/subtype combo', async () => {
const res = await injectWithAuth(server, {
method: 'POST',
url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'request',
subtype: 'discovery', // discovery is broadcast-only
content: {},
},
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toContain('Invalid type-subtype combination');
});
it('POST rejects unknown subtype', async () => {
const res = await injectWithAuth(server, {
method: 'POST',
url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast',
subtype: 'made_up_subtype',
content: {},
},
});
expect(res.statusCode).toBe(400);
});
it('POST accepts a routed_share with routing list', async () => {
const res = 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: 'subscribed' }],
},
});
expect(res.statusCode).toBe(201);
const body = res.json();
expect(body.message.routing).toHaveLength(1);
});
it('POST accepts a knowledge_match response with referenceId', async () => {
const res = 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-msg-id',
content: { matchedEntities: ['e-1'] },
},
});
expect(res.statusCode).toBe(201);
expect(res.json().message.referenceId).toBe('orig-msg-id');
});
// ── GET /api/waggle-dance/signals ────────────────────────────────
it('scopes an external run credential to its own sender, Room, and workspace', async () => {
const room = server.agentRunRegistry.createRoom({
workspaceIds: ['default'], source: 'external_tool', title: 'External room', task: 'Collaborate',
});
const run = server.agentRunRegistry.createWorker({
parentRunId: room.id, workspaceId: 'default', source: 'external_tool',
executor: { kind: 'external_tool', toolId: 'codex' }, title: 'Codex', task: room.task,
});
const token = server.agentRunRegistry.issueCredential(run.id);
const spoofed = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': token },
payload: {
type: 'broadcast', subtype: 'discovery', senderId: 'run::someone-else',
teamId: 'room::someone-else', content: { runId: 'someone-else', topic: 'spoof' },
},
});
expect(spoofed.statusCode).toBe(403);
expect(spoofed.json().code).toBe('RUN_SCOPE_VIOLATION');
const sent = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': token },
payload: {
type: 'request', subtype: 'knowledge_check', content: { query: 'Who has the schema?' },
},
});
expect(sent.statusCode).toBe(201);
expect(sent.json().message).toMatchObject({
senderId: `run::${run.id}`,
teamId: `room::${room.id}`,
content: { roomId: room.id, runId: run.id, workspaceId: 'default' },
});
const discovery = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': token },
payload: {
type: 'broadcast', subtype: 'discovery',
content: {
summary: 'Found the migration owner and saved the result.',
frameId: '42', memoryWorkspace: 'default',
},
},
});
expect(discovery.statusCode).toBe(201);
const updated = server.agentRunRegistry.get(run.id);
expect(updated).toMatchObject({
status: 'running',
result: { summary: 'Found the migration owner and saved the result.' },
memoryRefs: {
status: 'complete',
workspaceFrameIds: { default: [42] },
},
});
expect(updated?.memoryRefs.personalFrameIds).toHaveLength(1);
expect(new FrameStore(server.multiMind.personal).getById(updated!.memoryRefs.personalFrameIds![0])?.content)
.toContain('Found the migration owner');
const forbiddenRead = await server.inject({
method: 'GET', url: '/api/waggle-dance/signals?teamId=room%3A%3Aother',
headers: { 'x-waggle-run-token': token },
});
expect(forbiddenRead.statusCode).toBe(403);
const ownRead = await server.inject({
method: 'GET', url: '/api/waggle-dance/signals',
headers: { 'x-waggle-run-token': token },
});
expect(ownRead.statusCode).toBe(200);
expect((ownRead.json() as { signals: Array<{ teamId: string }> }).signals.every(
(signal) => signal.teamId === `room::${room.id}`,
)).toBe(true);
});
it('relays Room requests, correlated responses, and broadcasts exactly once', async () => {
const room = server.agentRunRegistry.createRoom({
workspaceIds: ['workspace-a', 'workspace-b'], source: 'external_tool',
title: 'Collaboration room', task: 'Solve together',
});
const runA = server.agentRunRegistry.createWorker({
parentRunId: room.id, workspaceId: 'workspace-a', source: 'external_tool',
executor: { kind: 'external_tool', toolId: 'codex' }, title: 'Codex', task: room.task,
});
const runB = server.agentRunRegistry.createWorker({
parentRunId: room.id, workspaceId: 'workspace-b', source: 'external_tool',
executor: { kind: 'external_tool', toolId: 'claude-code' }, title: 'Claude Code', task: room.task,
});
const otherRoom = server.agentRunRegistry.createRoom({
workspaceIds: ['workspace-c'], source: 'external_tool',
title: 'Other room', task: 'Stay isolated',
});
const runC = server.agentRunRegistry.createWorker({
parentRunId: otherRoom.id, workspaceId: 'workspace-c', source: 'external_tool',
executor: { kind: 'external_tool', toolId: 'cursor' }, title: 'Cursor', task: otherRoom.task,
});
const tokenA = server.agentRunRegistry.issueCredential(runA.id);
const tokenB = server.agentRunRegistry.issueCredential(runB.id);
const tokenC = server.agentRunRegistry.issueCredential(runC.id);
const knowledgeRequest = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': tokenA },
payload: {
type: 'request', subtype: 'knowledge_check',
content: { query: 'Who owns the schema?' },
},
});
expect(knowledgeRequest.statusCode).toBe(201);
const knowledgeRequestId = knowledgeRequest.json().message.id as string;
const taskRequest = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': tokenA },
payload: {
type: 'request', subtype: 'task_delegation',
content: { task: 'Inspect the migrations', role: 'analyst' },
},
});
expect(taskRequest.statusCode).toBe(201);
const taskRequestId = taskRequest.json().message.id as string;
const peerRead = await server.inject({
method: 'GET', url: '/api/waggle-dance/signals',
headers: { 'x-waggle-run-token': tokenB },
});
expect(peerRead.statusCode).toBe(200);
const peerSignals = peerRead.json().signals as Array<{ id: string; senderId: string; teamId: string }>;
expect(peerSignals.filter((signal) => signal.id === knowledgeRequestId)).toHaveLength(1);
expect(peerSignals.filter((signal) => signal.id === taskRequestId)).toHaveLength(1);
expect(peerSignals.every((signal) => signal.senderId === `run::${runA.id}`)).toBe(true);
expect(peerSignals.every((signal) => signal.teamId === `room::${room.id}`)).toBe(true);
const response = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': tokenB },
payload: {
type: 'response', subtype: 'knowledge_match', referenceId: knowledgeRequestId,
content: { matchedEntities: ['schema-owner'] },
},
});
expect(response.statusCode).toBe(201);
const responseId = response.json().message.id as string;
const broadcast = await server.inject({
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json', 'x-waggle-run-token': tokenB },
payload: {
type: 'broadcast', subtype: 'routed_share',
content: { payload: { owner: 'run-b' } },
},
});
expect(broadcast.statusCode).toBe(201);
const broadcastId = broadcast.json().message.id as string;
const requesterRead = await server.inject({
method: 'GET', url: '/api/waggle-dance/signals',
headers: { 'x-waggle-run-token': tokenA },
});
expect(requesterRead.statusCode).toBe(200);
const requesterSignals = requesterRead.json().signals as Array<{
id: string; senderId: string; referenceId?: string | null;
}>;
for (const id of [knowledgeRequestId, taskRequestId, responseId, broadcastId]) {
expect(requesterSignals.filter((signal) => signal.id === id)).toHaveLength(1);
}
expect(requesterSignals.find((signal) => signal.id === responseId)).toMatchObject({
senderId: `run::${runB.id}`,
referenceId: knowledgeRequestId,
});
const isolatedRead = await server.inject({
method: 'GET', url: '/api/waggle-dance/signals',
headers: { 'x-waggle-run-token': tokenC },
});
expect(isolatedRead.statusCode).toBe(200);
expect(isolatedRead.json().signals).toHaveLength(0);
const forbiddenCrossRoomRead = await server.inject({
method: 'GET',
url: `/api/waggle-dance/signals?teamId=${encodeURIComponent(`room::${room.id}`)}`,
headers: { 'x-waggle-run-token': tokenC },
});
expect(forbiddenCrossRoomRead.statusCode).toBe(403);
expect(forbiddenCrossRoomRead.json().code).toBe('RUN_SCOPE_VIOLATION');
});
it('GET returns recently posted signals newest-first', async () => {
for (const topic of ['a', 'b', 'c']) {
await injectWithAuth(server, {
method: 'POST',
url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast',
subtype: 'discovery',
senderId: 'test',
content: { tool: 'cursor', topic },
},
});
}
const res = await injectWithAuth(server, {
method: 'GET',
url: '/api/waggle-dance/signals',
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.total).toBe(3);
expect(body.signals[0].content.topic).toBe('c');
expect(body.signals[2].content.topic).toBe('a');
});
it('GET filters by subtype', async () => {
await injectWithAuth(server, {
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast', subtype: 'discovery',
senderId: 't', content: { tool: 'claude-code' },
},
});
await injectWithAuth(server, {
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast', subtype: 'model_recipe',
senderId: 't', content: { name: 'recipe-1', model: 'haiku' },
},
});
const res = await injectWithAuth(server, {
method: 'GET',
url: '/api/waggle-dance/signals?subtype=discovery',
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.total).toBe(1);
expect(body.signals[0].subtype).toBe('discovery');
});
it('GET filters by tool', async () => {
for (const tool of ['claude-code', 'cursor', 'claude-code']) {
await injectWithAuth(server, {
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast', subtype: 'discovery',
senderId: 't', content: { tool },
},
});
}
const res = await injectWithAuth(server, {
method: 'GET',
url: '/api/waggle-dance/signals?tool=claude-code',
});
expect(res.statusCode).toBe(200);
expect(res.json().total).toBe(2);
});
it('GET honors limit', async () => {
for (let i = 0; i < 5; i++) {
await injectWithAuth(server, {
method: 'POST', url: '/api/waggle-dance/signal',
headers: { 'content-type': 'application/json' },
payload: {
type: 'broadcast', subtype: 'discovery',
senderId: 't', content: { tool: 'x', idx: i },
},
});
}
const res = await injectWithAuth(server, {
method: 'GET',
url: '/api/waggle-dance/signals?limit=2',
});
expect(res.statusCode).toBe(200);
expect(res.json().signals).toHaveLength(2);
});
it('GET rejects unknown subtype filter', async () => {
const res = await injectWithAuth(server, {
method: 'GET',
url: '/api/waggle-dance/signals?subtype=garbage',
});
expect(res.statusCode).toBe(400);
});
// ── round trip ────────────────────────────────────────────────────
it('POST → GET round trip preserves full message shape', 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: 'rotation', importance: 'high' },
},
});
const postedId = postRes.json().message.id;
const getRes = await injectWithAuth(server, {
method: 'GET',
url: '/api/waggle-dance/signals',
});
const signals = getRes.json().signals;
const found = signals.find((s: { id: string }) => s.id === postedId);
expect(found).toBeTruthy();
expect(found.content.topic).toBe('rotation');
expect(found.senderId).toBe('claude-code-hook');
expect(found.teamId).toBe('personal::claude-code-hook');
});
});