Files
pig/apps/piggy/test/chat-server.test.ts
T
claude 18d5f5bfc0
CI / verify (push) Successful in 7m10s
CI / publish (push) Has been skipped
Make Piggy part of the product rather than a guest in it
Piggy arrived as a chat panel bolted onto a CRM and then grew a workspace
around it. The layout was already right — the audit found the approval card
to be the best-designed object in the repo, and the account page's empty
panels less finished than anything in the workspace. What was wrong was
vocabulary: nobody had written the small things down, so both halves kept
inventing them.

Piggy was drawn with five different marks — a pig in the dock, a sparkle in
the sidebar and again on the model picker, a speech bubble on the Ask
buttons, and a stock robot glyph on every assistant message, which is the
one people look at most. There is now one mark. The composer, which is the
first control in the product since sign-in lands on /piggy, was the only
un-adapted shadcn field left: 6px radius against a 12px Send button it sat
8px from. A stat tile had been reinvented six times at three numeral scales,
and the same uppercase micro-label existed in five variants, two of them one
tab apart in the same rail. There were 63 hand-written font sizes: not a
scale, sixty-three opinions.

Underneath that, the focus ring was invisible. The global rule used
ring-accent, which Tailwind deliberately aliases onto the hover tint, so the
ring measured 1.01:1 against the light canvas — no visible focus indicator
anywhere in the product, for any accent, in either theme. It is ring-brand
now and measures 17:1. The warning, positive and info tones were darkened
until each clears 4.5:1 on a card, on inset and on its own chip, and the
light canvas moved to 98% so a card lifts without leaning on its shadow.

The mobile work is the part worth reading. A landscape phone gave the
transcript 28% of the viewport and a keyboard-up phone 16%, against a 45%
floor — and the fixed tab bar painted over the composer, covering the safety
sentence and half the Send button, because two source comments asserted the
bar stood down on short viewports and it never had. Both fixed and measured
by hit-testing rather than by screenshot. The composer itself was 64px tall
for a blank second line nobody typed, because the auto-resize effect sizes
to scrollHeight and scrollHeight counts rows — a CSS height could not win
against an inline style, so the attribute was the honest lever.

Verified across both themes driven through the app's own control: no
horizontal overflow on 15 routes at four viewports, 672 stat values that fit,
297 labels at exactly 11px/500, Escape returning focus to its opener rather
than the body on every overlay, and a rejected write no longer reporting
"Succeeded" with a green check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:22:15 -07:00

1144 lines
40 KiB
TypeScript

