/** * End-to-end tests for the unified CU proxy flow. * * Tests the surfaceProxyResolver's CU tool routing — the integration * point between the agent loop and the HostCuProxy. */ import { afterEach, describe, expect, mock, test } from "bun:test"; import { asConversation } from "./helpers/mock-conversation.js"; const sentMessages: unknown[] = []; let mockHasClient = true; // Default to true for CU unified flow tests // Default principal id used for both ctx.trustContext and clients in the // existing single-user tests. Tests that exercise cross-user behaviour // override this on individual clients and on the Conversation. const DEFAULT_PRINCIPAL = "user-1"; let mockCuClients: Array<{ clientId: string; capabilities: string[]; actorPrincipalId?: string; }> = [ { clientId: "mock-client-1", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; mock.module("../runtime/assistant-event-hub.js", () => ({ broadcastMessage: (msg: unknown) => { // `interaction_resolved` envelopes are emitted by the // pending-interactions tracker for every resolution. They are // orthogonal to the surface-proxy wire messages these tests assert // on, so swallow them here. if ((msg as { type?: string } | null)?.type === "interaction_resolved") { return; } sentMessages.push(msg); }, assistantEventHub: { getMostRecentClientByCapability: (cap: string) => cap === "host_cu" && mockHasClient ? { id: "mock-client" } : null, listClientsByCapability: (cap: string) => cap === "host_cu" ? mockCuClients : [], getClientById: (id: string) => mockCuClients.find((c) => c.clientId === id) ?? null, getActorPrincipalIdForClient: (id: string) => mockCuClients.find((c) => c.clientId === id)?.actorPrincipalId, }, })); const { createSurfaceMutex, surfaceProxyResolver } = await import("../daemon/conversation-surfaces.js"); const { HostCuProxy } = await import("../daemon/host-cu-proxy.js"); type Conversation = import("../daemon/conversation.js").Conversation; // --------------------------------------------------------------------------- // Test helpers // --------------------------------------------------------------------------- /** * Build a minimal Conversation with optional hostCuProxy. * Only the fields required by the CU routing path are populated. * * `trustContext` defaults to a guardian context owned by `DEFAULT_PRINCIPAL`. * Pass `null` to omit the field entirely (used to verify same-user * enforcement when the conversation has no source actor principal). */ function buildMockContext( hostCuProxy?: InstanceType, trustGuardianPrincipalId: string | null = DEFAULT_PRINCIPAL, actorPrincipalId: string | null = trustGuardianPrincipalId, // Mirrors `Conversation.ensureHostProxiesForTurn`: attaches a CU proxy when // the gate passes. Left undefined to model a conversation that cannot // attach one (the gate denies, or the context predates the hook). ensureHostProxiesForTurn?: () => void, ): Conversation { const ctx: Conversation = asConversation({ conversationId: "test-session", trustContext: trustGuardianPrincipalId != null ? { sourceChannel: "vellum", trustClass: "guardian", guardianPrincipalId: trustGuardianPrincipalId, } : undefined, authContext: actorPrincipalId != null ? ({ actorPrincipalId } as Conversation["authContext"]) : undefined, currentTurnAuthContext: actorPrincipalId != null ? ({ actorPrincipalId, } as Conversation["currentTurnAuthContext"]) : undefined, sendToClient: () => {}, emit: () => {}, pendingSurfaceActions: new Map(), lastSurfaceAction: new Map(), surfaceState: new Map(), surfaceUndoStacks: new Map(), accumulatedSurfaceState: new Map(), surfaceActionRequestIds: new Set(), currentTurnSurfaces: [], hostCuProxy, transportInterface: "web", ensureHostProxiesForTurn, isProcessing: () => false, enqueueMessage: () => ({ queued: false, requestId: "r1" }), getQueueDepth: () => 0, processMessage: async () => "", withSurface: createSurfaceMutex(), }); return ctx; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("surfaceProxyResolver — CU tool routing", () => { let proxy: InstanceType; function setupProxy(maxSteps?: number): Conversation { sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "mock-client-1", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; proxy = new HostCuProxy(maxSteps); return buildMockContext(proxy); } afterEach(() => { proxy?.dispose(); }); // ------------------------------------------------------------------------- // No desktop client connected // ------------------------------------------------------------------------- describe("attaching the proxy at tool-call time", () => { test("attaches a proxy when the conversation has none and dispatches", async () => { // The reported bug: `assistant clients list` showed the desktop // connected and usable, while every computer_use_* call answered "no // desktop client connected" — because attachment was decided at turn // start, before the desktop was reachable, and never revisited. sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "mock-client-1", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; let attachCalls = 0; const ctx: Conversation = buildMockContext( /* no proxy */ undefined, DEFAULT_PRINCIPAL, DEFAULT_PRINCIPAL, () => { attachCalls++; proxy = new HostCuProxy(); (ctx as { hostCuProxy?: unknown }).hostCuProxy = proxy; }, ); const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 42, reasoning: "click the button", }); expect(attachCalls).toBe(1); expect(ctx.hostCuProxy).toBeDefined(); // Dispatched for real: the request reached the wire, not an error result. expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as { type?: string; requestId?: string }; expect(sent.type).toBe("host_cu_request"); proxy.processObservation(sent.requestId!, { axTree: "window Safari" }); const result = await resultPromise; expect(result.isError).toBe(false); }); test("reports the interface, not a missing desktop, when the gate still denies", async () => { // The gate ran and refused (e.g. a chrome-extension source, or a // desktop signed in as someone else). Saying "no desktop client // connected" here contradicts a listing that plainly shows one. mockCuClients = [ { clientId: "mock-client-other", capabilities: ["host_cu"], actorPrincipalId: "someone-else", }, ]; const ctx = buildMockContext(/* no proxy, no attach hook */); const result = await surfaceProxyResolver( ctx, "computer_use_screenshot", {}, ); expect(result.isError).toBe(true); expect(result.content).toContain("not available for this conversation"); expect(result.content).not.toContain("no desktop client connected"); }); test("reports no connected client when the hub has none", async () => { mockCuClients = []; const ctx = buildMockContext(/* no proxy */); const result = await surfaceProxyResolver( ctx, "computer_use_screenshot", {}, ); expect(result.isError).toBe(true); expect(result.content).toContain("no desktop client connected"); }); test("returns error when proxy exists but client not connected", async () => { mockHasClient = false; mockCuClients = []; const proxyObj = new HostCuProxy(); const ctx = buildMockContext(proxyObj); const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, }); expect(result.isError).toBe(true); expect(result.content).toContain("no desktop client connected"); proxyObj.dispose(); }); test("returns error for terminal tools when no proxy", async () => { const ctx = buildMockContext(); const doneResult = await surfaceProxyResolver(ctx, "computer_use_done", { summary: "finished", }); expect(doneResult.isError).toBe(true); const respondResult = await surfaceProxyResolver( ctx, "computer_use_respond", { answer: "42" }, ); expect(respondResult.isError).toBe(true); }); }); // ------------------------------------------------------------------------- // Terminal tools (computer_use_done, computer_use_respond) // ------------------------------------------------------------------------- describe("terminal tools resolve immediately", () => { test("computer_use_done resets proxy and returns summary", async () => { const ctx = setupProxy(); // Record some actions first to verify reset proxy.recordAction("computer_use_click", { element_id: 1 }); proxy.recordAction("computer_use_click", { element_id: 2 }); expect(proxy.stepCount).toBe(2); const result = await surfaceProxyResolver(ctx, "computer_use_done", { summary: "Completed the file upload", }); expect(result.isError).toBe(false); expect(result.content).toBe("Completed the file upload"); // No message sent to client for terminal tools expect(sentMessages).toHaveLength(0); // Proxy state should be reset expect(proxy.stepCount).toBe(0); expect(proxy.actionHistory).toHaveLength(0); }); test("computer_use_respond resets proxy and returns answer", async () => { const ctx = setupProxy(); proxy.recordAction("computer_use_click", { element_id: 1 }); const result = await surfaceProxyResolver(ctx, "computer_use_respond", { answer: "The price is $42", reasoning: "Found the price on the page", }); expect(result.isError).toBe(false); expect(result.content).toBe("The price is $42"); expect(sentMessages).toHaveLength(0); expect(proxy.stepCount).toBe(0); }); test("computer_use_done uses default when no summary provided", async () => { const ctx = setupProxy(); const result = await surfaceProxyResolver(ctx, "computer_use_done", {}); expect(result.isError).toBe(false); expect(result.content).toBe("Task complete"); }); test("computer_use_respond falls back to summary then default", async () => { const ctx = setupProxy(); // No answer but has summary — done tool uses summary const r1 = await surfaceProxyResolver(ctx, "computer_use_done", { summary: "All done", }); expect(r1.content).toBe("All done"); // respond with answer field const r2 = await surfaceProxyResolver(ctx, "computer_use_respond", { answer: "The answer is 7", }); expect(r2.content).toBe("The answer is 7"); }); }); // ------------------------------------------------------------------------- // Action tools (computer_use_click, screenshot, etc.) — proxy to client // ------------------------------------------------------------------------- describe("action tools proxy to client", () => { test("computer_use_click routes through proxy and returns observation", async () => { const ctx = setupProxy(); const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 42, reasoning: "Click the submit button", }); // Verify the proxy sent a request to the client expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as Record; expect(sent.type).toBe("host_cu_request"); expect(sent.toolName).toBe("computer_use_click"); expect(sent.input).toEqual({ element_id: 42, reasoning: "Click the submit button", }); expect(sent.conversationId).toBe("test-session"); // Action was recorded expect(proxy.stepCount).toBe(1); expect(proxy.actionHistory).toHaveLength(1); expect(proxy.actionHistory[0].toolName).toBe("computer_use_click"); // Simulate client resolving with observation const requestId = sent.requestId as string; proxy.processObservation(requestId, { axTree: "SubmitButton [1]\nTextField [2]", executionResult: "Clicked element 42", }); const result = await resultPromise; expect(result.isError).toBe(false); expect(result.content).toContain("Clicked element 42"); expect(result.content).toContain(""); expect(result.content).toContain("SubmitButton [1]"); }); test("computer_use_screenshot routes through proxy", async () => { const ctx = setupProxy(); const resultPromise = surfaceProxyResolver( ctx, "computer_use_screenshot", { reasoning: "Capture current state" }, ); expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as Record; expect(sent.type).toBe("host_cu_request"); expect(sent.toolName).toBe("computer_use_screenshot"); proxy.processObservation(sent.requestId as string, { axTree: "Window [1]", screenshot: "base64screenshot", screenshotWidthPx: 1920, screenshotHeightPx: 1080, }); const result = await resultPromise; expect(result.isError).toBe(false); expect(result.content).toContain("1920x1080 px"); expect(result.contentBlocks).toHaveLength(1); expect(result.contentBlocks![0]).toEqual({ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "base64screenshot", }, }); }); test("computer_use_type_text routes through proxy", async () => { const ctx = setupProxy(); const resultPromise = surfaceProxyResolver( ctx, "computer_use_type_text", { text: "Hello world", reasoning: "Type into search box" }, ); const sent = sentMessages[0] as Record; expect(sent.toolName).toBe("computer_use_type_text"); expect(sent.input).toEqual({ text: "Hello world", reasoning: "Type into search box", }); proxy.processObservation(sent.requestId as string, { axTree: "SearchBox [1] value='Hello world'", executionResult: "Typed text", }); const result = await resultPromise; expect(result.isError).toBe(false); expect(result.content).toContain("Typed text"); }); }); // ------------------------------------------------------------------------- // Full proxy lifecycle (observe → click → done) // ------------------------------------------------------------------------- describe("full proxy lifecycle", () => { test("observe → click → done sequence", async () => { const ctx = setupProxy(); // Step 1: observe (screenshot) const p1 = surfaceProxyResolver(ctx, "computer_use_screenshot", { reasoning: "Check what's on screen", }); const sent1 = sentMessages[0] as Record; proxy.processObservation(sent1.requestId as string, { axTree: "LoginButton [1]\nUsernameField [2]", }); const r1 = await p1; expect(r1.isError).toBe(false); expect(r1.content).toContain("LoginButton [1]"); expect(proxy.stepCount).toBe(1); // Step 2: click const p2 = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "Click login button", }); const sent2 = sentMessages[1] as Record; proxy.processObservation(sent2.requestId as string, { axTree: "PasswordField [1]\nSubmitButton [2]", axDiff: "+ PasswordField [1]\n+ SubmitButton [2]\n- LoginButton [1]", executionResult: "Clicked element 1", }); const r2 = await p2; expect(r2.isError).toBe(false); expect(r2.content).toContain("Clicked element 1"); expect(r2.content).toContain("PasswordField [1]"); expect(proxy.stepCount).toBe(2); // Step 3: done const r3 = await surfaceProxyResolver(ctx, "computer_use_done", { summary: "Logged in successfully", }); expect(r3.isError).toBe(false); expect(r3.content).toBe("Logged in successfully"); // Proxy state is clean after done expect(proxy.stepCount).toBe(0); expect(proxy.actionHistory).toHaveLength(0); // Only 2 messages sent to client (screenshot + click; done is terminal) expect(sentMessages).toHaveLength(2); }); }); // ------------------------------------------------------------------------- // Step limit enforced through resolver // ------------------------------------------------------------------------- // ------------------------------------------------------------------------- // Multi-client ambiguity guard // ------------------------------------------------------------------------- describe("multi-client ambiguity guard", () => { test("returns error when multiple same-user CU clients connected and no target_client_id given", async () => { const ctx = setupProxy(); mockCuClients = [ { clientId: "client-a", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, { clientId: "client-b", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", }); expect(result.isError).toBe(true); expect(result.content).toContain("Multiple host_cu clients"); expect(result.content).toContain("target_client_id"); // No message should have been dispatched expect(sentMessages).toHaveLength(0); }); test("proceeds when multiple clients connected and target_client_id is given", async () => { const ctx = setupProxy(); mockCuClients = [ { clientId: "client-a", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, { clientId: "client-b", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", target_client_id: "client-a", }); // Should have dispatched the request expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as Record; expect(sent.type).toBe("host_cu_request"); expect(sent.targetClientId).toBe("client-a"); proxy.processObservation(sent.requestId as string, { axTree: "ok" }); const result = await resultPromise; expect(result.isError).toBe(false); }); test("proceeds normally when exactly one CU client is connected", async () => { const ctx = setupProxy(); // mockCuClients already has 1 entry from setupProxy const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 2, reasoning: "safe click", }); expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as Record; expect(sent.type).toBe("host_cu_request"); proxy.processObservation(sent.requestId as string, { axTree: "ok" }); const result = await resultPromise; expect(result.isError).toBe(false); }); test("multi-client guard does not apply to terminal tools (computer_use_done)", async () => { const ctx = setupProxy(); mockCuClients = [ { clientId: "client-a", capabilities: ["host_cu"] }, { clientId: "client-b", capabilities: ["host_cu"] }, ]; // Terminal tools short-circuit before the ambiguity check const result = await surfaceProxyResolver(ctx, "computer_use_done", { summary: "all done", }); expect(result.isError).toBe(false); expect(result.content).toBe("all done"); expect(sentMessages).toHaveLength(0); }); }); // ------------------------------------------------------------------------- // targetClientId validation (lives at resolver layer so step count and // action history are not mutated when validation rejects the request). // ------------------------------------------------------------------------- describe("targetClientId validation", () => { test("returns fast error when targetClientId does not match any connected client", async () => { const ctx = setupProxy(); mockCuClients = [{ clientId: "real-client", capabilities: ["host_cu"] }]; const stepCountBefore = proxy.stepCount; const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", target_client_id: "nonexistent-client", }); expect(result.isError).toBe(true); expect(result.content).toContain("nonexistent-client"); expect(result.content).toContain("host_cu"); // Critical: validation must run BEFORE recordAction. stepCount and // actionHistory must be unchanged when rejection fires — otherwise // every invalid target_client_id burns a step and leaves a ghost // entry the LLM can reason about. expect(proxy.stepCount).toBe(stepCountBefore); expect(proxy.actionHistory).toHaveLength(0); expect(sentMessages).toHaveLength(0); }); test("returns fast error when targetClientId points to a client without host_cu capability", async () => { const ctx = setupProxy(); mockCuClients = [ { clientId: "no-cu-client", capabilities: ["host_bash"] }, // bash, not cu ]; const stepCountBefore = proxy.stepCount; const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", target_client_id: "no-cu-client", }); expect(result.isError).toBe(true); expect(result.content).toContain("no-cu-client"); expect(result.content).toContain("host_cu"); // No step burned, no ghost in history. expect(proxy.stepCount).toBe(stepCountBefore); expect(proxy.actionHistory).toHaveLength(0); expect(sentMessages).toHaveLength(0); }); test("dispatches and records action when targetClientId is valid", async () => { const ctx = setupProxy(); mockCuClients = [ { clientId: "cu-client", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, // Second client present to ensure target_client_id resolves // unambiguously and would otherwise trip the ambiguity guard. { clientId: "client-b", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 5, reasoning: "click", target_client_id: "cu-client", }); expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as Record; expect(sent.type).toBe("host_cu_request"); expect(sent.targetClientId).toBe("cu-client"); // recordAction did fire on the success path. expect(proxy.stepCount).toBe(1); expect(proxy.actionHistory).toHaveLength(1); proxy.processObservation(sent.requestId as string, { axTree: "ok" }); const result = await resultPromise; expect(result.isError).toBe(false); }); }); // ------------------------------------------------------------------------- // Same-user enforcement (dispatch layer) // // The proxy enforces this internally as well — these tests verify the // dispatch performs the same-user rejection before the proxy is invoked // (so no step is burned and no action history mutated), and uses the // canonical rejection message. // ------------------------------------------------------------------------- describe("same-user enforcement", () => { test("rejects targeted CU dispatch from a different actor principal", async () => { sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "cu-client", capabilities: ["host_cu"], actorPrincipalId: "user-other", }, ]; proxy = new HostCuProxy(); const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL); const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", target_client_id: "cu-client", }); expect(result.isError).toBe(true); expect(result.content).toContain("signed in as a different user"); // No state mutation, no dispatch. expect(proxy.stepCount).toBe(0); expect(proxy.actionHistory).toHaveLength(0); expect(sentMessages).toHaveLength(0); }); test("rejects when the conversation has no source actor principal", async () => { sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "cu-client", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, ]; proxy = new HostCuProxy(); const ctx = buildMockContext(proxy, null); const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", target_client_id: "cu-client", }); expect(result.isError).toBe(true); expect(result.content).toContain("no authenticated actor"); expect(sentMessages).toHaveLength(0); }); test("rejects untargeted CU dispatch to owner client when current requester is a trusted contact", async () => { sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "owner-cu-client", capabilities: ["host_cu"], actorPrincipalId: "owner-user", }, ]; proxy = new HostCuProxy(); const ctx = buildMockContext(proxy, "owner-user", "trusted-contact-user"); const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", }); expect(result.isError).toBe(true); expect(result.content).toContain("current actor"); expect(proxy.stepCount).toBe(0); expect(proxy.actionHistory).toHaveLength(0); expect(sentMessages).toHaveLength(0); }); test("auto-resolves to the unique same-user CU client when cross-user clients are present", async () => { // Regression: previously the dispatch counted only same-user clients // for the multi-client guard, so 1 same-user + 1 cross-user passed the // guard with no targetClientId — and the proxy then broadcast to ALL // host_cu subscribers, including the cross-user one. sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "cu-mine", capabilities: ["host_cu"], actorPrincipalId: DEFAULT_PRINCIPAL, }, { clientId: "cu-other", capabilities: ["host_cu"], actorPrincipalId: "user-other", }, ]; proxy = new HostCuProxy(); const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL); const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", // Intentionally no target_client_id — exercises auto-resolve. }); // Broadcast happens, but with the same-user clientId set so only // that client receives it. expect(sentMessages).toHaveLength(1); const sent = sentMessages[0] as Record; expect(sent.targetClientId).toBe("cu-mine"); // Manually resolve to clean up the pending promise. proxy.processObservation(sent.requestId as string, { executionResult: "ok", }); await resultPromise; }); test("rejects untargeted CU dispatch when only another actor has a capable client", async () => { sentMessages.length = 0; mockHasClient = true; mockCuClients = [ { clientId: "cu-other", capabilities: ["host_cu"], actorPrincipalId: "user-other", }, ]; proxy = new HostCuProxy(); const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL); const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "click", }); expect(result.isError).toBe(true); expect(result.content).toContain("current actor"); // No step burned, no dispatch. expect(proxy.stepCount).toBe(0); expect(sentMessages).toHaveLength(0); }); }); describe("step limit enforcement through resolver", () => { test("rejects action tools when step limit exceeded", async () => { const ctx = setupProxy(2); // maxSteps = 2 // Record enough actions to exceed the limit proxy.recordAction("computer_use_click", { element_id: 1 }); proxy.recordAction("computer_use_click", { element_id: 2 }); proxy.recordAction("computer_use_click", { element_id: 3 }); expect(proxy.stepCount).toBe(3); // The surfaceProxyResolver calls proxy.request, which checks step limit const result = await surfaceProxyResolver(ctx, "computer_use_click", { element_id: 4, reasoning: "click", }); expect(result.isError).toBe(true); expect(result.content).toContain("Step limit"); expect(result.content).toContain("computer_use_done"); }); test("terminal tools still work after step limit exceeded", async () => { const ctx = setupProxy(2); proxy.recordAction("computer_use_click", { element_id: 1 }); proxy.recordAction("computer_use_click", { element_id: 2 }); proxy.recordAction("computer_use_click", { element_id: 3 }); // computer_use_done should still work (terminal, resolves immediately) const result = await surfaceProxyResolver(ctx, "computer_use_done", { summary: "Stopped because step limit", }); expect(result.isError).toBe(false); expect(result.content).toBe("Stopped because step limit"); expect(proxy.stepCount).toBe(0); }); }); // ------------------------------------------------------------------------- // Error from client // ------------------------------------------------------------------------- describe("error from client observation", () => { test("returns error result when client reports execution error", async () => { const ctx = setupProxy(); const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 999, reasoning: "click missing element", }); const sent = sentMessages[0] as Record; proxy.processObservation(sent.requestId as string, { executionError: "Element 999 not found in AX tree", axTree: "Window [1]", }); const result = await resultPromise; expect(result.isError).toBe(true); expect(result.content).toContain("Action failed"); expect(result.content).toContain("Element 999 not found"); }); }); // ------------------------------------------------------------------------- // Reasoning propagation // ------------------------------------------------------------------------- describe("reasoning propagation", () => { test("reasoning from input is passed to proxy request", async () => { const ctx = setupProxy(); const resultPromise = surfaceProxyResolver(ctx, "computer_use_key", { key: "Enter", reasoning: "Submit the form", }); const sent = sentMessages[0] as Record; expect(sent.reasoning).toBe("Submit the form"); // Resolve to avoid unhandled rejection on dispose proxy.processObservation(sent.requestId as string, { axTree: "..." }); await resultPromise; }); test("reasoning is recorded in action history", async () => { const ctx = setupProxy(); surfaceProxyResolver(ctx, "computer_use_scroll", { direction: "down", amount: 3, reasoning: "Scroll to see more", }); expect(proxy.actionHistory[0].reasoning).toBe("Scroll to see more"); // Resolve to avoid hanging const sent = sentMessages[0] as Record; proxy.processObservation(sent.requestId as string, { axTree: "..." }); }); }); // ------------------------------------------------------------------------- // Non-CU tools are not handled by CU routing // ------------------------------------------------------------------------- describe("non-CU tools are not handled by CU routing", () => { test("ui_show is not affected by CU routing", async () => { const ctx = setupProxy(); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "confirmation", data: { message: "Are you sure?" }, }); // ui_show goes through its own path, not the CU path expect(result.content).not.toContain("not available"); expect(result.content).not.toContain("desktop client"); }); test("unknown tool returns error", async () => { const ctx = setupProxy(); const result = await surfaceProxyResolver(ctx, "not_a_real_tool", {}); expect(result.isError).toBe(true); expect(result.content).toContain("Unknown proxy tool"); }); }); // ------------------------------------------------------------------------- // Multiple sequential CU actions accumulate state // ------------------------------------------------------------------------- describe("state accumulation across actions", () => { test("step count increments across multiple actions", async () => { const ctx = setupProxy(); // Action 1 const p1 = surfaceProxyResolver(ctx, "computer_use_click", { element_id: 1, reasoning: "first", }); const s1 = sentMessages[0] as Record; proxy.processObservation(s1.requestId as string, { axTree: "A" }); await p1; expect(proxy.stepCount).toBe(1); // Action 2 const p2 = surfaceProxyResolver(ctx, "computer_use_type_text", { text: "hello", reasoning: "second", }); const s2 = sentMessages[1] as Record; proxy.processObservation(s2.requestId as string, { axTree: "B" }); await p2; expect(proxy.stepCount).toBe(2); // Action 3 const p3 = surfaceProxyResolver(ctx, "computer_use_scroll", { direction: "down", amount: 1, reasoning: "third", }); const s3 = sentMessages[2] as Record; proxy.processObservation(s3.requestId as string, { axTree: "C" }); await p3; expect(proxy.stepCount).toBe(3); // History has all 3 expect(proxy.actionHistory).toHaveLength(3); expect(proxy.actionHistory.map((a) => a.toolName)).toEqual([ "computer_use_click", "computer_use_type_text", "computer_use_scroll", ]); }); }); });