diff --git a/src/gateway/node-registry.test.ts b/src/gateway/node-registry.test.ts index 5d5119889fe..c42e5d82de6 100644 --- a/src/gateway/node-registry.test.ts +++ b/src/gateway/node-registry.test.ts @@ -64,23 +64,72 @@ function makeClient( }; } +function makeConnectivitySocket(emitPong: boolean) { + const socket = new EventEmitter() as EventEmitter & { + readyState: number; + send: (frame: unknown) => void; + ping: (data?: Buffer, mask?: boolean, cb?: (err?: Error) => void) => void; + }; + socket.readyState = 1; + socket.send = () => {}; + socket.ping = (_dataValue, _mask, cb) => { + cb?.(); + if (emitPong) { + queueMicrotask(() => socket.emit("pong")); + } + }; + return socket as unknown as GatewayWsClient["socket"]; +} + +function registerNode(registry: NodeRegistry, opts: Parameters[3] = {}) { + const frames: string[] = []; + registry.register(makeClient("conn-1", "node-1", frames, opts), {}); + return frames; +} + +function registerLinuxNode(registry: NodeRegistry) { + return registerNode(registry, { + clientId: "openclaw-node-host", + platform: "linux", + }); +} + +function invokeSystemRun( + registry: NodeRegistry, + frames: string[], + params: Record, + timeoutMs = 1_000, +) { + const invoke = registry.invoke({ + nodeId: "node-1", + command: "system.run", + params, + timeoutMs, + }); + const request = JSON.parse(frames[0] ?? "{}") as { + payload?: { id?: string; paramsJSON?: string | null }; + }; + return { invoke, request }; +} + +type SystemRunEvent = Parameters[0]; + +function authorizeSystemRun(registry: NodeRegistry, overrides: Partial = {}) { + return registry.authorizeSystemRunEvent({ + nodeId: "node-1", + connId: "conn-1", + sessionKey: "agent:main:main", + terminal: true, + ...overrides, + }); +} + describe("gateway/node-registry", () => { it("checks node websocket connectivity with ping/pong", async () => { const registry = new NodeRegistry(); - const socket = new EventEmitter() as EventEmitter & { - readyState: number; - send: (frame: unknown) => void; - ping: (data?: Buffer, mask?: boolean, cb?: (err?: Error) => void) => void; - }; - socket.readyState = 1; - socket.send = () => {}; - socket.ping = (dataValue, _mask, cb) => { - cb?.(); - queueMicrotask(() => socket.emit("pong")); - }; registry.register( makeClient("conn-1", "node-1", [], { - socket: socket as unknown as GatewayWsClient["socket"], + socket: makeConnectivitySocket(true), }), {}, ); @@ -90,19 +139,9 @@ describe("gateway/node-registry", () => { it("reports stale node websocket connectivity before invoke timeout", async () => { const registry = new NodeRegistry(); - const socket = new EventEmitter() as EventEmitter & { - readyState: number; - send: (frame: unknown) => void; - ping: (data?: Buffer, mask?: boolean, cb?: (err?: Error) => void) => void; - }; - socket.readyState = 1; - socket.send = () => {}; - socket.ping = (dataValue, _mask, cb) => { - cb?.(); - }; registry.register( makeClient("conn-1", "node-1", [], { - socket: socket as unknown as GatewayWsClient["socket"], + socket: makeConnectivitySocket(false), }), {}, ); @@ -145,46 +184,28 @@ describe("gateway/node-registry", () => { it("matches pending system.run events to the issuing connection", async () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register( - makeClient("conn-1", "node-1", frames, { - clientId: "openclaw-node-host", - platform: "linux", - }), - {}, - ); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-1", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerLinuxNode(registry); + const { invoke, request } = invokeSystemRun(registry, frames, { + runId: "run-1", + sessionKey: "agent:main:main", }); - const request = JSON.parse(frames[0] ?? "{}") as { payload?: { id?: string } }; expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-1", - sessionKey: "agent:main:main", terminal: false, }), ).toBe(true); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", + authorizeSystemRun(registry, { connId: "conn-other", runId: "run-1", - sessionKey: "agent:main:main", terminal: false, }), ).toBe(false); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-other", - sessionKey: "agent:main:main", terminal: false, }), ).toBe(false); @@ -202,20 +223,14 @@ describe("gateway/node-registry", () => { error: null, }); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-1", - sessionKey: "agent:main:main", terminal: true, }), ).toBe(true); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-1", - sessionKey: "agent:main:main", terminal: false, }), ).toBe(false); @@ -224,15 +239,14 @@ describe("gateway/node-registry", () => { it("keeps no-timeout system.run event authorization after invoke timeout", async () => { vi.useFakeTimers(); const registry = new NodeRegistry(); - const frames: string[] = []; try { - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-timeout", sessionKey: "agent:main:main", timeoutMs: 0 }, - timeoutMs: 1, - }); + const frames = registerNode(registry); + const { invoke } = invokeSystemRun( + registry, + frames, + { runId: "run-timeout", sessionKey: "agent:main:main", timeoutMs: 0 }, + 1, + ); await vi.advanceTimersByTimeAsync(1); await expect(invoke).resolves.toEqual({ @@ -242,12 +256,8 @@ describe("gateway/node-registry", () => { await vi.advanceTimersByTimeAsync(2 * 60 * 60 * 1000); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-timeout", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(true); } finally { @@ -259,19 +269,18 @@ describe("gateway/node-registry", () => { vi.useFakeTimers(); vi.setSystemTime(0); const registry = new NodeRegistry(); - const frames: string[] = []; try { - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { + const frames = registerNode(registry); + const { invoke } = invokeSystemRun( + registry, + frames, + { runId: "run-oversized", sessionKey: "agent:main:main", timeoutMs: Number.MAX_SAFE_INTEGER, }, - timeoutMs: Number.MAX_SAFE_INTEGER, - }); + Number.MAX_SAFE_INTEGER, + ); await vi.advanceTimersByTimeAsync(MAX_TIMER_TIMEOUT_MS); await expect(invoke).resolves.toEqual({ @@ -279,12 +288,8 @@ describe("gateway/node-registry", () => { error: { code: "TIMEOUT", message: "node invoke timed out" }, }); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-oversized", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(false); } finally { @@ -295,24 +300,18 @@ describe("gateway/node-registry", () => { it("expires system.run authorization when the process clock is invalid", () => { const nowSpy = vi.spyOn(Date, "now").mockReturnValue(Number.NaN); const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-invalid-clock", sessionKey: "agent:main:main", timeoutMs: 1_000 }, + const frames = registerNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "run-invalid-clock", + sessionKey: "agent:main:main", timeoutMs: 1_000, }); void invoke.catch(() => {}); try { expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-invalid-clock", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(false); } finally { @@ -325,24 +324,18 @@ describe("gateway/node-registry", () => { vi.useFakeTimers(); vi.setSystemTime(MAX_DATE_TIMESTAMP_MS); const registry = new NodeRegistry(); - const frames: string[] = []; try { - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-overflow", sessionKey: "agent:main:main", timeoutMs: 1_000 }, + const frames = registerNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "run-overflow", + sessionKey: "agent:main:main", timeoutMs: 1_000, }); void invoke.catch(() => {}); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-overflow", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(false); registry.unregister("conn-1"); @@ -353,79 +346,43 @@ describe("gateway/node-registry", () => { it("matches a single system.run event when legacy payload omits runId", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-legacy", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "run-legacy", + sessionKey: "agent:main:main", }); - expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", - sessionKey: "agent:main:main", - terminal: true, - }), - ).toBe(true); + expect(authorizeSystemRun(registry)).toBe(true); registry.unregister("conn-1"); void invoke.catch(() => {}); }); it("rejects runId-less system.run events for non-legacy nodes", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register( - makeClient("conn-1", "node-1", frames, { - clientId: "openclaw-node-host", - platform: "linux", - }), - {}, - ); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-required", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerLinuxNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "run-required", + sessionKey: "agent:main:main", }); - expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", - sessionKey: "agent:main:main", - terminal: true, - }), - ).toBe(false); + expect(authorizeSystemRun(registry)).toBe(false); registry.unregister("conn-1"); void invoke.catch(() => {}); }); it("generates and forwards a runId when system.run params omit it", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { command: ["/bin/sh", "-lc", "printf ok"], sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerNode(registry); + const { invoke, request } = invokeSystemRun(registry, frames, { + command: ["/bin/sh", "-lc", "printf ok"], + sessionKey: "agent:main:main", }); - const request = JSON.parse(frames[0] ?? "{}") as { - payload?: { paramsJSON?: string | null }; - }; const forwarded = JSON.parse(request.payload?.paramsJSON ?? "{}") as { runId?: unknown }; expect(typeof forwarded.runId).toBe("string"); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: forwarded.runId as string, - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(true); registry.unregister("conn-1"); @@ -434,15 +391,12 @@ describe("gateway/node-registry", () => { it("clears system.run event authorization when invoke result fails", async () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-failed", sessionKey: "agent:main:main", timeoutMs: 0 }, - timeoutMs: 1_000, + const frames = registerNode(registry); + const { invoke, request } = invokeSystemRun(registry, frames, { + runId: "run-failed", + sessionKey: "agent:main:main", + timeoutMs: 0, }); - const request = JSON.parse(frames[0] ?? "{}") as { payload?: { id?: string } }; expect( registry.handleInvokeResult({ @@ -460,34 +414,23 @@ describe("gateway/node-registry", () => { error: { code: "INVALID_REQUEST", message: "invalid params" }, }); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-failed", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(false); }); it("matches legacy macOS exec events with runtime-generated runId when single pending run matches", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "gateway-run", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "gateway-run", + sessionKey: "agent:main:main", }); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "legacy-runtime-run", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(true); registry.unregister("conn-1"); @@ -496,28 +439,15 @@ describe("gateway/node-registry", () => { it("rejects mismatched runId fallback for non-macOS nodes", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register( - makeClient("conn-1", "node-1", frames, { - clientId: "openclaw-node-host", - platform: "linux", - }), - {}, - ); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "gateway-run", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerLinuxNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "gateway-run", + sessionKey: "agent:main:main", }); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "runtime-run", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(false); registry.unregister("conn-1"); @@ -526,22 +456,14 @@ describe("gateway/node-registry", () => { it("matches system.run events with emitted session key when invoke omitted sessionKey", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const invoke = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-without-session" }, - timeoutMs: 1_000, + const frames = registerNode(registry); + const { invoke } = invokeSystemRun(registry, frames, { + runId: "run-without-session", }); expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", + authorizeSystemRun(registry, { runId: "run-without-session", - sessionKey: "agent:main:main", - terminal: true, }), ).toBe(true); registry.unregister("conn-1"); @@ -550,29 +472,17 @@ describe("gateway/node-registry", () => { it("rejects runId-less system.run events when the connection has multiple matches", () => { const registry = new NodeRegistry(); - const frames: string[] = []; - registry.register(makeClient("conn-1", "node-1", frames), {}); - const first = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-a", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const frames = registerNode(registry); + const { invoke: first } = invokeSystemRun(registry, frames, { + runId: "run-a", + sessionKey: "agent:main:main", }); - const second = registry.invoke({ - nodeId: "node-1", - command: "system.run", - params: { runId: "run-b", sessionKey: "agent:main:main" }, - timeoutMs: 1_000, + const { invoke: second } = invokeSystemRun(registry, frames, { + runId: "run-b", + sessionKey: "agent:main:main", }); - expect( - registry.authorizeSystemRunEvent({ - nodeId: "node-1", - connId: "conn-1", - sessionKey: "agent:main:main", - terminal: true, - }), - ).toBe(false); + expect(authorizeSystemRun(registry)).toBe(false); registry.unregister("conn-1"); void first.catch(() => {}); void second.catch(() => {});