import assert from 'node:assert/strict';
import type { AddressInfo } from 'node:net';
import test from 'node:test';
import type { AgentSession, AgentSessionEvent, ToolDefinition } from '@earendil-works/pi-coding-agent';
import type { PiggyApprovalDecision, PiggyChatEvent, PiggyModelOption } from '@pig/core';
import type { Database } from '@pig/db';
import type { CreatePiggySessionOptions, PiggySession } from '../src/agent/session';
import {
startPiggyChatServer,
type PiggyChatServerOptions,
type PiggySessionFactory,
} from '../src/chat-server';
import type { PigWriteToolDeps } from '../src/write-tools';
const TOKEN = 'test-internal-token-for-piggy-000000';
const MODELS: PiggyModelOption[] = [
{
id: 'nvidia/nemotron-3-nano-30b-a3b',
label: 'Nemotron 3 Nano',
costPerMTokIn: 0.05,
costPerMTokOut: 0.2,
contextWindow: 131_072,
reasoning: true,
isDefault: true,
},
{
id: 'anthropic/claude-opus-5',
label: 'Claude Opus 5',
costPerMTokIn: 5,
costPerMTokOut: 25,
contextWindow: 200_000,
reasoning: true,
},
];
/**
* The chat server writes exactly two statements per turn — one insert, one
* update — so a fake that records them is enough to assert the whole ledger.
*/
interface RecordedRun {
values: Record<string, unknown>;
closed?: Record<string, unknown>;
}
function fakeDatabase(runs: RecordedRun[]): Database {
return {
insert: () => ({
values: (values: Record<string, unknown>) => ({
returning: async () => {
runs.push({ values });
return [{ id: `run-${runs.length}` }];
},
}),
}),
update: () => ({
set: (closed: Record<string, unknown>) => ({
where: async () => {
const run = runs.at(-1);
if (run) run.closed = closed;
},
}),
}),
// The daily spend, read once before every turn. Zero here, so nothing in
// this file is refused for cost; `turn-limits.test.ts` owns that ceiling.
select: () => ({ from: () => ({ where: async () => [{ spent: '0' }] }) }),
} as unknown as Database;
}
/**
* A turn, written the way the harness would perform it: the script is handed
* the tool set the server assembled and an emitter, and drives both. Every
* event it emits has a real `AgentSessionEvent` shape; the casts are there only
* because an `AssistantMessage` carries thirty fields the translation never
* reads, and building all of them would test the fake rather than the server.
*
* The third argument is the signal a disposed session aborts. The real harness
* passes one to every tool `execute` and fires it on `session.abort()`, which is
* how a tool parked on an approval learns that the reader has gone; a fake that
* did not would deadlock the moment a turn was abandoned mid-approval.
*/
type TurnScript = (
tools: readonly ToolDefinition[],
emit: (event: AgentSessionEvent) => void,
signal: AbortSignal,
) => Promise<void>;
interface SessionSpy {
options?: CreatePiggySessionOptions;
disposed: number;
}
function spy(): SessionSpy {
return { disposed: 0 };
}
function fakeSessions(script: TurnScript, watched: SessionSpy): PiggySessionFactory {
return async (options) => {
watched.options = options;
const listeners = new Set<(event: AgentSessionEvent) => void>();
const aborted = new AbortController();
let disposed = false;
const session = {
subscribe(listener: (event: AgentSessionEvent) => void) {
listeners.add(listener);
return () => listeners.delete(listener);
},
async prompt() {
await script(
options.tools,
(event) => {
// `createPiggySession`'s dispose aborts the session before dropping
// its listeners, so a disposed session stops generating rather than
// streaming into a socket nobody is reading.
if (disposed) throw new Error('The session was aborted.');
for (const listener of [...listeners]) listener(event);
},
aborted.signal,
);
},
async abort() {},
dispose() {},
} as unknown as AgentSession;
return {
session,
modelId: options.modelId ?? MODELS[0]!.id,
systemPrompt: 'You are Piggy.',
dispose: () => {
disposed = true;
aborted.abort();
watched.disposed += 1;
},
} satisfies PiggySession;
};
}
function textDelta(delta: string): AgentSessionEvent {
return {
type: 'message_update',
message: { role: 'assistant' },
assistantMessageEvent: { type: 'text_delta', contentIndex: 0, delta },
} as unknown as AgentSessionEvent;
}
function turnEnd(
input: number,
output: number,
stopReason = 'stop',
errorMessage?: string,
): AgentSessionEvent {
return {
type: 'turn_end',
message: {
role: 'assistant',
usage: { input, output },
stopReason,
...(errorMessage ? { errorMessage } : {}),
},
toolResults: [],
} as unknown as AgentSessionEvent;
}
function toolStart(id: string, name: string, args: unknown): AgentSessionEvent {
return {
type: 'tool_execution_start',
toolCallId: id,
toolName: name,
args,
} as unknown as AgentSessionEvent;
}
function toolEnd(
id: string,
name: string,
result: { content: { type: 'text'; text: string }[]; details?: unknown },
isError = false,
): AgentSessionEvent {
return {
type: 'tool_execution_end',
toolCallId: id,
toolName: name,
result,
isError,
} as unknown as AgentSessionEvent;
}
/** An event PIG does not render, which must never reach the wire. */
const noiseEvent = { type: 'queue_update', steering: [], followUp: [] } as unknown as AgentSessionEvent;
/** A tool needs only a name to be assembled, allowlisted and handed over. */
function namedTool(name: string): ToolDefinition {
return { name } as unknown as ToolDefinition;
}
async function startForTest(
t: { after: (fn: () => void) => void },
runs: RecordedRun[],
options: Partial<PiggyChatServerOptions>,
): Promise<string> {
const server = startPiggyChatServer(fakeDatabase(runs), {
port: 0,
internalToken: TOKEN,
models: MODELS,
createReadTools: () => [],
createWriteTools: () => [],
...options,
});
t.after(() => server.close());
// Port 0 is only resolved once the socket is bound.
await new Promise((resolve) => server.once('listening', resolve));
const { port } = server.address() as AddressInfo;
return `http://127.0.0.1:${port}`;
}
const PRINCIPAL = {
userId: '20000000-0000-4000-8000-000000000001',
email: 'ada@primeintellect.example',
name: 'Ada',
isPlatformAdmin: false,
teams: [{ team: 'supply', role: 'lead' }],
via: 'jwt',
scopes: ['read', 'write'],
};
function chatBody(overrides: Record<string, unknown> = {}): string {
return JSON.stringify({
principal: PRINCIPAL,
message: 'What is idle costing us?',
context: { type: 'page', route: '/capacity' },
mode: 'read_only',
conversationId: 'conv-1',
...overrides,
});
}
const authorised = { authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' };
function parseFrames(body: string): PiggyChatEvent[] {
return body
.trim()
.split('\n')
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as PiggyChatEvent);
}
test('health answers without a token, and nothing else does', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, { createSession: fakeSessions(async () => {}, spy()) });
const health = await fetch(`${base}/internal/health`);
assert.equal(health.status, 200);
assert.deepEqual(await health.json(), {
ok: true,
service: 'piggy-chat',
model: 'nvidia/nemotron-3-nano-30b-a3b',
});
assert.equal((await fetch(`${base}/internal/anything`)).status, 404);
assert.equal((await fetch(`${base}/internal/models`)).status, 401);
assert.equal(
(await fetch(`${base}/internal/chat`, { method: 'POST', body: chatBody() })).status,
401,
);
assert.equal(
(await fetch(`${base}/internal/approve`, { method: 'POST', body: '{}' })).status,
401,
);
});
test('the catalogue is served to the relay, so no client hard-codes a model list', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, { createSession: fakeSessions(async () => {}, spy()) });
const response = await fetch(`${base}/internal/models`, { headers: authorised });
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), MODELS);
});
test('a turn is translated into PIG events, and the harness noise is dropped', async (t) => {
const runs: RecordedRun[] = [];
const watched = spy();
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(noiseEvent);
emit(toolStart('call_1', 'pig_get_idle_capacity', { since: '2026-01-01' }));
emit(
toolEnd('call_1', 'pig_get_idle_capacity', {
content: [{ type: 'text', text: '{"totalIdleCostCents":1200000}' }],
details: { tool: 'pig_get_idle_capacity', result: { totalIdleCostCents: 1_200_000 } },
}),
);
emit(textDelta('Idle is '));
emit(textDelta('$12,000.'));
emit(turnEnd(1_240, 180));
}, watched),
});
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody(),
});
assert.equal(response.status, 200);
const frames = parseFrames(await response.text());
assert.deepEqual(frames[0], {
type: 'meta',
model: 'nvidia/nemotron-3-nano-30b-a3b',
mode: 'read_only',
conversationId: 'conv-1',
});
assert.deepEqual(frames[1], {
type: 'tool_call',
id: 'call_1',
name: 'pig_get_idle_capacity',
arguments: { since: '2026-01-01' },
});
// The bridge already structured the answer in `details`; the panel is handed
// that rather than the JSON string the model was shown.
assert.deepEqual(frames[2], {
type: 'tool_result',
id: 'call_1',
name: 'pig_get_idle_capacity',
ok: true,
result: { totalIdleCostCents: 1_200_000 },
});
assert.deepEqual(frames.slice(3, 5), [
{ type: 'content_delta', delta: 'Idle is ' },
{ type: 'content_delta', delta: '$12,000.' },
]);
assert.deepEqual(frames.at(-1), {
type: 'done',
inputTokens: 1_240,
outputTokens: 180,
// 1240 x 5 + 180 x 20 micro-cents, at $0.05/$0.20 per million tokens.
costMicroCents: 9_800,
});
assert.equal(frames.length, 6);
const run = runs[0];
assert.equal(run?.values.model, 'nvidia/nemotron-3-nano-30b-a3b');
assert.equal(run?.values.principalUserId, PRINCIPAL.userId);
assert.equal(run?.closed?.status, 'succeeded');
assert.equal(run?.closed?.summary, 'Idle is $12,000.');
assert.equal(run?.closed?.inputTokens, 1_240);
assert.equal(run?.closed?.costMicroCents, 9_800);
assert.ok(run?.closed?.finishedAt instanceof Date);
// The session is unwound on the happy path too; a leaked one holds an
// inference connection open and billing.
assert.equal(watched.disposed, 1);
});
// ------------------------------------------------- the harness's vocabulary
/**
* The four harness events PIG renders something for.
*
* Everything else is dropped on the server, deliberately: PIG's own event
* vocabulary is what the browser speaks, so a harness upgrade is a server change
* and never a client one.
*/
const MAPPED_EVENTS = [
'message_update',
'tool_execution_start',
'tool_execution_end',
'turn_end',
] as const;
/**
* Everything the harness can emit that PIG deliberately drops.
*
* Written out in full rather than implied by a `default:` arm, because the
* failure this catches is an upgrade that ADDS an event type — a harness that
* starts announcing, say, a delegated sub-agent, or a permission request, would
* otherwise fall into `default` and be dropped in silence for as long as it took
* somebody to notice the product had lost a feature it never knew it had.
*
* A few of these are worth knowing by name. `bash_execution_update` exists
* because the harness can run a shell; Piggy cannot, and if this event ever
* arrives on a Piggy session something is very wrong. `compaction_start` and
* `compaction_end` are the harness rewriting its own transcript, which PIG
* neither triggers nor persists — conversations are stored as PIG messages in
* Postgres. `auto_retry_start` is a retry PIG does not surface because the user
* is watching a spinner either way.
*/
const DROPPED_EVENTS = [
'agent_start',
'agent_end',
'agent_settled',
'turn_start',
'message_start',
'message_end',
'tool_execution_update',
'queue_update',
'compaction_start',
'compaction_end',
'entry_appended',
'session_info_changed',
'thinking_level_changed',
'auto_retry_start',
'auto_retry_end',
'summarization_retry_scheduled',
'summarization_retry_attempt_start',
'summarization_retry_finished',
'bash_execution_update',
] as const;
/**
* Both directions, at compile time.
*
* `[A] extends [B]` and back again is mutual assignability rather than
* assignability one way: a harness event missing from the lists fails, and a
* name in the lists the harness no longer emits fails too. Either way the
* failure is `tsc`, before a single test runs.
*/
type MutuallyAssignable<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
type HarnessEventType = AgentSessionEvent['type'];
type AccountedFor = (typeof MAPPED_EVENTS)[number] | (typeof DROPPED_EVENTS)[number];
const EVENT_VOCABULARY_IS_ACCOUNTED_FOR: MutuallyAssignable<HarnessEventType, AccountedFor> = true;
test('every harness event is either rendered or deliberately dropped', async (t) => {
// The type above is the real assertion; this keeps it from being deleted as
// an unused declaration, and states in words what it is for.
assert.equal(EVENT_VOCABULARY_IS_ACCOUNTED_FOR, true);
assert.equal(
new Set([...MAPPED_EVENTS, ...DROPPED_EVENTS]).size,
MAPPED_EVENTS.length + DROPPED_EVENTS.length,
'an event cannot be both rendered and dropped',
);
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
// Every dropped type, in one turn. None of them may reach the wire.
for (const type of DROPPED_EVENTS) emit({ type } as unknown as AgentSessionEvent);
emit(turnEnd(10, 5));
}, spy()),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text()),
);
assert.deepEqual(
frames.map((frame) => frame.type),
['meta', 'done'],
'a harness event PIG does not render must not reach the browser at all',
);
// And the turn still completed: dropping an event is not the same as being
// confused by one.
assert.equal(runs[0]?.closed?.status, 'succeeded');
});
test('a failed tool is reported as failed rather than as an empty answer', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(toolStart('call_1', 'pig_get_record', {}));
emit(
toolEnd(
'call_1',
'pig_get_record',
{ content: [{ type: 'text', text: 'Account 9c1 not found.' }] },
true,
),
);
emit(turnEnd(40, 10));
}, spy()),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text()),
);
assert.deepEqual(frames[2], {
type: 'tool_result',
id: 'call_1',
name: 'pig_get_record',
ok: false,
error: 'Account 9c1 not found.',
});
});
test('the chosen model is the one that answers, and is priced as itself', async (t) => {
const runs: RecordedRun[] = [];
const watched = spy();
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(textDelta('Considered.'));
emit(turnEnd(1_000_000, 1_000_000));
}, watched),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ modelId: 'anthropic/claude-opus-5' }),
}).then((response) => response.text()),
);
assert.equal(watched.options?.modelId, 'anthropic/claude-opus-5');
assert.equal(frames[0]?.type === 'meta' ? frames[0].model : null, 'anthropic/claude-opus-5');
const done = frames.at(-1);
// A million tokens each way at $5/$25 per million: 500 + 2500 cents, in
// micro-cents. Billing that at the nano's price would understate it a
// hundredfold, which is why the catalogue is the only price list.
assert.equal(done?.type === 'done' ? done.costMicroCents : null, 3_000_000_000);
const unknown = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ modelId: 'openai/o-something' }),
});
assert.equal(unknown.status, 400);
assert.deepEqual(await unknown.json(), { error: 'Unknown Piggy model.' });
});
test('read_only withholds the write tools; confirm hands them over', async (t) => {
const runs: RecordedRun[] = [];
const watched = spy();
const seen: PigWriteToolDeps[] = [];
const base = await startForTest(t, runs, {
createReadTools: () => [namedTool('pig_get_idle_capacity')],
createWriteTools: (deps) => {
seen.push(deps);
return [namedTool('pig_log_activity')];
},
createSession: fakeSessions(async (_tools, emit) => emit(turnEnd(10, 10)), watched),
});
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text());
// A tool the model is never shown is a tool it cannot be talked into calling.
assert.deepEqual(watched.options?.tools.map((tool) => tool.name), ['pig_get_idle_capacity']);
assert.equal(seen.length, 0);
await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ mode: 'confirm' }),
}).then((response) => response.text());
assert.deepEqual(watched.options?.tools.map((tool) => tool.name), [
'pig_get_idle_capacity',
'pig_log_activity',
]);
assert.equal(watched.options?.mode, 'confirm');
// Built as the caller, never as an elevated or synthetic principal: this is
// the identity `executeMutation` will check capabilities against.
assert.deepEqual(seen[0]?.principal, PRINCIPAL);
assert.equal(seen[0]?.mode, 'confirm');
});
test('a tool outside the PIG boundary never reaches the harness', async (t) => {
const runs: RecordedRun[] = [];
const watched = spy();
const base = await startForTest(t, runs, {
createReadTools: () => [namedTool('bash')],
createSession: fakeSessions(async () => {}, watched),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text()),
);
assert.deepEqual(frames.at(-1), {
type: 'error',
message: 'Piggy chat failed.',
code: 'agent_failed',
});
assert.equal(watched.options, undefined);
assert.equal(runs[0]?.closed?.status, 'failed');
assert.match(String(runs[0]?.closed?.error), /outside the PIG tool boundary/);
});
test('a malformed request is the only thing called an invalid request', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, { createSession: fakeSessions(async () => {}, spy()) });
for (const body of [
JSON.stringify({ principal: PRINCIPAL, message: '', mode: 'read_only', conversationId: 'c' }),
// The old bare `principalUserId`: a relay that has not been updated must
// fail here rather than write as nobody in particular.
JSON.stringify({
principalUserId: PRINCIPAL.userId,
message: 'Hello',
mode: 'read_only',
conversationId: 'c',
}),
// A principal the relay shaped differently is a relay that has drifted.
chatBody({ principal: { ...PRINCIPAL, extra: true } }),
chatBody({ principal: { ...PRINCIPAL, teams: [{ team: 'legal', role: 'lead' }] } }),
chatBody({ mode: 'god_mode' }),
chatBody({ conversationId: '' }),
]) {
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body,
});
assert.equal(response.status, 400);
assert.deepEqual(await response.json(), { error: 'Invalid Piggy chat request.' });
}
// No inference was attempted, so no run should have been opened for any.
assert.equal(runs.length, 0);
});
test('a fault raised mid-turn is not blamed on the user, and closes its run', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(textDelta('Idle is '));
throw new Error('inference stream stalled for 30000ms');
}, spy()),
});
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody(),
});
// The stream had already begun, so the turn ends as an error frame on a 200.
assert.equal(response.status, 200);
const frames = parseFrames(await response.text());
assert.deepEqual(frames.at(-1), {
type: 'error',
message: 'Piggy chat failed.',
code: 'agent_failed',
});
assert.equal(runs[0]?.closed?.status, 'failed');
assert.equal(runs[0]?.closed?.summary, 'Idle is');
});
test('a model that stops on its own error does not report a finished answer', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(textDelta('Idle is '));
emit(turnEnd(120, 4, 'error', 'upstream returned 502'));
}, spy()),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text()),
);
assert.deepEqual(frames.at(-1), {
type: 'error',
message: 'Piggy could not finish this answer.',
code: 'inference_failed',
});
assert.ok(!frames.some((frame) => frame.type === 'done'));
// The upstream body is not ours to relay to the browser, but it belongs in
// the ledger, where an operator can read it.
// The attempt count rides along with it: one go, which is the healthy
// reading and the one an operator needs in order to notice the days when it
// is not one.
assert.equal(runs[0]?.closed?.error, 'upstream returned 502 (1 attempt)');
});
test('a turn that ends badly still bills what it actually spent', async (t) => {
// The measurement this pins: a real turn made three tool calls, was billed
// for every model call behind them, and then the endpoint answered 429. The
// run closed as `failed` with inputTokens, outputTokens and costMicroCents
// all null, because the ledger was fed only from the `done` frame — which a
// failed turn never emits. The spend panel therefore under-reported, and in
// the reassuring direction, which is the worst way for a money figure to be
// wrong. Two model calls land before the fault here, so a ledger that reads
// only the last one would be wrong as well as short.
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(turnEnd(4_000, 100));
emit(turnEnd(6_000, 300));
throw new Error('429: rate limit reached');
}, spy()),
});
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text());
const closed = runs[0]?.closed;
assert.equal(closed?.status, 'failed');
assert.equal(closed?.inputTokens, 10_000);
assert.equal(closed?.outputTokens, 400);
// 10,000 x $0.05/Mtok + 400 x $0.20/Mtok, in micro-cents.
assert.equal(closed?.costMicroCents, 10_000 * 0.05 * 100 + 400 * 0.2 * 100);
});
test('an abandoned turn bills what it generated before the reader left', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(turnEnd(2_000, 50));
for (let index = 0; index < 40; index += 1) {
await new Promise((resolve) => setTimeout(resolve, 20));
emit(textDelta(`chunk ${index} `));
}
}, spy()),
});
const abort = new AbortController();
setTimeout(() => abort.abort(), 120);
await assert.rejects(
fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody(),
signal: abort.signal,
}).then((response) => response.text()),
);
await waitFor(() => runs[0]?.closed !== undefined);
assert.equal(runs[0]?.closed?.status, 'aborted');
// Generated, therefore billed, therefore in the ledger — a closed tab is not
// a refund.
assert.equal(runs[0]?.closed?.inputTokens, 2_000);
assert.equal(runs[0]?.closed?.outputTokens, 50);
assert.equal(runs[0]?.closed?.costMicroCents, 2_000 * 0.05 * 100 + 50 * 0.2 * 100);
});
test('an answer cut short by the token budget says so', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
emit(textDelta('The first half of a table'));
emit(turnEnd(900, 2_048, 'length'));
}, spy()),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, { method: 'POST', headers: authorised, body: chatBody() })
.then((response) => response.text()),
);
const done = frames.at(-1);
assert.equal(done?.type === 'done' ? done.finishReason : null, 'length');
});
test('a reader who leaves mid-answer closes the run as abandoned, not as running', async (t) => {
const runs: RecordedRun[] = [];
const watched = spy();
const base = await startForTest(t, runs, {
createSession: fakeSessions(async (_tools, emit) => {
for (let index = 0; index < 20; index += 1) {
await new Promise((resolve) => setTimeout(resolve, 20));
emit(textDelta(`chunk ${index} `));
}
}, watched),
});
const abort = new AbortController();
setTimeout(() => abort.abort(), 80);
await assert.rejects(
fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody(),
signal: abort.signal,
}).then((response) => response.text()),
);
await waitFor(() => runs[0]?.closed !== undefined);
// Without the finally this row stayed `running` for ever, and no later query
// could tell it from a turn still in flight.
assert.equal(runs[0]?.closed?.status, 'aborted');
// And the session is disposed rather than left generating tokens nobody will
// ever read.
assert.ok(watched.disposed >= 1);
});
// ------------------------------------------------------------- the approvals
/**
* A write tool in the shape `createPigWriteTools` builds: it proposes, waits,
* and reports what really happened as its own tool result — so a declined
* change cannot be summarised to the reader as a saved one.
*/
function proposingWriteTools(applied: string[]): (deps: PigWriteToolDeps) => ToolDefinition[] {
return ({ propose }) => [
{
name: 'pig_log_activity',
async execute() {
const decision = await propose({
tool: 'pig_log_activity',
kind: 'activity',
summary: 'Log a call on Northwind Robotics',
fields: [{ label: 'Subject', value: 'Capacity review' }],
});
if (decision === 'apply') applied.push('applied');
return {
content: [{ type: 'text', text: `The change was ${decision === 'apply' ? 'applied' : 'declined'}.` }],
details: {
tool: 'pig_log_activity',
kind: 'activity',
status: decision === 'apply' ? 'applied' : 'declined',
},
};
},
} as unknown as ToolDefinition,
];
}
/** Drives the write tool the way the harness would, around a real approval. */
const writingScript: TurnScript = async (tools, emit, signal) => {
const tool = tools.find((candidate) => candidate.name === 'pig_log_activity');
assert.ok(tool, 'the write tool should have been handed over');
emit(toolStart('call_1', 'pig_log_activity', { subject: 'Capacity review' }));
// The signal is forwarded because the harness forwards it: it is the only
// thing that tells a tool parked on an approval that the turn has been
// abandoned underneath it.
const result = await tool.execute('call_1', {}, signal, undefined, undefined as never);
emit(
toolEnd('call_1', 'pig_log_activity', {
content: result.content.filter(
(part): part is { type: 'text'; text: string } => part.type === 'text',
),
details: result.details,
}),
);
emit(textDelta('Logged.'));
emit(turnEnd(200, 20));
};
/**
* Reads the stream up to the approval card, then hands back a reader for the
* rest — because the decision has to be posted while the turn is still open,
* which is the entire point of the rendezvous.
*/
async function readUntilApproval(
response: Response,
): Promise<{ frames: PiggyChatEvent[]; rest: () => Promise<PiggyChatEvent[]> }> {
const body = response.body;
assert.ok(body, 'the turn should have streamed a body');
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const drain = (chunk: Uint8Array | undefined, into: PiggyChatEvent[]): void => {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) if (line) into.push(JSON.parse(line) as PiggyChatEvent);
};
const frames: PiggyChatEvent[] = [];
while (!frames.some((frame) => frame.type === 'approval_required')) {
const { done, value } = await reader.read();
if (done) break;
drain(value, frames);
}
const rest = async (): Promise<PiggyChatEvent[]> => {
const tail: PiggyChatEvent[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
drain(value, tail);
}
return tail;
};
return { frames, rest };
}
test('a proposed write waits for the user, then applies once and only once', async (t) => {
const runs: RecordedRun[] = [];
const applied: string[] = [];
const base = await startForTest(t, runs, {
createWriteTools: proposingWriteTools(applied),
createSession: fakeSessions(writingScript, spy()),
});
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ mode: 'confirm', message: 'Log a call on Northwind.' }),
});
const { frames, rest } = await readUntilApproval(response);
const asked = frames.find((frame) => frame.type === 'approval_required');
assert.ok(asked && asked.type === 'approval_required');
assert.equal(asked.change.summary, 'Log a call on Northwind Robotics');
assert.ok(asked.change.id.length > 0);
// The turn is still open, and nothing has been written yet.
assert.equal(applied.length, 0);
const decision = await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({ conversationId: 'conv-1', changeId: asked.change.id, decision: 'apply' }),
});
assert.equal(decision.status, 202);
const tail = await rest();
assert.deepEqual(tail[0], {
type: 'approval_resolved',
changeId: asked.change.id,
decision: 'apply',
ok: true,
});
// The tool result carries the truth, so the model cannot claim a save it did
// not make.
const result = tail.find((frame) => frame.type === 'tool_result');
assert.deepEqual(result?.type === 'tool_result' ? result.result : null, {
tool: 'pig_log_activity',
kind: 'activity',
status: 'applied',
});
assert.equal(applied.length, 1);
// A replayed decision must not apply the change a second time.
const replay = await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({ conversationId: 'conv-1', changeId: asked.change.id, decision: 'apply' }),
});
assert.equal(replay.status, 404);
assert.equal(applied.length, 1);
assert.equal(runs[0]?.closed?.status, 'succeeded');
// One model call, and one that waited five minutes' worth of human time
// without that counting against anything: the ceilings measure model work.
assert.deepEqual(runs[0]?.closed?.result, {
toolCalls: 1,
approvalsRequested: 1,
approvalsApplied: 1,
modelCalls: 1,
inference: { attempts: 1 },
});
});
test("a decision on another conversation's id settles nothing", async (t) => {
const runs: RecordedRun[] = [];
const applied: string[] = [];
const base = await startForTest(t, runs, {
approvalTimeoutMs: 200,
createWriteTools: proposingWriteTools(applied),
createSession: fakeSessions(writingScript, spy()),
});
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ mode: 'confirm' }),
});
const { frames, rest } = await readUntilApproval(response);
const asked = frames.find((frame) => frame.type === 'approval_required');
assert.ok(asked && asked.type === 'approval_required');
const wrong = await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({
conversationId: 'conv-someone-else',
changeId: asked.change.id,
decision: 'apply',
}),
});
assert.equal(wrong.status, 404);
await rest();
assert.equal(applied.length, 0);
});
test('a declined write is reported to the model as declined', async (t) => {
const runs: RecordedRun[] = [];
const applied: string[] = [];
const base = await startForTest(t, runs, {
createWriteTools: proposingWriteTools(applied),
createSession: fakeSessions(writingScript, spy()),
});
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ mode: 'confirm' }),
});
const { frames, rest } = await readUntilApproval(response);
const asked = frames.find((frame) => frame.type === 'approval_required');
assert.ok(asked && asked.type === 'approval_required');
await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({ conversationId: 'conv-1', changeId: asked.change.id, decision: 'reject' }),
});
const tail = await rest();
assert.equal(applied.length, 0);
const settled = tail.find((frame) => frame.type === 'approval_resolved');
assert.equal(settled?.type === 'approval_resolved' ? settled.ok : null, true);
const result = tail.find((frame) => frame.type === 'tool_result');
assert.deepEqual(result?.type === 'tool_result' ? result.result : null, {
tool: 'pig_log_activity',
kind: 'activity',
status: 'declined',
});
assert.deepEqual(runs[0]?.closed?.result, {
toolCalls: 1,
approvalsRequested: 1,
approvalsApplied: 0,
modelCalls: 1,
inference: { attempts: 1 },
});
});
test('an unanswered approval times out as a rejection rather than holding the turn open', async (t) => {
const runs: RecordedRun[] = [];
const applied: string[] = [];
const base = await startForTest(t, runs, {
approvalTimeoutMs: 60,
createWriteTools: proposingWriteTools(applied),
createSession: fakeSessions(writingScript, spy()),
});
const frames = parseFrames(
await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ mode: 'auto' }),
}).then((response) => response.text()),
);
const settled = frames.find((frame) => frame.type === 'approval_resolved');
assert.ok(settled && settled.type === 'approval_resolved');
assert.equal(settled.decision, 'reject');
assert.equal(settled.ok, false);
assert.match(String(settled.error), /within five minutes/);
// The turn finished rather than hanging on a reader who never answered, which
// is what was holding the inference connection open.
assert.equal(frames.at(-1)?.type, 'done');
assert.equal(applied.length, 0);
});
/**
* A write tool that behaves the way the real ones do when a turn is abandoned.
*
* `awaitDecision` in `write-tools.ts` races the pending decision against the
* abort signal the harness passes to `execute`, precisely so a reader who closes
* the tab does not leave a tool call — and the billed inference connection
* behind it — parked for ever. This double mirrors that, and records which
* decision it actually observed.
*/
function abandonableWriteTools(
seen: PiggyApprovalDecision[],
): (deps: PigWriteToolDeps) => ToolDefinition[] {
return ({ propose }) => [
{
name: 'pig_log_activity',
async execute(_id: string, _params: unknown, signal?: AbortSignal) {
const decision = await Promise.race<PiggyApprovalDecision>([
propose({
tool: 'pig_log_activity',
kind: 'activity',
summary: 'Log a call on Northwind Robotics',
fields: [{ label: 'Subject', value: 'Capacity review' }],
}),
new Promise<PiggyApprovalDecision>((resolve) => {
signal?.addEventListener('abort', () => resolve('reject'), { once: true });
}),
]);
seen.push(decision);
return {
content: [{ type: 'text', text: `The change was ${decision}ed.` }],
details: { tool: 'pig_log_activity', kind: 'activity', status: 'declined' },
};
},
} as unknown as ToolDefinition,
];
}
test('an abandoned turn rejects the approval it left open', async (t) => {
const runs: RecordedRun[] = [];
const seen: PiggyApprovalDecision[] = [];
const base = await startForTest(t, runs, {
// Long enough that the deadline cannot be what settles this: the abandoned
// turn has to do it, or the assertion below is measuring the timeout.
approvalTimeoutMs: 60_000,
createWriteTools: abandonableWriteTools(seen),
createSession: fakeSessions(writingScript, spy()),
});
const abort = new AbortController();
const response = await fetch(`${base}/internal/chat`, {
method: 'POST',
headers: authorised,
body: chatBody({ mode: 'confirm' }),
signal: abort.signal,
});
const { frames } = await readUntilApproval(response);
const asked = frames.find((frame) => frame.type === 'approval_required');
assert.ok(asked && asked.type === 'approval_required');
// The user closes the tab with the card still on screen.
abort.abort();
await waitFor(() => runs[0]?.closed !== undefined);
assert.equal(runs[0]?.closed?.status, 'aborted');
assert.deepEqual(seen, ['reject'], 'the tool was told the change was not approved');
// And the card is gone from the registry rather than sitting there waiting
// out its five minutes: an answer arriving now settles nothing.
const late = await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({ conversationId: 'conv-1', changeId: asked.change.id, decision: 'apply' }),
});
assert.equal(late.status, 404);
assert.deepEqual(seen, ['reject'], 'a late approval cannot revive an abandoned change');
});
test('a decision for an unknown change settles nothing, and says so', async (t) => {
const runs: RecordedRun[] = [];
const base = await startForTest(t, runs, { createSession: fakeSessions(async () => {}, spy()) });
const unknown = await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({
conversationId: 'conv-1',
changeId: '30000000-0000-4000-8000-000000000009',
decision: 'apply',
}),
});
assert.equal(unknown.status, 404);
const malformed = await fetch(`${base}/internal/approve`, {
method: 'POST',
headers: authorised,
body: JSON.stringify({ conversationId: 'conv-1', changeId: 'nope', decision: 'apply' }),
});
assert.equal(malformed.status, 400);
assert.deepEqual(await malformed.json(), { error: 'Invalid Piggy approval decision.' });
});
async function waitFor(condition: () => boolean): Promise<void> {
for (let attempt = 0; attempt < 100; attempt += 1) {
if (condition()) return;
await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.fail('the run was never closed');
}