Download src/gateway/desktop/computer-process.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/gateway/desktop/computer-process.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/gateway/desktop/computer-process.test.ts
-
curl -L -o computer-process.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/gateway/desktop/computer-process.test.ts
10.4 kB
| import path from "node:path"; | |
| import { afterEach, describe, expect, it, vi } from "vitest"; | |
| import type { ComputerUseCapabilityDescriptor } from "../../plugins/computer-use-contract.js"; | |
| import type { | |
| ManagedRun, | |
| ProcessSupervisor, | |
| RunExit, | |
| SpawnInput, | |
| } from "../../process/supervisor/types.js"; | |
| import { createDeferredCore } from "../../shared/deferred.js"; | |
| import { startComputerHostProcess } from "./computer-process.js"; | |
| import { ComputerHostFinalizationError, type ComputerHostInput } from "./computer-protocol.js"; | |
| const computerUse: ComputerUseCapabilityDescriptor = { | |
| contractVersion: 2, | |
| provider: { id: "fixture", label: "Fixture", generation: "native-generation" }, | |
| actions: ["screenshot", "left_click"], | |
| targets: ["screen"], | |
| deliveryModes: ["foreground"], | |
| observations: ["image"], | |
| features: { recording: false, agentCursor: false, multiDisplay: false }, | |
| }; | |
| const cleanups: Array<() => Promise<void>> = []; | |
| afterEach(async () => { | |
| for (const cleanup of cleanups.splice(0)) { | |
| await cleanup(); | |
| } | |
| }); | |
| function createChildFixture(options: { startupError?: string; finalizationError?: RegExp } = {}) { | |
| const exited = createDeferredCore<RunExit>(); | |
| const extinction = createDeferredCore(); | |
| const cleanupStarted = createDeferredCore(); | |
| const stopReceived = createDeferredCore(); | |
| const invocationReceived = createDeferredCore<Extract<ComputerHostInput, { type: "invoke" }>>(); | |
| const messages: ComputerHostInput[] = []; | |
| const inputs: SpawnInput[] = []; | |
| let input: SpawnInput | undefined; | |
| let resultSettled = false; | |
| const exit = (exitCode = 0) => { | |
| resultSettled = true; | |
| exited.resolve({ | |
| reason: "exit", | |
| exitCode, | |
| exitSignal: null, | |
| durationMs: 1, | |
| stdout: "", | |
| stderr: "", | |
| timedOut: false, | |
| noOutputTimedOut: false, | |
| }); | |
| }; | |
| const emit = (message: unknown) => input?.onStdout?.(`${JSON.stringify(message)}\n`); | |
| const run: ManagedRun = { | |
| runId: "synthetic-computer", | |
| startedAtMs: 0, | |
| activity: { | |
| get resultSettled() { | |
| return resultSettled; | |
| }, | |
| lastOutputAtMs: 0, | |
| }, | |
| wait: () => exited.promise, | |
| cancel: vi.fn(() => exit()), | |
| stdin: { | |
| write(data, callback) { | |
| const message = JSON.parse(data.toString()) as ComputerHostInput; | |
| messages.push(message); | |
| if (message.type === "start") { | |
| emit( | |
| options.startupError | |
| ? { type: "error", message: options.startupError } | |
| : { type: "ready", computerUse }, | |
| ); | |
| } | |
| if (message.type === "invoke") { | |
| invocationReceived.resolve(message); | |
| } | |
| if (message.type === "stop") { | |
| stopReceived.resolve(); | |
| } | |
| callback?.(); | |
| }, | |
| end() {}, | |
| }, | |
| }; | |
| const cleanupScope = vi.fn(async () => { | |
| cleanupStarted.resolve(); | |
| await extinction.promise; | |
| }); | |
| const supervisor: ProcessSupervisor = { | |
| acquireScopeCleanup: () => cleanupScope, | |
| spawn: async (params) => { | |
| params.assertCurrent?.(); | |
| input = params; | |
| inputs.push(params); | |
| return run; | |
| }, | |
| cancel: () => run.cancel(), | |
| cancelScope: () => run.cancel(), | |
| }; | |
| const env = { | |
| PATH: path.dirname(process.execPath), | |
| DISPLAY: ":99", | |
| DBUS_SESSION_BUS_ADDRESS: "unix:path=/fixture/bus", | |
| }; | |
| const child = startComputerHostProcess({ | |
| env, | |
| pluginIds: ["fixture"], | |
| assertCurrent: () => {}, | |
| supervisor, | |
| }); | |
| cleanups.push(async () => { | |
| exit(); | |
| extinction.resolve(); | |
| const closing = child.close(); | |
| if (options.finalizationError) { | |
| await expect(closing).rejects.toThrow(options.finalizationError); | |
| } else { | |
| await closing; | |
| } | |
| }); | |
| return { | |
| child, | |
| env, | |
| inputs, | |
| messages, | |
| emit, | |
| exit, | |
| extinction, | |
| cleanupStarted, | |
| cleanupScope, | |
| stopReceived, | |
| invocationReceived, | |
| run, | |
| }; | |
| } | |
| describe("Gateway computer process", () => { | |
| it("carries the acquired desktop environment and selected provider into the child, then finalizes through its scope owner", async () => { | |
| const originalDisplay = process.env.DISPLAY; | |
| const originalBus = process.env.DBUS_SESSION_BUS_ADDRESS; | |
| const f = createChildFixture(); | |
| const { child } = f; | |
| expect(f.inputs).toHaveLength(0); | |
| expect(await child.ready).toEqual(computerUse); | |
| expect(f.inputs[0]).toMatchObject({ env: f.env, exactEnv: true, stdinMode: "pipe-open" }); | |
| expect(f.messages[0]).toEqual({ type: "start", pluginIds: ["fixture"] }); | |
| expect(process.env.DISPLAY).toBe(originalDisplay); | |
| expect(process.env.DBUS_SESSION_BUS_ADDRESS).toBe(originalBus); | |
| const result = child.invoke({ | |
| command: "screen.snapshot", | |
| params: { executionId: "fixture" }, | |
| assertCurrent: () => {}, | |
| }); | |
| const invocation = await f.invocationReceived.promise; | |
| f.emit({ | |
| type: "result", | |
| id: invocation.id, | |
| payload: JSON.stringify({ format: "png", base64: "cGl4ZWxz" }), | |
| }); | |
| await expect(result).resolves.toEqual({ format: "png", base64: "cGl4ZWxz" }); | |
| const settled = vi.fn(); | |
| const closing = child.close().then(settled); | |
| await f.stopReceived.promise; | |
| expect(child.isCurrent()).toBe(false); | |
| expect(settled).not.toHaveBeenCalled(); | |
| f.exit(); | |
| await f.cleanupStarted.promise; | |
| expect(settled).not.toHaveBeenCalled(); | |
| f.extinction.resolve(); | |
| await closing; | |
| expect(f.run.cancel).not.toHaveBeenCalled(); | |
| }); | |
| it.each(["malformed-envelope", "malformed-result-payload"])( | |
| "settles pending callers and joins cleanup after a %s", | |
| async (kind) => { | |
| const f = createChildFixture({ finalizationError: /Invalid Gateway computer response/ }); | |
| const { child } = f; | |
| await child.ready; | |
| const result = child.invoke({ | |
| command: "computer.act", | |
| params: { action: "left_click" }, | |
| assertCurrent: () => {}, | |
| }); | |
| const rejected = expect(result).rejects.toThrow("Invalid Gateway computer response"); | |
| const invocation = await f.invocationReceived.promise; | |
| f.emit( | |
| kind === "malformed-envelope" | |
| ? { type: "not-a-message" } | |
| : { type: "result", id: invocation.id, payload: "{" }, | |
| ); | |
| await f.stopReceived.promise; | |
| expect(child.isCurrent()).toBe(false); | |
| f.exit(); | |
| await f.cleanupStarted.promise; | |
| f.extinction.resolve(); | |
| await rejected; | |
| }, | |
| ); | |
| it("reports a failed graceful exit only after joining descendant cleanup", async () => { | |
| const f = createChildFixture({ finalizationError: /shutdown failed/ }); | |
| const { child } = f; | |
| await child.ready; | |
| const settled = vi.fn(); | |
| const closing = child.close(); | |
| const observed = closing.then(settled, settled); | |
| const rejected = expect(closing).rejects.toBeInstanceOf(ComputerHostFinalizationError); | |
| await f.stopReceived.promise; | |
| f.exit(1); | |
| await f.cleanupStarted.promise; | |
| expect(settled).not.toHaveBeenCalled(); | |
| f.extinction.resolve(); | |
| await rejected; | |
| await observed; | |
| expect(child.isCurrent()).toBe(false); | |
| }); | |
| it("retains the eager cleanup handle when startup and the first physical cleanup attempt fail", async () => { | |
| const f = createChildFixture({ | |
| startupError: "synthetic provider startup failed", | |
| finalizationError: /synthetic provider startup failed/, | |
| }); | |
| const joinFailure = new Error("descendant cleanup could not join"); | |
| f.cleanupScope.mockRejectedValueOnce(joinFailure); | |
| expect(f.inputs).toHaveLength(0); | |
| await expect(f.child.ready).rejects.toThrow("synthetic provider startup failed"); | |
| const firstClose = f.child.close(); | |
| f.exit(1); | |
| await expect(firstClose).rejects.toBe(joinFailure); | |
| const settled = vi.fn(); | |
| const retry = f.child.close(); | |
| const observed = retry.then(settled, settled); | |
| const rejected = expect(retry).rejects.toBeInstanceOf(ComputerHostFinalizationError); | |
| await f.cleanupStarted.promise; | |
| try { | |
| expect(settled).not.toHaveBeenCalled(); | |
| expect(f.cleanupScope).toHaveBeenCalledTimes(2); | |
| } finally { | |
| f.extinction.resolve(); | |
| } | |
| await rejected; | |
| await observed; | |
| }); | |
| it.each(["exit", "fatal transport error"])( | |
| "preserves an unexpected idle %s through joined finalization", | |
| async (failure) => { | |
| const f = createChildFixture({ finalizationError: /process exited|fatal transport error/ }); | |
| await f.child.ready; | |
| if (failure === "exit") { | |
| f.exit(1); | |
| } else { | |
| f.emit({ type: "error", message: "fatal transport error" }); | |
| f.exit(); | |
| } | |
| await f.cleanupStarted.promise; | |
| const settled = vi.fn(); | |
| const closing = f.child.close(); | |
| const observed = closing.then(settled, settled); | |
| const rejected = expect(closing).rejects.toBeInstanceOf(ComputerHostFinalizationError); | |
| try { | |
| expect(f.child.isCurrent()).toBe(false); | |
| expect(settled).not.toHaveBeenCalled(); | |
| } finally { | |
| f.extinction.resolve(); | |
| } | |
| await rejected; | |
| await observed; | |
| }, | |
| ); | |
| it("cancels an aborted invocation and joins native exit and descendant cleanup before rejecting", async () => { | |
| const f = createChildFixture(); | |
| const { child } = f; | |
| await child.ready; | |
| const controller = new AbortController(); | |
| const settled = vi.fn(); | |
| const reason = new Error("caller cancelled"); | |
| const result = child.invoke({ | |
| command: "computer.act", | |
| params: { action: "left_click" }, | |
| signal: controller.signal, | |
| assertCurrent: () => {}, | |
| }); | |
| const rejected = expect(result).rejects.toBe(reason); | |
| const observed = result.then(settled, settled); | |
| const invocation = await f.invocationReceived.promise; | |
| controller.abort(reason); | |
| await f.stopReceived.promise; | |
| expect(f.messages).toContainEqual({ type: "cancel", id: invocation.id }); | |
| expect(settled).not.toHaveBeenCalled(); | |
| f.exit(); | |
| await f.cleanupStarted.promise; | |
| expect(settled).not.toHaveBeenCalled(); | |
| f.extinction.resolve(); | |
| await rejected; | |
| await observed; | |
| await expect( | |
| child.invoke({ command: "screen.snapshot", params: {}, assertCurrent: () => {} }), | |
| ).rejects.toThrow(); | |
| expect(f.messages.filter((message) => message.type === "invoke")).toHaveLength(1); | |
| }); | |
| }); | |