
























@@ -1,4 +1,5 @@
11import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2+import type { CallGatewayScopedOptions } from "../../gateway/call.js";
23import { callGatewayTool, resolveGatewayOptions } from "./gateway.js";
3445const mocks = vi.hoisted(() => ({
@@ -15,6 +16,15 @@ vi.mock("../../gateway/call.js", () => ({
1516callGateway: (...args: unknown[]) => mocks.callGateway(...args),
1617}));
171819+function capturedGatewayCall(): CallGatewayScopedOptions {
20+expect(mocks.callGateway).toHaveBeenCalledTimes(1);
21+const call = mocks.callGateway.mock.calls[0];
22+if (!call) {
23+throw new Error("expected callGateway to be called");
24+}
25+return call[0] as CallGatewayScopedOptions;
26+}
27+1828describe("gateway tool defaults", () => {
1929const envSnapshot = {
2030openclaw: process.env.OPENCLAW_GATEWAY_TOKEN,
@@ -46,14 +56,13 @@ describe("gateway tool defaults", () => {
4656{ gatewayUrl: "ws://127.0.0.1:18789", gatewayToken: "t", timeoutMs: 5000 },
4757{},
4858);
49-expect(mocks.callGateway).toHaveBeenCalledWith(
50-expect.objectContaining({
51-url: "ws://127.0.0.1:18789",
52-token: "t",
53-timeoutMs: 5000,
54-scopes: ["operator.read"],
55-}),
56-);
59+const call = capturedGatewayCall();
60+expect(call.method).toBe("health");
61+expect(call.params).toEqual({});
62+expect(call.url).toBe("ws://127.0.0.1:18789");
63+expect(call.token).toBe("t");
64+expect(call.timeoutMs).toBe(5000);
65+expect(call.scopes).toEqual(["operator.read"]);
5766});
58675968it("uses OPENCLAW_GATEWAY_TOKEN for allowlisted local overrides", () => {
@@ -142,23 +151,19 @@ describe("gateway tool defaults", () => {
142151it("uses least-privilege write scope for write methods", async () => {
143152mocks.callGateway.mockResolvedValueOnce({ ok: true });
144153await callGatewayTool("wake", {}, { mode: "now", text: "hi" });
145-expect(mocks.callGateway).toHaveBeenCalledWith(
146-expect.objectContaining({
147-method: "wake",
148-scopes: ["operator.write"],
149-}),
150-);
154+const call = capturedGatewayCall();
155+expect(call.method).toBe("wake");
156+expect(call.params).toEqual({ mode: "now", text: "hi" });
157+expect(call.scopes).toEqual(["operator.write"]);
151158});
152159153160it("uses admin scope only for admin methods", async () => {
154161mocks.callGateway.mockResolvedValueOnce({ ok: true });
155162await callGatewayTool("cron.add", {}, { id: "job-1" });
156-expect(mocks.callGateway).toHaveBeenCalledWith(
157-expect.objectContaining({
158-method: "cron.add",
159-scopes: ["operator.admin"],
160-}),
161-);
163+const call = capturedGatewayCall();
164+expect(call.method).toBe("cron.add");
165+expect(call.params).toEqual({ id: "job-1" });
166+expect(call.scopes).toEqual(["operator.admin"]);
162167});
163168164169it("allows explicit scope overrides for dynamic callers", async () => {
@@ -169,23 +174,19 @@ describe("gateway tool defaults", () => {
169174{ requestId: "req-1" },
170175{ scopes: ["operator.admin"] },
171176);
172-expect(mocks.callGateway).toHaveBeenCalledWith(
173-expect.objectContaining({
174-method: "node.pair.approve",
175-scopes: ["operator.admin"],
176-}),
177-);
177+const call = capturedGatewayCall();
178+expect(call.method).toBe("node.pair.approve");
179+expect(call.params).toEqual({ requestId: "req-1" });
180+expect(call.scopes).toEqual(["operator.admin"]);
178181});
179182180183it("default-denies unknown methods by sending no scopes", async () => {
181184mocks.callGateway.mockResolvedValueOnce({ ok: true });
182185await callGatewayTool("nonexistent.method", {}, {});
183-expect(mocks.callGateway).toHaveBeenCalledWith(
184-expect.objectContaining({
185-method: "nonexistent.method",
186-scopes: [],
187-}),
188-);
186+const call = capturedGatewayCall();
187+expect(call.method).toBe("nonexistent.method");
188+expect(call.params).toEqual({});
189+expect(call.scopes).toEqual([]);
189190});
190191191192it("rejects non-allowlisted overrides (SSRF hardening)", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。