Files
openclaw/src/cli/nodes-cli.coverage.test.ts
Peter Steinberger c6575891c7 fix(exec): inherit ask from exec-approvals.json when tools.exec.ask unset
Landed from contributor PR #29187 by @Bartok9.

Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
2026-03-08 00:35:50 +00:00

316 lines
8.0 KiB
TypeScript

import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ExecApprovalsFile } from "../infra/exec-approvals.js";
import { buildSystemRunPreparePayload } from "../test-utils/system-run-prepare-payload.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
type NodeInvokeCall = {
method?: string;
params?: {
idempotencyKey?: string;
command?: string;
params?: unknown;
timeoutMs?: number;
};
};
let lastNodeInvokeCall: NodeInvokeCall | null = null;
let lastApprovalRequestCall: { params?: Record<string, unknown> } | null = null;
let localExecApprovalsFile: ExecApprovalsFile = { version: 1, agents: {} };
let nodeExecApprovalsFile: ExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
ask: "on-miss",
askFallback: "deny",
},
agents: {},
};
const callGateway = vi.fn(async (opts: NodeInvokeCall) => {
if (opts.method === "node.list") {
return {
nodes: [
{
nodeId: "mac-1",
displayName: "Mac",
platform: "macos",
caps: ["canvas"],
connected: true,
permissions: { screenRecording: true },
},
],
};
}
if (opts.method === "node.invoke") {
lastNodeInvokeCall = opts;
const command = opts.params?.command;
if (command === "system.run.prepare") {
const params = (opts.params?.params ?? {}) as {
command?: unknown[];
rawCommand?: unknown;
cwd?: unknown;
agentId?: unknown;
};
return buildSystemRunPreparePayload(params);
}
return {
payload: {
stdout: "",
stderr: "",
exitCode: 0,
success: true,
timedOut: false,
},
};
}
if (opts.method === "exec.approvals.node.get") {
return {
path: "/tmp/exec-approvals.json",
exists: true,
hash: "hash",
file: nodeExecApprovalsFile,
};
}
if (opts.method === "exec.approval.request") {
lastApprovalRequestCall = opts as { params?: Record<string, unknown> };
return { decision: "allow-once" };
}
return { ok: true };
});
const randomIdempotencyKey = vi.fn(() => "rk_test");
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
vi.mock("../gateway/call.js", () => ({
callGateway: (opts: unknown) => callGateway(opts as NodeInvokeCall),
randomIdempotencyKey: () => randomIdempotencyKey(),
}));
vi.mock("../runtime.js", () => ({
defaultRuntime,
}));
vi.mock("../config/config.js", () => ({
loadConfig: () => ({}),
}));
vi.mock("../infra/exec-approvals.js", async () => {
const actual = await vi.importActual<typeof import("../infra/exec-approvals.js")>(
"../infra/exec-approvals.js",
);
return {
...actual,
loadExecApprovals: () => localExecApprovalsFile,
};
});
describe("nodes-cli coverage", () => {
let registerNodesCli: (program: Command) => void;
let sharedProgram: Command;
const getNodeInvokeCall = () => {
const last = lastNodeInvokeCall;
if (!last) {
throw new Error("expected node.invoke call");
}
return last;
};
const getApprovalRequestCall = () => lastApprovalRequestCall;
const runNodesCommand = async (args: string[]) => {
await sharedProgram.parseAsync(args, { from: "user" });
return getNodeInvokeCall();
};
beforeAll(async () => {
({ registerNodesCli } = await import("./nodes-cli.js"));
sharedProgram = new Command();
sharedProgram.exitOverride();
registerNodesCli(sharedProgram);
});
beforeEach(() => {
resetRuntimeCapture();
callGateway.mockClear();
randomIdempotencyKey.mockClear();
lastNodeInvokeCall = null;
lastApprovalRequestCall = null;
localExecApprovalsFile = { version: 1, agents: {} };
nodeExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
ask: "on-miss",
askFallback: "deny",
},
agents: {},
};
});
it("invokes system.run with parsed params", async () => {
const invoke = await runNodesCommand([
"nodes",
"run",
"--node",
"mac-1",
"--cwd",
"/tmp",
"--env",
"FOO=bar",
"--command-timeout",
"1200",
"--needs-screen-recording",
"--invoke-timeout",
"5000",
"echo",
"hi",
]);
expect(invoke).toBeTruthy();
expect(invoke?.params?.idempotencyKey).toBe("rk_test");
expect(invoke?.params?.command).toBe("system.run");
expect(invoke?.params?.params).toEqual({
command: ["echo", "hi"],
rawCommand: null,
cwd: "/tmp",
env: { FOO: "bar" },
timeoutMs: 1200,
needsScreenRecording: true,
agentId: "main",
approved: true,
approvalDecision: "allow-once",
runId: expect.any(String),
});
expect(invoke?.params?.timeoutMs).toBe(5000);
const approval = getApprovalRequestCall();
expect(approval?.params?.["commandArgv"]).toEqual(["echo", "hi"]);
expect(approval?.params?.["systemRunPlan"]).toEqual({
argv: ["echo", "hi"],
cwd: "/tmp",
rawCommand: null,
agentId: "main",
sessionKey: null,
});
});
it("invokes system.run with raw command", async () => {
const invoke = await runNodesCommand([
"nodes",
"run",
"--agent",
"main",
"--node",
"mac-1",
"--raw",
"echo hi",
]);
expect(invoke).toBeTruthy();
expect(invoke?.params?.idempotencyKey).toBe("rk_test");
expect(invoke?.params?.command).toBe("system.run");
expect(invoke?.params?.params).toMatchObject({
command: ["/bin/sh", "-lc", "echo hi"],
rawCommand: "echo hi",
agentId: "main",
approved: true,
approvalDecision: "allow-once",
runId: expect.any(String),
});
const approval = getApprovalRequestCall();
expect(approval?.params?.["commandArgv"]).toEqual(["/bin/sh", "-lc", "echo hi"]);
expect(approval?.params?.["systemRunPlan"]).toEqual({
argv: ["/bin/sh", "-lc", "echo hi"],
cwd: null,
rawCommand: "echo hi",
agentId: "main",
sessionKey: null,
});
});
it("inherits ask=off from local exec approvals when tools.exec.ask is unset", async () => {
localExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
ask: "off",
askFallback: "deny",
},
agents: {},
};
nodeExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
askFallback: "deny",
},
agents: {},
};
const invoke = await runNodesCommand(["nodes", "run", "--node", "mac-1", "echo", "hi"]);
expect(invoke).toBeTruthy();
expect(invoke?.params?.command).toBe("system.run");
expect(invoke?.params?.params).toMatchObject({
command: ["echo", "hi"],
approved: false,
});
expect(invoke?.params?.params).not.toHaveProperty("approvalDecision");
expect(getApprovalRequestCall()).toBeNull();
});
it("invokes system.notify with provided fields", async () => {
const invoke = await runNodesCommand([
"nodes",
"notify",
"--node",
"mac-1",
"--title",
"Ping",
"--body",
"Gateway ready",
"--delivery",
"overlay",
]);
expect(invoke).toBeTruthy();
expect(invoke?.params?.command).toBe("system.notify");
expect(invoke?.params?.params).toEqual({
title: "Ping",
body: "Gateway ready",
sound: undefined,
priority: undefined,
delivery: "overlay",
});
});
it("invokes location.get with params", async () => {
const invoke = await runNodesCommand([
"nodes",
"location",
"get",
"--node",
"mac-1",
"--accuracy",
"precise",
"--max-age",
"1000",
"--location-timeout",
"5000",
"--invoke-timeout",
"6000",
]);
expect(invoke).toBeTruthy();
expect(invoke?.params?.command).toBe("location.get");
expect(invoke?.params?.params).toEqual({
maxAgeMs: 1000,
desiredAccuracy: "precise",
timeoutMs: 5000,
});
expect(invoke?.params?.timeoutMs).toBe(6000);
});
});