From a7e2dd0538f30799d3f05fcb25108e2b89d8b802 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sat, 9 May 2026 17:19:50 +0100 Subject: [PATCH] test: hoist gateway helper imports --- src/agents/tools/gateway.test.ts | 56 +++++++++++++++----------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/agents/tools/gateway.test.ts b/src/agents/tools/gateway.test.ts index bfbbf6902e6..c674e9bdf0c 100644 --- a/src/agents/tools/gateway.test.ts +++ b/src/agents/tools/gateway.test.ts @@ -1,32 +1,28 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { callGatewayTool, resolveGatewayOptions } from "./gateway.js"; -const callGatewayMock = vi.fn(); -const configState = vi.hoisted(() => ({ - value: {} as Record, +const mocks = vi.hoisted(() => ({ + callGateway: vi.fn(), + configState: { + value: {} as Record, + }, })); vi.mock("../../config/config.js", () => ({ - getRuntimeConfig: () => configState.value, + getRuntimeConfig: () => mocks.configState.value, resolveGatewayPort: () => 18789, })); vi.mock("../../gateway/call.js", () => ({ - callGateway: (...args: unknown[]) => callGatewayMock(...args), + callGateway: (...args: unknown[]) => mocks.callGateway(...args), })); -let callGatewayTool: typeof import("./gateway.js").callGatewayTool; -let resolveGatewayOptions: typeof import("./gateway.js").resolveGatewayOptions; - describe("gateway tool defaults", () => { const envSnapshot = { openclaw: process.env.OPENCLAW_GATEWAY_TOKEN, }; - beforeAll(async () => { - ({ callGatewayTool, resolveGatewayOptions } = await import("./gateway.js")); - }); - beforeEach(() => { - callGatewayMock.mockClear(); - configState.value = {}; + mocks.callGateway.mockClear(); + mocks.configState.value = {}; delete process.env.OPENCLAW_GATEWAY_TOKEN; }); @@ -44,13 +40,13 @@ describe("gateway tool defaults", () => { }); it("accepts allowlisted gatewayUrl overrides (SSRF hardening)", async () => { - callGatewayMock.mockResolvedValueOnce({ ok: true }); + mocks.callGateway.mockResolvedValueOnce({ ok: true }); await callGatewayTool( "health", { gatewayUrl: "ws://127.0.0.1:18789", gatewayToken: "t", timeoutMs: 5000 }, {}, ); - expect(callGatewayMock).toHaveBeenCalledWith( + expect(mocks.callGateway).toHaveBeenCalledWith( expect.objectContaining({ url: "ws://127.0.0.1:18789", token: "t", @@ -68,7 +64,7 @@ describe("gateway tool defaults", () => { }); it("falls back to config gateway.auth.token when env is unset for local overrides", () => { - configState.value = { + mocks.configState.value = { gateway: { auth: { token: "config-token" }, }, @@ -78,7 +74,7 @@ describe("gateway tool defaults", () => { }); it("uses gateway.remote.token for allowlisted remote overrides", () => { - configState.value = { + mocks.configState.value = { gateway: { remote: { url: "wss://gateway.example", @@ -93,7 +89,7 @@ describe("gateway tool defaults", () => { it("does not leak local env/config tokens to remote overrides", () => { process.env.OPENCLAW_GATEWAY_TOKEN = "local-env-token"; - configState.value = { + mocks.configState.value = { gateway: { auth: { token: "local-config-token" }, remote: { @@ -106,7 +102,7 @@ describe("gateway tool defaults", () => { }); it("ignores unresolved local token SecretRef for strict remote overrides", () => { - configState.value = { + mocks.configState.value = { gateway: { auth: { mode: "token", @@ -128,7 +124,7 @@ describe("gateway tool defaults", () => { it("explicit gatewayToken overrides fallback token resolution", () => { process.env.OPENCLAW_GATEWAY_TOKEN = "local-env-token"; - configState.value = { + mocks.configState.value = { gateway: { remote: { url: "wss://gateway.example", @@ -144,9 +140,9 @@ describe("gateway tool defaults", () => { }); it("uses least-privilege write scope for write methods", async () => { - callGatewayMock.mockResolvedValueOnce({ ok: true }); + mocks.callGateway.mockResolvedValueOnce({ ok: true }); await callGatewayTool("wake", {}, { mode: "now", text: "hi" }); - expect(callGatewayMock).toHaveBeenCalledWith( + expect(mocks.callGateway).toHaveBeenCalledWith( expect.objectContaining({ method: "wake", scopes: ["operator.write"], @@ -155,9 +151,9 @@ describe("gateway tool defaults", () => { }); it("uses admin scope only for admin methods", async () => { - callGatewayMock.mockResolvedValueOnce({ ok: true }); + mocks.callGateway.mockResolvedValueOnce({ ok: true }); await callGatewayTool("cron.add", {}, { id: "job-1" }); - expect(callGatewayMock).toHaveBeenCalledWith( + expect(mocks.callGateway).toHaveBeenCalledWith( expect.objectContaining({ method: "cron.add", scopes: ["operator.admin"], @@ -166,14 +162,14 @@ describe("gateway tool defaults", () => { }); it("allows explicit scope overrides for dynamic callers", async () => { - callGatewayMock.mockResolvedValueOnce({ ok: true }); + mocks.callGateway.mockResolvedValueOnce({ ok: true }); await callGatewayTool( "node.pair.approve", {}, { requestId: "req-1" }, { scopes: ["operator.admin"] }, ); - expect(callGatewayMock).toHaveBeenCalledWith( + expect(mocks.callGateway).toHaveBeenCalledWith( expect.objectContaining({ method: "node.pair.approve", scopes: ["operator.admin"], @@ -182,9 +178,9 @@ describe("gateway tool defaults", () => { }); it("default-denies unknown methods by sending no scopes", async () => { - callGatewayMock.mockResolvedValueOnce({ ok: true }); + mocks.callGateway.mockResolvedValueOnce({ ok: true }); await callGatewayTool("nonexistent.method", {}, {}); - expect(callGatewayMock).toHaveBeenCalledWith( + expect(mocks.callGateway).toHaveBeenCalledWith( expect.objectContaining({ method: "nonexistent.method", scopes: [],