import { afterEach, describe, expect, it, vi } from "vitest"; import type { CliCommandCatalogEntry, CliCommandPathPolicy } from "./command-catalog.js"; import { resolveCliCatalogCommandPath, resolveCliCommandPathPolicy, resolveCliNetworkProxyPolicy, } from "./command-path-policy.js"; const DEFAULT_EXPECTED_POLICY: CliCommandPathPolicy = { bypassConfigGuard: false, routeConfigGuard: "never", loadPlugins: "never", pluginRegistry: { scope: "all" }, hideBanner: false, ensureCliPath: true, networkProxy: "default", }; function expectResolvedPolicy( commandPath: string[], expected: Partial, ): void { expect(resolveCliCommandPathPolicy(commandPath)).toEqual({ ...DEFAULT_EXPECTED_POLICY, ...expected, }); } describe("command-path-policy", () => { afterEach(() => { vi.doUnmock("./command-catalog.js"); vi.resetModules(); }); it("resolves status policy with shared startup semantics", () => { expectResolvedPolicy(["status"], { routeConfigGuard: "when-suppressed", loadPlugins: "never", pluginRegistry: { scope: "channels" }, ensureCliPath: false, networkProxy: "bypass", }); }); it("applies exact overrides after broader channel plugin rules", () => { expectResolvedPolicy(["channels", "send"], { loadPlugins: "always", pluginRegistry: { scope: "configured-channels" }, }); expectResolvedPolicy(["channels", "login"], { loadPlugins: "always", pluginRegistry: { scope: "configured-channels" }, }); expectResolvedPolicy(["channels", "capabilities"], { loadPlugins: "always", pluginRegistry: { scope: "configured-channels" }, }); expectResolvedPolicy(["channels", "add"], { loadPlugins: "never", pluginRegistry: { scope: "configured-channels" }, networkProxy: "bypass", }); expectResolvedPolicy(["channels", "status"], { loadPlugins: "never", pluginRegistry: { scope: "configured-channels" }, networkProxy: expect.any(Function), }); expectResolvedPolicy(["channels", "list"], { loadPlugins: "never", pluginRegistry: { scope: "configured-channels" }, networkProxy: "bypass", }); expectResolvedPolicy(["channels", "logs"], { loadPlugins: "never", pluginRegistry: { scope: "configured-channels" }, networkProxy: "bypass", }); expectResolvedPolicy(["channels", "remove"], { loadPlugins: "always", pluginRegistry: { scope: "configured-channels" }, networkProxy: "bypass", }); expectResolvedPolicy(["channels", "resolve"], { loadPlugins: "always", pluginRegistry: { scope: "configured-channels" }, networkProxy: "bypass", }); }); it("keeps config-only agent commands on config-only startup", () => { expectResolvedPolicy(["agent"], { loadPlugins: expect.any(Function), pluginRegistry: { scope: "all" }, networkProxy: expect.any(Function), }); for (const commandPath of [ ["agents"], ["agents", "list"], ["agents", "bind"], ["agents", "bindings"], ["agents", "unbind"], ["agents", "set-identity"], ["agents", "delete"], ]) { expectResolvedPolicy(commandPath, { loadPlugins: "never", networkProxy: "bypass", }); } }); it("resolves mixed startup-only rules", () => { expectResolvedPolicy(["configure"], { bypassConfigGuard: true, loadPlugins: "never", }); expectResolvedPolicy(["config", "validate"], { bypassConfigGuard: true, loadPlugins: "never", networkProxy: "bypass", }); expectResolvedPolicy(["gateway", "status"], { routeConfigGuard: "always", loadPlugins: "never", networkProxy: "bypass", }); expectResolvedPolicy(["plugins", "update"], { loadPlugins: "never", hideBanner: true, }); for (const commandPath of [ ["plugins", "install"], ["plugins", "list"], ["plugins", "inspect"], ["plugins", "registry"], ["plugins", "doctor"], ]) { expectResolvedPolicy(commandPath, { loadPlugins: "never", }); } expectResolvedPolicy(["cron", "list"], { bypassConfigGuard: true, loadPlugins: "never", networkProxy: "bypass", }); }); it("defaults unknown command paths to network proxy routing", () => { expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "googlemeet", "login"])).toBe( "default", ); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "tool", "image_generate"])).toBe( "bypass", ); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "tools", "effective"])).toBe("bypass"); }); it("resolves static network proxy bypass policies from the catalog", () => { expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "status"])).toBe("bypass"); expect( resolveCliNetworkProxyPolicy(["node", "openclaw", "config", "get", "proxy.enabled"]), ).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "proxy", "start"])).toBe("bypass"); }); it("resolves mixed network proxy policies from argv-sensitive catalog entries", () => { expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway"])).toBe("default"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway", "run"])).toBe("default"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway", "health"])).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "node", "run"])).toBe("default"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "node", "status"])).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "agent", "--local"])).toBe("default"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "agent", "run"])).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "channels", "status"])).toBe("bypass"); expect( resolveCliNetworkProxyPolicy(["node", "openclaw", "channels", "status", "--probe"]), ).toBe("default"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "models", "status"])).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "models", "status", "--probe"])).toBe( "default", ); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "info", "browser"])).toBe( "bypass", ); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "check"])).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "list"])).toBe("bypass"); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "search", "browser"])).toBe( "default", ); }); it("uses the longest catalog command path for deep network proxy overrides", async () => { const catalog: readonly CliCommandCatalogEntry[] = [ { commandPath: ["nodes"], policy: { networkProxy: "bypass" } }, { commandPath: ["nodes", "camera", "snap"], exact: true, policy: { networkProxy: "default" }, }, ]; vi.resetModules(); vi.doMock("./command-catalog.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, cliCommandCatalog: catalog }; }); const { resolveCliCatalogCommandPath, resolveCliNetworkProxyPolicy } = await import("./command-path-policy.js"); expect(resolveCliCatalogCommandPath(["node", "openclaw", "nodes", "camera", "snap"])).toEqual([ "nodes", "camera", "snap", ]); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "nodes", "camera", "snap"])).toBe( "default", ); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "nodes", "camera", "list"])).toBe( "bypass", ); }); it("stops catalog command path resolution before positional arguments", () => { expect( resolveCliCatalogCommandPath(["node", "openclaw", "config", "get", "proxy.enabled"]), ).toEqual(["config", "get"]); expect( resolveCliCatalogCommandPath(["node", "openclaw", "message", "send", "--to", "demo"]), ).toEqual(["message"]); }); it("treats bare gateway invocations with options as the gateway runtime", () => { const argv = ["node", "openclaw", "gateway", "--port", "1234"]; expect(resolveCliCatalogCommandPath(argv)).toEqual(["gateway"]); expect(resolveCliNetworkProxyPolicy(argv)).toBe("default"); }); it("does not let gateway run option values spoof bypass subcommands", () => { for (const argv of [ ["node", "openclaw", "gateway", "--token", "status"], ["node", "openclaw", "gateway", "--token=status"], ["node", "openclaw", "gateway", "--password", "health"], ["node", "openclaw", "gateway", "--password-file", "status"], ["node", "openclaw", "gateway", "--ws-log", "compact"], ]) { expect(resolveCliCatalogCommandPath(argv), argv.join(" ")).toEqual(["gateway"]); expect(resolveCliNetworkProxyPolicy(argv), argv.join(" ")).toBe("default"); } }); it("still resolves real gateway bypass subcommands after their command token", () => { expect(resolveCliCatalogCommandPath(["node", "openclaw", "gateway", "status"])).toEqual([ "gateway", "status", ]); expect( resolveCliCatalogCommandPath(["node", "openclaw", "gateway", "status", "--token", "secret"]), ).toEqual(["gateway", "status"]); expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway", "status"])).toBe("bypass"); }); });