Download src/agents/tools/computer-tool.gateway.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 16.6 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/tools/computer-tool.gateway.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/tools/computer-tool.gateway.test.ts
-
curl -L -o computer-tool.gateway.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/tools/computer-tool.gateway.test.ts
16.6 kB
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import type { GatewayRequestContext } from "../../gateway/server-methods/types.js"; | |
| import { | |
| claimAgentRunDelegatedAuthority, | |
| releaseAgentRunDelegatedAuthority, | |
| validateAgentRunDelegatedAuthority, | |
| type AgentRunDelegatedAuthority, | |
| } from "../../infra/agent-run-registry.js"; | |
| import { createOperationalRunInstanceRef } from "../admitted-run-context.js"; | |
| import { | |
| callGatewayToolMock, | |
| createVisionComputerTool, | |
| gatewayComputerStatusMock, | |
| listNodesMock, | |
| loadPairedComputerUseAvailabilityForSurface, | |
| macComputerNode, | |
| readActionEnum, | |
| readFrameId, | |
| resetComputerToolMocks, | |
| screenshotPayload, | |
| v2Descriptor, | |
| } from "./computer-tool.test-helpers.js"; | |
| import { wrapToolWithGatewayCallerIdentity } from "./gateway-caller-context.js"; | |
| beforeEach(resetComputerToolMocks); | |
| const authorities: AgentRunDelegatedAuthority[] = []; | |
| afterEach(() => { | |
| for (const authority of authorities.splice(0)) { | |
| releaseAgentRunDelegatedAuthority(authority); | |
| } | |
| }); | |
| function createHostedComputerTool(options: Parameters<typeof createVisionComputerTool>[0] = {}) { | |
| const gatewayContext = {} as GatewayRequestContext; | |
| const operationalRunInstance = createOperationalRunInstanceRef("computer-test"); | |
| const authority = claimAgentRunDelegatedAuthority(operationalRunInstance); | |
| authorities.push(authority); | |
| return wrapToolWithGatewayCallerIdentity(createVisionComputerTool(options), { | |
| agentId: "main", | |
| sessionKey: "agent:main:computer-test", | |
| operationalRunInstance, | |
| approvalAuthority: authority, | |
| gatewayContextResolver: () => gatewayContext, | |
| receiptAuthority: () => validateAgentRunDelegatedAuthority(authority), | |
| }); | |
| } | |
| describe("computer Gateway and node targets", () => { | |
| it.each([ | |
| { gatewayUrl: "wss://released-gateway.example" }, | |
| { gatewayToken: "fixture-token" }, | |
| { gatewayUrl: "wss://released-gateway.example", gatewayToken: "fixture-token" }, | |
| ])("preserves v2026.9.4 implicit remote-node selection with %j", async (gatewayOptions) => { | |
| const [nodes, computer] = await Promise.all([ | |
| vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js"), | |
| vi.importActual<typeof import("./computer-tool-gateway.js")>("./computer-tool-gateway.js"), | |
| ]); | |
| listNodesMock.mockImplementation(nodes.listNodes); | |
| gatewayComputerStatusMock.mockImplementation(computer.loadGatewayComputerStatus); | |
| // v2026.9.4 exposes these paired-node methods, but no computer.status/invoke. | |
| callGatewayToolMock.mockImplementation(async (method, _options, request) => { | |
| if (method === "node.list") { | |
| return { nodes: [macComputerNode()] }; | |
| } | |
| if (method === "node.invoke") { | |
| return request.command === "screen.snapshot" | |
| ? screenshotPayload() | |
| : { payload: { ok: true } }; | |
| } | |
| throw new Error(`unknown method: ${method}`); | |
| }); | |
| const tool = createVisionComputerTool(); | |
| const screenshot = await tool.execute("observe", { action: "screenshot", ...gatewayOptions }); | |
| expect(screenshot.details).toMatchObject({ node: "mac-1" }); | |
| await tool.execute("input", { action: "type", text: "fixture" }); | |
| expect(callGatewayToolMock.mock.calls.map(([method]) => method)).toEqual([ | |
| "node.list", | |
| "node.invoke", | |
| "node.invoke", | |
| "node.invoke", | |
| ]); | |
| for (const [, options] of callGatewayToolMock.mock.calls) { | |
| expect(options).toMatchObject(gatewayOptions); | |
| } | |
| expect(gatewayComputerStatusMock).not.toHaveBeenCalled(); | |
| await expect( | |
| tool.execute("unsupported-host", { | |
| action: "screenshot", | |
| target: "gateway", | |
| ...gatewayOptions, | |
| }), | |
| ).rejects.toThrow("one persistent operator RPC connection"); | |
| expect(callGatewayToolMock).toHaveBeenCalledTimes(4); | |
| expect(gatewayComputerStatusMock).not.toHaveBeenCalled(); | |
| }); | |
| it.each([undefined, "gateway"] as const)( | |
| "refreshes failed prepared discovery for target %s", | |
| async (target) => { | |
| const unavailable = { | |
| configured: true as const, | |
| available: false as const, | |
| error: "Desktop temporarily unavailable", | |
| }; | |
| gatewayComputerStatusMock | |
| .mockResolvedValueOnce(unavailable) | |
| .mockResolvedValueOnce(unavailable) | |
| .mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: v2Descriptor(["screenshot"]), | |
| }); | |
| listNodesMock.mockResolvedValue([]); | |
| const availability = await loadPairedComputerUseAvailabilityForSurface({ | |
| computerAllowed: true, | |
| }); | |
| const tool = createHostedComputerTool({ pairedNodeComputerUse: availability?.prepared }); | |
| await expect(tool.execute("unavailable", { action: "screenshot", target })).rejects.toThrow( | |
| "Desktop temporarily unavailable", | |
| ); | |
| expect(callGatewayToolMock).not.toHaveBeenCalled(); | |
| const screenshot = await tool.execute("recovered", { action: "screenshot", target }); | |
| expect(screenshot.details).toMatchObject({ target: "gateway" }); | |
| expect(gatewayComputerStatusMock).toHaveBeenCalledTimes(3); | |
| expect(listNodesMock).toHaveBeenCalledOnce(); | |
| expect(callGatewayToolMock.mock.calls.every(([method]) => method === "computer.invoke")).toBe( | |
| true, | |
| ); | |
| }, | |
| ); | |
| it.each([false, true])( | |
| "rejects ephemeral Gateway connections (override=%s)", | |
| async (override) => { | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: v2Descriptor(["screenshot"]), | |
| }); | |
| const tool = override ? createHostedComputerTool() : createVisionComputerTool(); | |
| await expect( | |
| tool.execute("observe", { | |
| action: "screenshot", | |
| target: "gateway", | |
| ...(override ? { gatewayUrl: "ws://localhost:18789" } : {}), | |
| }), | |
| ).rejects.toThrow("one persistent operator RPC connection"); | |
| expect(callGatewayToolMock).not.toHaveBeenCalled(); | |
| expect(listNodesMock).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| it("publishes Gateway capabilities and controls its desktop without a paired node", async () => { | |
| const computerUse = v2Descriptor(["screenshot", "left_click", "list_windows"]); | |
| gatewayComputerStatusMock.mockResolvedValue({ configured: true, available: true, computerUse }); | |
| listNodesMock.mockResolvedValue([]); | |
| const availability = await loadPairedComputerUseAvailabilityForSurface({ | |
| computerAllowed: true, | |
| modelHasVision: true, | |
| }); | |
| let cleanup: ((reason: string) => Promise<void>) | undefined; | |
| const tool = createHostedComputerTool({ | |
| pairedNodeComputerUse: availability?.prepared, | |
| registerRunCleanup: (registered) => { | |
| cleanup = registered; | |
| }, | |
| }); | |
| expect(readActionEnum(tool)).toContain("list_windows"); | |
| expect(tool.description).toContain("Gateway desktop"); | |
| const screenshot = await tool.execute("observe", { action: "screenshot" }); | |
| expect(screenshot.details).toMatchObject({ target: "gateway" }); | |
| expect(screenshot.details).not.toHaveProperty("node"); | |
| await tool.execute("click", { | |
| action: "left_click", | |
| coordinate: [0, 0], | |
| frameId: readFrameId(screenshot), | |
| }); | |
| await cleanup?.("completed"); | |
| expect(gatewayComputerStatusMock).toHaveBeenCalledOnce(); | |
| expect(listNodesMock).toHaveBeenCalledOnce(); | |
| expect(callGatewayToolMock.mock.calls.map(([method]) => method)).toEqual([ | |
| "computer.invoke", | |
| "computer.invoke", | |
| "computer.invoke", | |
| "computer.invoke", | |
| ]); | |
| expect( | |
| callGatewayToolMock.mock.calls.every( | |
| (call) => call[2].generation === computerUse.provider.generation, | |
| ), | |
| ).toBe(true); | |
| expect(callGatewayToolMock.mock.calls.at(-1)?.[2]).toMatchObject({ | |
| command: "computer.act", | |
| params: { action: "__close_execution" }, | |
| }); | |
| }); | |
| it("unites both hosts' initial actions and keeps explicit node selection on the node", async () => { | |
| const gateway = v2Descriptor(["screenshot", "list_windows"]); | |
| const node = v2Descriptor(["screenshot", "get_cursor_position"]); | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: gateway, | |
| }); | |
| listNodesMock.mockResolvedValue([macComputerNode({ nodeId: "gateway", computerUse: node })]); | |
| const availability = await loadPairedComputerUseAvailabilityForSurface({ | |
| computerAllowed: true, | |
| }); | |
| const tool = createHostedComputerTool({ pairedNodeComputerUse: availability?.prepared }); | |
| expect(readActionEnum(tool)).toEqual( | |
| expect.arrayContaining(["list_windows", "get_cursor_position"]), | |
| ); | |
| const first = await tool.execute("node", { action: "screenshot", node: "gateway" }); | |
| expect(first.details).toMatchObject({ node: "gateway" }); | |
| const second = await tool.execute("sticky", { action: "screenshot" }); | |
| expect(second.details).toMatchObject({ node: "gateway" }); | |
| expect(callGatewayToolMock.mock.calls.every(([method]) => method === "node.invoke")).toBe(true); | |
| await expect( | |
| tool.execute("conflict", { action: "screenshot", target: "gateway", node: "gateway" }), | |
| ).rejects.toThrow("does not accept a node selector"); | |
| }); | |
| it("rejects a node frame on the Gateway even when the node is named gateway", async () => { | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: v2Descriptor(["screenshot", "left_click"]), | |
| }); | |
| listNodesMock.mockResolvedValue([macComputerNode({ nodeId: "gateway" })]); | |
| const tool = createHostedComputerTool(); | |
| const nodeScreenshot = await tool.execute("node", { action: "screenshot", node: "gateway" }); | |
| await expect( | |
| tool.execute("wrong-frame", { | |
| action: "left_click", | |
| target: "gateway", | |
| coordinate: [0, 0], | |
| frameId: readFrameId(nodeScreenshot), | |
| }), | |
| ).rejects.toThrow("no screenshot of this computer"); | |
| expect(callGatewayToolMock).toHaveBeenCalledOnce(); | |
| const gatewayScreenshot = await tool.execute("gateway", { | |
| action: "screenshot", | |
| target: "gateway", | |
| }); | |
| await expect( | |
| tool.execute("wrong-node-frame", { | |
| action: "left_click", | |
| node: "gateway", | |
| coordinate: [0, 0], | |
| frameId: readFrameId(gatewayScreenshot), | |
| }), | |
| ).rejects.toThrow("no screenshot of this computer"); | |
| expect(callGatewayToolMock).toHaveBeenCalledTimes(2); | |
| }); | |
| it.each([undefined, "gateway"] as const)( | |
| "reports configured Gateway failure for target %s without selecting a node", | |
| async (target) => { | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: false, | |
| error: "Desktop session stopped", | |
| }); | |
| const tool = createHostedComputerTool(); | |
| await expect(tool.execute("observe", { action: "screenshot", target })).rejects.toThrow( | |
| "Desktop session stopped", | |
| ); | |
| expect(listNodesMock).not.toHaveBeenCalled(); | |
| expect(callGatewayToolMock).not.toHaveBeenCalled(); | |
| }, | |
| ); | |
| it("keeps a failed selected Gateway on its bound route", async () => { | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: v2Descriptor(["screenshot", "type"]), | |
| }); | |
| const tool = createHostedComputerTool(); | |
| await tool.execute("observe", { action: "screenshot" }); | |
| gatewayComputerStatusMock.mockResolvedValue({ configured: false, available: false }); | |
| callGatewayToolMock.mockRejectedValue(new Error("Gateway computer disconnected")); | |
| await expect(tool.execute("input", { action: "type", text: "fixture" })).rejects.toThrow( | |
| "disconnected", | |
| ); | |
| expect(gatewayComputerStatusMock).toHaveBeenCalledOnce(); | |
| expect(listNodesMock).not.toHaveBeenCalled(); | |
| expect(callGatewayToolMock.mock.calls.map(([method]) => method)).toEqual([ | |
| "computer.invoke", | |
| "computer.invoke", | |
| ]); | |
| }); | |
| it("recovers a retired Gateway on a fresh screenshot without retrying its input", async () => { | |
| const first = v2Descriptor(["screenshot", "left_click", "type"]); | |
| const second = v2Descriptor(["screenshot", "left_click", "type"], { | |
| provider: { ...first.provider, generation: "generation-2" }, | |
| }); | |
| gatewayComputerStatusMock | |
| .mockResolvedValueOnce({ configured: true, available: true, computerUse: first }) | |
| .mockResolvedValue({ configured: true, available: true, computerUse: second }); | |
| const availability = await loadPairedComputerUseAvailabilityForSurface({ | |
| computerAllowed: true, | |
| }); | |
| let cleanup: ((reason: string) => Promise<void>) | undefined; | |
| const tool = createHostedComputerTool({ | |
| pairedNodeComputerUse: availability?.prepared, | |
| contextEpoch: { value: 0 }, | |
| registerRunCleanup: (registered) => { | |
| cleanup = registered; | |
| }, | |
| }); | |
| let retired = false; | |
| callGatewayToolMock.mockImplementation(async (_method, _options, request) => { | |
| if (request.params.action === "__close_execution") { | |
| return { payload: { ok: true } }; | |
| } | |
| if (request.generation === first.provider.generation && retired) { | |
| throw new Error("Error: COMPUTER_STALE_OBSERVATION: Gateway computer generation changed"); | |
| } | |
| return request.command === "screen.snapshot" | |
| ? screenshotPayload() | |
| : { payload: { ok: true } }; | |
| }); | |
| const before = await tool.execute("before", { action: "screenshot" }); | |
| retired = true; | |
| await expect(tool.execute("input", { action: "type", text: "fixture" })).rejects.toThrow( | |
| "COMPUTER_STALE_OBSERVATION", | |
| ); | |
| expect(gatewayComputerStatusMock).toHaveBeenCalledOnce(); | |
| expect( | |
| callGatewayToolMock.mock.calls.filter((call) => call[2].params.action === "type"), | |
| ).toHaveLength(1); | |
| const after = await tool.execute("recover", { action: "screenshot" }); | |
| expect(gatewayComputerStatusMock).toHaveBeenCalledTimes(2); | |
| expect(readFrameId(after)).not.toBe(readFrameId(before)); | |
| expect(after.content.some((part) => part.type === "image")).toBe(true); | |
| await expect( | |
| tool.execute("old-frame", { | |
| action: "left_click", | |
| coordinate: [0, 0], | |
| frameId: readFrameId(before), | |
| }), | |
| ).rejects.toThrow("frameId does not match"); | |
| await tool.execute("fresh-input", { | |
| action: "left_click", | |
| coordinate: [0, 0], | |
| frameId: readFrameId(after), | |
| }); | |
| await cleanup?.("completed"); | |
| expect( | |
| callGatewayToolMock.mock.calls.filter((call) => call[2].params.action === "left_click"), | |
| ).toHaveLength(1); | |
| expect( | |
| callGatewayToolMock.mock.calls | |
| .filter((call) => call[2].params.action === "__close_execution") | |
| .map((call) => call[2].generation) | |
| .toSorted((a, b) => a.localeCompare(b)), | |
| ).toEqual([first.provider.generation, second.provider.generation]); | |
| expect(listNodesMock).toHaveBeenCalledOnce(); | |
| }); | |
| it.each(["gateway", "node"] as const)( | |
| "bounds stale screenshot recovery on %s", | |
| async (target) => { | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: v2Descriptor(["screenshot"]), | |
| }); | |
| callGatewayToolMock.mockRejectedValue( | |
| new Error("COMPUTER_STALE_OBSERVATION: native capture retired"), | |
| ); | |
| const tool = createHostedComputerTool(); | |
| await expect(tool.execute("observe", { action: "screenshot", target })).rejects.toThrow( | |
| "COMPUTER_STALE_OBSERVATION", | |
| ); | |
| expect(callGatewayToolMock).toHaveBeenCalledTimes(target === "gateway" ? 2 : 1); | |
| expect(gatewayComputerStatusMock).toHaveBeenCalledTimes(target === "gateway" ? 2 : 0); | |
| expect(listNodesMock).toHaveBeenCalledTimes(target === "gateway" ? 0 : 1); | |
| }, | |
| ); | |
| it("surfaces Gateway cleanup failure without dropping execution cleanup", async () => { | |
| gatewayComputerStatusMock.mockResolvedValue({ | |
| configured: true, | |
| available: true, | |
| computerUse: v2Descriptor(["screenshot"]), | |
| }); | |
| let cleanup: ((reason: string) => Promise<void>) | undefined; | |
| const tool = createHostedComputerTool({ | |
| registerRunCleanup: (registered) => { | |
| cleanup = registered; | |
| }, | |
| }); | |
| callGatewayToolMock.mockResolvedValueOnce(screenshotPayload()); | |
| await tool.execute("observe", { action: "screenshot" }); | |
| callGatewayToolMock.mockRejectedValue(new Error("cleanup failed")); | |
| await expect(cleanup?.("completed")).rejects.toThrow("desktop cleanup failed"); | |
| }); | |
| }); | |