This commit is contained in:
429
packages/server/tests/local/route-proposals.test.ts
Normal file
429
packages/server/tests/local/route-proposals.test.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
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> = {}): 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> = {}): 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<ExecutorBrief>;
|
||||
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<string, unknown> = {},
|
||||
) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user