import { afterEach, describe, expect, it, vi } from 'vitest'; import Fastify, { type FastifyInstance } from 'fastify'; import { buildTaskFit, type ExecutorCandidate } from '@waggle/agent'; import type { ExecutorBrief } from '../../src/local/executor-brief.js'; import { routeProposalRoutes } from '../../src/local/routes/route-proposals.js'; const NOW_MS = Date.UTC(2026, 6, 15, 10, 0, 0); const WORKSPACE_ID = 'workspace-alpha'; const PROMPT = 'Implement the router tests'; const servers: FastifyInstance[] = []; afterEach(async () => { vi.useRealTimers(); await Promise.all(servers.splice(0).map((server) => server.close())); }); function externalCandidate(overrides: Partial = {}): ExecutorCandidate { const id = overrides.id ?? 'external:codex'; return { id, kind: 'external', displayName: 'Codex CLI', taskFit: buildTaskFit(id), authClass: 'subscription-cli', installed: true, healthy: true, rateLimit: { state: 'available' }, supportsHeadless: true, egressDestination: 'OpenAI', ...overrides, }; } function personaCandidate(overrides: Partial = {}): ExecutorCandidate { const id = overrides.id ?? 'persona:coder'; return { id, kind: 'persona', displayName: 'Coder', taskFit: buildTaskFit(id), authClass: 'api-key', installed: true, healthy: true, rateLimit: { state: 'available' }, supportsHeadless: false, egressDestination: null, ...overrides, }; } function briefFixture( briefHash = 'brief-full-hash', text = '## Waggle task context\nMemory evidence:\n- Full retained context', ): ExecutorBrief { return { text, items: [{ frameId: 'frame-1', date: '2026-07-14', source: 'user_stated', preview: 'Full retained context', content: 'Full retained context', }], briefHash, chars: text.length, blocked: false, }; } interface HarnessOptions { snapshots: ExecutorCandidate[][]; briefProvider?: (opts: { workspaceId: string; prompt: string; excludeFrameIds?: string[]; }) => Promise; personaDispatcher?: (...args: unknown[]) => Promise<{ content: string; approvalRequired: boolean; error?: string; }>; } async function createHarness(options: HarnessOptions) { const server = Fastify({ logger: false }); servers.push(server); let snapshotIndex = 0; const snapshot = vi.fn(async () => { const candidates = options.snapshots[Math.min(snapshotIndex, options.snapshots.length - 1)] ?? []; snapshotIndex += 1; return candidates; }); const briefProvider = options.briefProvider ?? vi.fn(async () => briefFixture()); const personaDispatcher = options.personaDispatcher ?? vi.fn(async () => ({ content: 'Dispatched', approvalRequired: false, })); const toolRunPayloads: unknown[] = []; server.decorate('localConfig', { dataDir: '', port: 4567, host: '127.0.0.1', litellmUrl: '', } as never); server.decorate('agentState', { wsSessionToken: 'ws-session-token' } as never); server.decorate('executorRegistry', { snapshot, noteRateLimit: vi.fn(), noteHealthy: vi.fn(), } as never); server.decorate('routeProposalBriefProvider', briefProvider as never); server.decorate('routeProposalPersonaDispatcher', personaDispatcher as never); server.post('/api/tools/run', async (request, reply) => { toolRunPayloads.push(request.body); return reply.code(202).send({ roomId: 'room-1', runs: [{ runId: 'run-1' }], }); }); await server.register(routeProposalRoutes); return { server, snapshot, briefProvider, personaDispatcher, toolRunPayloads }; } async function propose( server: FastifyInstance, overrides: Record = {}, ) { return server.inject({ method: 'POST', url: '/api/route-proposals', payload: { workspaceId: WORKSPACE_ID, prompt: PROMPT, category: 'coding', ...overrides, }, }); } describe('route proposal routes', () => { it('confirms an external proposal with the reviewed brief, read-only access, and attribution', async () => { const fullBrief = briefFixture('brief-full-hash'); const briefProvider = vi.fn(async () => fullBrief); const { server, toolRunPayloads } = await createHarness({ snapshots: [[externalCandidate()], [externalCandidate()]], briefProvider, }); const proposed = await propose(server); expect(proposed.statusCode).toBe(200); const proposal = proposed.json() as { routeDecisionId: string; selected: { id: string } }; expect(proposal.selected.id).toBe('external:codex'); const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${proposal.routeDecisionId}/confirm`, payload: {}, }); expect(confirmed.statusCode).toBe(200); expect(confirmed.json()).toEqual({ status: 'dispatched', mode: 'external', roomId: 'room-1', runId: 'run-1', }); // Retrieval runs exactly once, at propose time — confirm must never // re-query memory (removed frames could be backfilled by new results). expect(briefProvider).toHaveBeenCalledTimes(1); expect(briefProvider).toHaveBeenNthCalledWith(1, { workspaceId: WORKSPACE_ID, prompt: PROMPT, }); expect(toolRunPayloads).toEqual([{ toolId: 'codex', workspaceIds: [WORKSPACE_ID], prompt: `${fullBrief.text}\n\n${PROMPT}`, access: 'read-only', attribution: { routeDecisionId: proposal.routeDecisionId, briefHash: fullBrief.briefHash, }, }]); }); it('refuses an executor override whose egress destination differs from the disclosure', async () => { const codex = externalCandidate(); const claude = externalCandidate({ id: 'external:claude-code', displayName: 'Claude Code', egressDestination: 'Anthropic', }); const { server, toolRunPayloads } = await createHarness({ snapshots: [[codex, claude], [codex, claude]], }); const proposed = await propose(server); const proposal = proposed.json() as { routeDecisionId: string; selected: { id: string }; alternatives: Array<{ id: string }>; }; const alternative = proposal.alternatives.find((item) => item.id !== proposal.selected.id); expect(alternative).toBeDefined(); const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${proposal.routeDecisionId}/confirm`, payload: { executorId: alternative!.id }, }); expect(confirmed.statusCode).toBe(409); expect(confirmed.json()).toMatchObject({ error: 'revalidation_failed' }); expect((confirmed.json() as { reason: string }).reason).toContain('different destination'); expect(toolRunPayloads).toHaveLength(0); // Proposal stays claimable: a same-destination confirm still succeeds. const retry = await server.inject({ method: 'POST', url: `/api/route-proposals/${proposal.routeDecisionId}/confirm`, payload: {}, }); expect(retry.statusCode).toBe(200); }); it('dispatches without any memory when the user removes every disclosed frame', async () => { const fullBrief = briefFixture('brief-full-hash'); const briefProvider = vi.fn(async () => fullBrief); const { server, toolRunPayloads } = await createHarness({ snapshots: [[externalCandidate()], [externalCandidate()]], briefProvider, }); const proposed = await propose(server); const proposal = proposed.json() as { routeDecisionId: string }; const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${proposal.routeDecisionId}/confirm`, payload: { removeFrameIds: ['frame-1'] }, }); expect(confirmed.statusCode).toBe(200); expect(briefProvider).toHaveBeenCalledTimes(1); expect(toolRunPayloads).toHaveLength(1); const payload = toolRunPayloads[0] as { prompt: string; attribution: { routeDecisionId: string; briefHash?: string }; }; // No brief text, no undisclosed frames, no briefHash. expect(payload.prompt).toBe(PROMPT); expect(payload.attribution).toEqual({ routeDecisionId: proposal.routeDecisionId }); }); it('dispatches a persona with the bare persona id, router origin, and websocket token', async () => { const personaDispatcher = vi.fn(async () => ({ content: 'Done', approvalRequired: false })); const { server, briefProvider, toolRunPayloads } = await createHarness({ snapshots: [[personaCandidate()], [personaCandidate()]], personaDispatcher, }); const proposed = await propose(server); const { routeDecisionId } = proposed.json() as { routeDecisionId: string }; const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: {}, }); expect(confirmed.statusCode).toBe(200); expect(confirmed.json()).toEqual({ status: 'dispatched', mode: 'internal', resultText: 'Done' }); expect(personaDispatcher).toHaveBeenCalledWith({ port: 4567, sessionToken: 'ws-session-token', message: PROMPT, workspace: WORKSPACE_ID, session: WORKSPACE_ID, persona: 'coder', proposeHeld: true, origin: 'router', }); expect(briefProvider).not.toHaveBeenCalled(); expect(toolRunPayloads).toEqual([]); }); it('fails revalidation when the selected external tool is uninstalled', async () => { const { server, snapshot, briefProvider, toolRunPayloads } = await createHarness({ snapshots: [ [externalCandidate()], [externalCandidate({ installed: false, healthy: false })], ], }); const proposed = await propose(server); const { routeDecisionId } = proposed.json() as { routeDecisionId: string }; const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: {}, }); expect(confirmed.statusCode).toBe(409); expect(confirmed.json()).toMatchObject({ error: 'revalidation_failed', reason: 'not installed', }); expect(snapshot).toHaveBeenCalledTimes(2); expect(briefProvider).toHaveBeenCalledTimes(1); expect(toolRunPayloads).toEqual([]); }); it('expires proposals after ten minutes', async () => { vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(NOW_MS); const { server, snapshot } = await createHarness({ snapshots: [[personaCandidate()]] }); const proposed = await propose(server); const { routeDecisionId } = proposed.json() as { routeDecisionId: string }; vi.setSystemTime(NOW_MS + 10 * 60_000); const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: {}, }); expect(confirmed.statusCode).toBe(404); expect(confirmed.json()).toEqual({ error: 'route_proposal_not_found' }); expect(snapshot).toHaveBeenCalledTimes(1); }); it('allows a proposal to be confirmed only once', async () => { const { server, personaDispatcher, snapshot } = await createHarness({ snapshots: [[personaCandidate()], [personaCandidate()]], }); const proposed = await propose(server); const { routeDecisionId } = proposed.json() as { routeDecisionId: string }; const first = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: {}, }); const second = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: {}, }); expect(first.statusCode).toBe(200); expect(second.statusCode).toBe(409); expect(second.json()).toEqual({ error: 'route_proposal_already_claimed', status: 'dispatched', }); expect(snapshot).toHaveBeenCalledTimes(2); expect(personaDispatcher).toHaveBeenCalledTimes(1); }); it('never selects an external executor for a private task', async () => { const { server, briefProvider } = await createHarness({ snapshots: [[externalCandidate(), personaCandidate()]], }); const proposed = await propose(server, { privacy: 'private' }); expect(proposed.statusCode).toBe(200); expect(proposed.json()).toMatchObject({ selected: { id: 'persona:coder' }, rejected: [{ id: 'external:codex', reason: 'blocked by private-task policy (egress to OpenAI)', }], egress: null, costLine: 'runs on your configured API key', }); expect(briefProvider).not.toHaveBeenCalled(); }); it('rejects a proposed route and prevents it from being claimed later', async () => { const { server, personaDispatcher } = await createHarness({ snapshots: [[personaCandidate()]], }); const proposed = await propose(server); const { routeDecisionId } = proposed.json() as { routeDecisionId: string }; const rejected = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/reject`, }); const confirmed = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: {}, }); expect(rejected.statusCode).toBe(200); expect(rejected.json()).toEqual({ status: 'rejected' }); expect(confirmed.statusCode).toBe(409); expect(confirmed.json()).toEqual({ error: 'route_proposal_already_claimed', status: 'rejected', }); expect(personaDispatcher).not.toHaveBeenCalled(); }); it('strictly validates proposal and confirmation input', async () => { const { server, snapshot } = await createHarness({ snapshots: [[personaCandidate()]], }); const extraProposalField = await propose(server, { unexpected: true }); const emptyPrompt = await propose(server, { prompt: '' }); const validProposal = await propose(server); const { routeDecisionId } = validProposal.json() as { routeDecisionId: string }; const extraConfirmField = await server.inject({ method: 'POST', url: `/api/route-proposals/${routeDecisionId}/confirm`, payload: { unexpected: true }, }); expect(extraProposalField.statusCode).toBe(400); expect(emptyPrompt.statusCode).toBe(400); expect(extraConfirmField.statusCode).toBe(400); expect(snapshot).toHaveBeenCalledTimes(1); }); });