Files
ThothII/backend/test/session-bridge.test.ts
T
marcopanandClaude Fable 5 2410f01b34 fix(bridge): forward Pi agent_end so the spinner stops at workflow completion
The FE derived 'working' purely as activeSession && !pendingWidget, so the
final workflow turn — the only one that ends without a follow-up gate —
left the spinner on forever (observed live: 21592s after F8 approve).

- SessionBridge maps Pi's agent_end -> SSE system_event {event: agent_end}
- PiProcessManager notifies the client (info error + synthetic agent_end)
  when the child dies unexpectedly; expected teardowns stay silent
- sessionStore tracks agentActive (on: user entry/text_delta/ui_request,
  off: agent_end); AppShell working now requires it; resume sets it
  optimistically

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:14:43 +02:00

73 lines
3.3 KiB
TypeScript

import { test, expect, vi } from "vitest";
import { SessionBridge } from "../src/bridge/session-bridge.js";
function fakeRpc() {
const sent: any[] = [];
let evcb: any;
return {
rpc: {
send: (c: any) => sent.push(c),
on: (_: any, cb: any) => { evcb = cb; },
request: vi.fn(),
} as any,
sent,
fire: (m: any) => evcb(m),
};
}
test("real Pi message_update (assistantMessageEvent text_delta) becomes a text_delta to the FE", () => {
const { rpc, fire } = fakeRpc();
const b = new SessionBridge(rpc);
const seen: any[] = [];
b.onClientEvent((e) => seen.push(e));
// Shape emitted by real `pi --mode rpc`: text streams inside message_update.assistantMessageEvent.delta
fire({ type: "message_update", assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "Avvio" } });
expect(seen[0]).toEqual({ type: "text_delta", text: "Avvio" });
// Non-text message_update events (e.g. text_start) are ignored.
fire({ type: "message_update", assistantMessageEvent: { type: "text_start", contentIndex: 0 } });
expect(seen).toHaveLength(1);
});
test("extension_ui_request nativo (method:input, title=json) diventa ui_request col descriptor ed è il pendente", () => {
const { rpc, fire } = fakeRpc();
const b = new SessionBridge(rpc);
const seen: any[] = [];
b.onClientEvent((e) => seen.push(e));
const descriptor = { id: "u1", widget: "select" };
// Pi assegna a ctx.ui.input un proprio id RPC (crypto.randomUUID), distinto dall'id
// interno del descriptor che il gate mette nel `title`. Al frontend va il descriptor.
fire({ type: "extension_ui_request", id: "pi-req-1", method: "input", title: JSON.stringify(descriptor) });
expect(seen[0]).toEqual({ type: "ui_request", ui_request: descriptor });
expect(b.pendingWidget()).toEqual(descriptor);
});
test("respond correla sull'id RPC di Pi (non sull'id del descriptor) e azzera il pendente", () => {
const { rpc, sent, fire } = fakeRpc();
const b = new SessionBridge(rpc);
// Pi emette la richiesta con il SUO id RPC ("pi-req-1"); il descriptor nel title ha id "u1".
fire({ type: "extension_ui_request", id: "pi-req-1", method: "input", title: JSON.stringify({ id: "u1", widget: "select" }) });
// Il frontend rimanda l'id del descriptor ("u1").
b.respond({ id: "u1", choices: ["a"] });
// Pi correla la risposta sul SUO id ("pi-req-1") per risolvere ctx.ui.input; il value
// continua a portare l'id del descriptor, cosi' il check interno del gate regge.
expect(sent.at(-1)).toEqual({ type: "extension_ui_response", id: "pi-req-1", value: JSON.stringify({ id: "u1", choices: ["a"] }) });
expect(b.pendingWidget()).toBeNull();
});
test("agent_end di Pi diventa un system_event agent_end per il FE", () => {
const { rpc, fire } = fakeRpc();
const b = new SessionBridge(rpc);
const seen: any[] = [];
b.onClientEvent((e) => seen.push(e));
// Pi emette agent_end alla fine di ogni prompt (pi-agent-core agent-loop); e' il solo
// segnale di fine turno: senza mapparlo il FE non puo' mai uscire dallo stato "working".
fire({ type: "agent_end", messages: [] });
expect(seen).toEqual([{ type: "system_event", event: "agent_end" }]);
});
test("steer invia un comando steer", () => {
const { rpc, sent } = fakeRpc();
new SessionBridge(rpc).steer("considera solo il 2024");
expect(sent.at(-1)).toEqual({ type: "steer", message: "considera solo il 2024" });
});