import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerSecurityCli } from "./security-cli.js"; const mocks = vi.hoisted(() => { const runtimeLogs: string[] = []; const stringifyArgs = (args: unknown[]) => args.map((value) => String(value)).join(" "); const defaultRuntime = { log: vi.fn((...args: unknown[]) => { runtimeLogs.push(stringifyArgs(args)); }), error: vi.fn(), writeStdout: vi.fn((value: string) => { defaultRuntime.log(value.endsWith("\n") ? value.slice(0, -1) : value); }), writeJson: vi.fn((value: unknown, space = 2) => { defaultRuntime.log(JSON.stringify(value, null, space > 0 ? space : undefined)); }), exit: vi.fn((code: number) => { throw new Error(`__exit__:${code}`); }), }; return { loadConfig: vi.fn(), runSecurityAudit: vi.fn(), fixSecurityFootguns: vi.fn(), resolveCommandSecretRefsViaGateway: vi.fn(), getSecurityAuditCommandSecretTargetIds: vi.fn( () => new Set(["gateway.auth.token", "gateway.auth.password"]), ), defaultRuntime, runtimeLogs, }; }); const { loadConfig, runSecurityAudit, fixSecurityFootguns, resolveCommandSecretRefsViaGateway, getSecurityAuditCommandSecretTargetIds, runtimeLogs, } = mocks; vi.mock("../config/config.js", () => ({ loadConfig: () => mocks.loadConfig(), })); vi.mock("../runtime.js", () => ({ defaultRuntime: mocks.defaultRuntime, })); vi.mock("../security/audit.js", () => ({ runSecurityAudit: (opts: unknown) => mocks.runSecurityAudit(opts), })); vi.mock("../security/fix.js", () => ({ fixSecurityFootguns: () => mocks.fixSecurityFootguns(), })); vi.mock("./command-secret-gateway.js", () => ({ resolveCommandSecretRefsViaGateway: (opts: unknown) => mocks.resolveCommandSecretRefsViaGateway(opts), })); vi.mock("./command-secret-targets.js", () => ({ getSecurityAuditCommandSecretTargetIds: () => mocks.getSecurityAuditCommandSecretTargetIds(), })); function createProgram() { const program = new Command(); program.exitOverride(); registerSecurityCli(program); return program; } function primeDeepAuditConfig(sourceConfig = { gateway: { mode: "local" } }) { loadConfig.mockReturnValue(sourceConfig); resolveCommandSecretRefsViaGateway.mockResolvedValue({ resolvedConfig: sourceConfig, diagnostics: [], targetStatesByPath: {}, hadUnresolvedTargets: false, }); runSecurityAudit.mockResolvedValue({ ts: 0, summary: { critical: 0, warn: 0, info: 0 }, findings: [], }); return sourceConfig; } describe("security CLI", () => { beforeEach(() => { runtimeLogs.length = 0; loadConfig.mockReset(); runSecurityAudit.mockReset(); fixSecurityFootguns.mockReset(); resolveCommandSecretRefsViaGateway.mockReset(); getSecurityAuditCommandSecretTargetIds.mockClear(); fixSecurityFootguns.mockResolvedValue({ changes: [], actions: [], errors: [], }); }); it("runs audit with read-only SecretRef resolution and prints JSON diagnostics", async () => { const sourceConfig = { gateway: { auth: { mode: "token", token: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" }, }, }, secrets: { providers: { default: { source: "env" }, }, }, }; const resolvedConfig = { ...sourceConfig, gateway: { ...sourceConfig.gateway, auth: { ...sourceConfig.gateway.auth, token: "resolved-token", }, }, }; loadConfig.mockReturnValue(sourceConfig); resolveCommandSecretRefsViaGateway.mockResolvedValue({ resolvedConfig, diagnostics: [ "security audit: gateway secrets.resolve unavailable (gateway closed); resolved command secrets locally.", ], targetStatesByPath: {}, hadUnresolvedTargets: false, }); runSecurityAudit.mockResolvedValue({ ts: 0, summary: { critical: 0, warn: 1, info: 0 }, findings: [ { checkId: "gateway.probe_failed", severity: "warn", title: "Gateway probe failed (deep)", detail: "connect failed: connect ECONNREFUSED 127.0.0.1:18789", }, ], }); await createProgram().parseAsync(["security", "audit", "--json"], { from: "user" }); expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith( expect.objectContaining({ config: sourceConfig, commandName: "security audit", mode: "read_only_status", targetIds: expect.any(Set), }), ); expect(runSecurityAudit).toHaveBeenCalledWith( expect.objectContaining({ config: resolvedConfig, sourceConfig, deep: false, includeFilesystem: true, includeChannelSecurity: true, }), ); const payload = JSON.parse(String(runtimeLogs.at(-1))); expect(payload.secretDiagnostics).toEqual([ "security audit: gateway secrets.resolve unavailable (gateway closed); resolved command secrets locally.", ]); }); it.each([ { title: "forwards --token to deep probe auth without altering command-level resolver mode", argv: ["--token", "explicit-token"], deepProbeAuth: { token: "explicit-token" }, }, { title: "forwards --password to deep probe auth without altering command-level resolver mode", argv: ["--password", "explicit-password"], deepProbeAuth: { password: "explicit-password" }, }, { title: "forwards both --token and --password to deep probe auth", argv: ["--token", "explicit-token", "--password", "explicit-password"], deepProbeAuth: { token: "explicit-token", password: "explicit-password", }, }, ])("$title", async ({ argv, deepProbeAuth }) => { primeDeepAuditConfig(); await createProgram().parseAsync(["security", "audit", "--deep", ...argv, "--json"], { from: "user", }); expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith( expect.objectContaining({ mode: "read_only_status", }), ); expect(runSecurityAudit).toHaveBeenCalledWith( expect.objectContaining({ deep: true, deepProbeAuth, }), ); }); });