test: tighten gateway tool assertions

This commit is contained in:
Peter Steinberger
2026-05-09 17:57:41 +01:00
parent 94e2928e6c
commit de9549f678

View File

@@ -1,4 +1,5 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { CallGatewayScopedOptions } from "../../gateway/call.js";
import { callGatewayTool, resolveGatewayOptions } from "./gateway.js";
const mocks = vi.hoisted(() => ({
@@ -15,6 +16,15 @@ vi.mock("../../gateway/call.js", () => ({
callGateway: (...args: unknown[]) => mocks.callGateway(...args),
}));
function capturedGatewayCall(): CallGatewayScopedOptions {
expect(mocks.callGateway).toHaveBeenCalledTimes(1);
const call = mocks.callGateway.mock.calls[0];
if (!call) {
throw new Error("expected callGateway to be called");
}
return call[0] as CallGatewayScopedOptions;
}
describe("gateway tool defaults", () => {
const envSnapshot = {
openclaw: process.env.OPENCLAW_GATEWAY_TOKEN,
@@ -46,14 +56,13 @@ describe("gateway tool defaults", () => {
{ gatewayUrl: "ws://127.0.0.1:18789", gatewayToken: "t", timeoutMs: 5000 },
{},
);
expect(mocks.callGateway).toHaveBeenCalledWith(
expect.objectContaining({
url: "ws://127.0.0.1:18789",
token: "t",
timeoutMs: 5000,
scopes: ["operator.read"],
}),
);
const call = capturedGatewayCall();
expect(call.method).toBe("health");
expect(call.params).toEqual({});
expect(call.url).toBe("ws://127.0.0.1:18789");
expect(call.token).toBe("t");
expect(call.timeoutMs).toBe(5000);
expect(call.scopes).toEqual(["operator.read"]);
});
it("uses OPENCLAW_GATEWAY_TOKEN for allowlisted local overrides", () => {
@@ -142,23 +151,19 @@ describe("gateway tool defaults", () => {
it("uses least-privilege write scope for write methods", async () => {
mocks.callGateway.mockResolvedValueOnce({ ok: true });
await callGatewayTool("wake", {}, { mode: "now", text: "hi" });
expect(mocks.callGateway).toHaveBeenCalledWith(
expect.objectContaining({
method: "wake",
scopes: ["operator.write"],
}),
);
const call = capturedGatewayCall();
expect(call.method).toBe("wake");
expect(call.params).toEqual({ mode: "now", text: "hi" });
expect(call.scopes).toEqual(["operator.write"]);
});
it("uses admin scope only for admin methods", async () => {
mocks.callGateway.mockResolvedValueOnce({ ok: true });
await callGatewayTool("cron.add", {}, { id: "job-1" });
expect(mocks.callGateway).toHaveBeenCalledWith(
expect.objectContaining({
method: "cron.add",
scopes: ["operator.admin"],
}),
);
const call = capturedGatewayCall();
expect(call.method).toBe("cron.add");
expect(call.params).toEqual({ id: "job-1" });
expect(call.scopes).toEqual(["operator.admin"]);
});
it("allows explicit scope overrides for dynamic callers", async () => {
@@ -169,23 +174,19 @@ describe("gateway tool defaults", () => {
{ requestId: "req-1" },
{ scopes: ["operator.admin"] },
);
expect(mocks.callGateway).toHaveBeenCalledWith(
expect.objectContaining({
method: "node.pair.approve",
scopes: ["operator.admin"],
}),
);
const call = capturedGatewayCall();
expect(call.method).toBe("node.pair.approve");
expect(call.params).toEqual({ requestId: "req-1" });
expect(call.scopes).toEqual(["operator.admin"]);
});
it("default-denies unknown methods by sending no scopes", async () => {
mocks.callGateway.mockResolvedValueOnce({ ok: true });
await callGatewayTool("nonexistent.method", {}, {});
expect(mocks.callGateway).toHaveBeenCalledWith(
expect.objectContaining({
method: "nonexistent.method",
scopes: [],
}),
);
const call = capturedGatewayCall();
expect(call.method).toBe("nonexistent.method");
expect(call.params).toEqual({});
expect(call.scopes).toEqual([]);
});
it("rejects non-allowlisted overrides (SSRF hardening)", async () => {