test: hoist gateway helper imports

This commit is contained in:
Shakker
2026-05-09 17:19:50 +01:00
parent b66e20d684
commit a7e2dd0538

View File

@@ -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<string, unknown>,
const mocks = vi.hoisted(() => ({
callGateway: vi.fn(),
configState: {
value: {} as Record<string, unknown>,
},
}));
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: [],