import { afterEach, describe, expect, it, vi } from "vitest"; import * as harness from "../src/index.js"; type Resolution = { approvalId: string; status: string }; type Request = { approvalId: string; sessionId: string; runId: string; toolCallId: string; toolName: string; arguments: unknown; riskLevel: "low" | "medium" | "high"; }; type Broker = { request(input: Request, signal?: AbortSignal): Promise; approve(approvalId: string): Resolution; deny(approvalId: string): Resolution; cancelForSession(sessionId: string): number; cancelForRun(runId: string): number; }; function createBroker(timeoutMs = 1_000): { broker: Broker; store: { listAfter(sessionId: string): { events: Array<{ type: string; payload: unknown }> } }; } { const Store = Reflect.get(harness, "InMemoryEventStore") as new () => { listAfter(sessionId: string): { events: Array<{ type: string; payload: unknown }> }; }; const Broker = Reflect.get(harness, "InMemoryApprovalBroker") as unknown as | (new (options: Record) => Broker) | undefined; expect(Broker, "InMemoryApprovalBroker must be exported").toBeDefined(); const store = new Store(); return { broker: new Broker!({ eventStore: store, timeoutMs }), store }; } function request(overrides: Partial = {}): Request { return { approvalId: "ap_1", sessionId: "ses_1", runId: "run_1", toolCallId: "call_1", toolName: "save_report_draft", arguments: { body: "report", apiKey: "never-log-me" }, riskLevel: "medium", ...overrides, }; } afterEach(() => { vi.useRealTimers(); }); describe("InMemoryApprovalBroker", () => { it("resolves an approved request and treats the same repeated decision as idempotent", async () => { const { broker, store } = createBroker(); const waiting = broker.request(request()); const first = broker.approve("ap_1"); const second = broker.approve("ap_1"); await expect(waiting).resolves.toEqual({ approvalId: "ap_1", status: "approved" }); expect(second).toEqual(first); expect(store.listAfter("ses_1").events.map((event) => event.type)).toEqual([ "approval.required", "approval.resolved", ]); expect(JSON.stringify(store.listAfter("ses_1"))).not.toContain("never-log-me"); }); it("rejects a conflicting second decision", async () => { const { broker } = createBroker(); const waiting = broker.request(request()); broker.approve("ap_1"); await waiting; expect(() => broker.deny("ap_1")).toThrowError( expect.objectContaining({ code: "APPROVAL_ALREADY_RESOLVED" }), ); }); it("expires pending approval once when its deadline passes", async () => { vi.useFakeTimers(); const { broker, store } = createBroker(50); const waiting = broker.request(request()); await vi.advanceTimersByTimeAsync(50); await expect(waiting).resolves.toEqual({ approvalId: "ap_1", status: "expired" }); expect(store.listAfter("ses_1").events.map((event) => event.type)).toEqual([ "approval.required", "approval.resolved", ]); }); it("cancels on abort and removes the abort listener after resolution", async () => { const { broker, store } = createBroker(); const controller = new AbortController(); const waiting = broker.request(request(), controller.signal); controller.abort(); await expect(waiting).resolves.toEqual({ approvalId: "ap_1", status: "cancelled" }); expect(store.listAfter("ses_1").events).toHaveLength(2); controller.abort(); expect(store.listAfter("ses_1").events).toHaveLength(2); }); it("cancels all pending approvals for a session or run", async () => { const { broker } = createBroker(); const first = broker.request(request()); const second = broker.request(request({ approvalId: "ap_2", runId: "run_2", toolCallId: "call_2" })); const third = broker.request(request({ approvalId: "ap_3", sessionId: "ses_2", toolCallId: "call_3" })); expect(broker.cancelForRun("run_2")).toBe(1); expect(broker.cancelForSession("ses_1")).toBe(1); expect(broker.cancelForSession("ses_2")).toBe(1); await expect(Promise.all([first, second, third])).resolves.toEqual([ { approvalId: "ap_1", status: "cancelled" }, { approvalId: "ap_2", status: "cancelled" }, { approvalId: "ap_3", status: "cancelled" }, ]); }); });