test(nodes): update coverage after exec consolidation

This commit is contained in:
Peter Steinberger
2026-03-30 00:40:18 +01:00
parent 5dae663ea4
commit 2255e04b07
5 changed files with 44 additions and 381 deletions

View File

@@ -17,7 +17,6 @@ vi.mock("../media/image-ops.js", () => ({
let createOpenClawTools: typeof import("./openclaw-tools.js").createOpenClawTools;
const NODE_ID = "mac-1";
const BASE_RUN_INPUT = { action: "run", node: NODE_ID, command: ["echo", "hi"] } as const;
const JPG_PAYLOAD = {
format: "jpg",
base64: "aGVsbG8=",
@@ -138,43 +137,6 @@ function setupNodeInvokeMock(params: {
});
}
function createSystemRunPreparePayload(cwd: string | null) {
return {
payload: {
plan: {
argv: ["echo", "hi"],
cwd,
commandText: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
function setupSystemRunGateway(params: {
onRunInvoke: (invokeParams: unknown) => GatewayMockResult | Promise<GatewayMockResult>;
onApprovalRequest?: (approvalParams: unknown) => GatewayMockResult | Promise<GatewayMockResult>;
prepareCwd?: string | null;
}) {
callGateway.mockImplementation(async ({ method, params: gatewayParams }: GatewayCall) => {
if (method === "node.list") {
return mockNodeList({ commands: ["system.run"] });
}
if (method === "node.invoke") {
const command = (gatewayParams as { command?: string } | undefined)?.command;
if (command === "system.run.prepare") {
return createSystemRunPreparePayload(params.prepareCwd ?? null);
}
return await params.onRunInvoke(gatewayParams);
}
if (method === "exec.approval.request" && params.onApprovalRequest) {
return await params.onApprovalRequest(gatewayParams);
}
return unexpectedGatewayMethod(method);
});
}
function setupPhotosLatestMock(params?: { remoteIp?: string }) {
setupNodeInvokeMock({
...(params?.remoteIp ? { remoteIp: params.remoteIp } : {}),
@@ -604,119 +566,6 @@ describe("nodes device_status and device_info", () => {
});
});
describe("nodes run", () => {
it("passes invoke and command timeouts", async () => {
setupSystemRunGateway({
prepareCwd: "/tmp",
onRunInvoke: (invokeParams) => {
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "system.run",
timeoutMs: 45_000,
params: {
command: ["echo", "hi"],
cwd: "/tmp",
env: { FOO: "bar" },
timeoutMs: 12_000,
},
});
return {
payload: { stdout: "", stderr: "", exitCode: 0, success: true },
};
},
});
await executeNodes({
...BASE_RUN_INPUT,
cwd: "/tmp",
env: ["FOO=bar"],
commandTimeoutMs: 12_000,
invokeTimeoutMs: 45_000,
});
});
it("requests approval and retries with allow-once decision", async () => {
let invokeCalls = 0;
let approvalId: string | null = null;
setupSystemRunGateway({
onRunInvoke: (invokeParams) => {
invokeCalls += 1;
if (invokeCalls === 1) {
throw new Error("SYSTEM_RUN_DENIED: approval required");
}
expect(invokeParams).toMatchObject({
nodeId: NODE_ID,
command: "system.run",
params: {
command: ["echo", "hi"],
runId: approvalId,
approved: true,
approvalDecision: "allow-once",
},
});
return { payload: { stdout: "", stderr: "", exitCode: 0, success: true } };
},
onApprovalRequest: (approvalParams) => {
expect(approvalParams).toMatchObject({
id: expect.any(String),
systemRunPlan: expect.objectContaining({
argv: ["echo", "hi"],
commandText: "echo hi",
}),
nodeId: NODE_ID,
host: "node",
timeoutMs: 120_000,
});
approvalId =
typeof (approvalParams as { id?: unknown } | undefined)?.id === "string"
? ((approvalParams as { id: string }).id ?? null)
: null;
return { decision: "allow-once" };
},
});
await executeNodes(BASE_RUN_INPUT);
expect(invokeCalls).toBe(2);
});
it("fails with user denied when approval decision is deny", async () => {
setupSystemRunGateway({
onRunInvoke: () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
},
onApprovalRequest: () => {
return { decision: "deny" };
},
});
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: user denied");
});
it("fails closed for timeout and invalid approval decisions", async () => {
setupSystemRunGateway({
onRunInvoke: () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
},
onApprovalRequest: () => {
return {};
},
});
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: approval timed out");
setupSystemRunGateway({
onRunInvoke: () => {
throw new Error("SYSTEM_RUN_DENIED: approval required");
},
onApprovalRequest: () => {
return { decision: "allow-never" };
},
});
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow(
"exec denied: invalid approval decision",
);
});
});
describe("nodes invoke", () => {
it("allows metadata-only camera.list via generic invoke", async () => {
setupNodeInvokeMock({

View File

@@ -8,8 +8,6 @@ const gatewayMocks = vi.hoisted(() => ({
const nodeUtilsMocks = vi.hoisted(() => ({
resolveNodeId: vi.fn(async () => "node-1"),
resolveNode: vi.fn(async () => ({ nodeId: "node-1", remoteIp: "127.0.0.1" })),
listNodes: vi.fn(async () => [] as Array<{ nodeId: string; commands?: string[] }>),
resolveNodeIdFromList: vi.fn(() => "node-1"),
}));
const nodesCameraMocks = vi.hoisted(() => ({
@@ -48,8 +46,6 @@ vi.mock("./gateway.js", () => ({
vi.mock("./nodes-utils.js", () => ({
resolveNodeId: nodeUtilsMocks.resolveNodeId,
resolveNode: nodeUtilsMocks.resolveNode,
listNodes: nodeUtilsMocks.listNodes,
resolveNodeIdFromList: nodeUtilsMocks.resolveNodeIdFromList,
}));
vi.mock("../../cli/nodes-camera.js", () => ({
@@ -77,8 +73,6 @@ async function loadFreshNodesToolModuleForTest() {
vi.doMock("./nodes-utils.js", () => ({
resolveNodeId: nodeUtilsMocks.resolveNodeId,
resolveNode: nodeUtilsMocks.resolveNode,
listNodes: nodeUtilsMocks.listNodes,
resolveNodeIdFromList: nodeUtilsMocks.resolveNodeIdFromList,
}));
vi.doMock("../../cli/nodes-camera.js", () => ({
cameraTempPath: nodesCameraMocks.cameraTempPath,
@@ -148,50 +142,15 @@ describe("createNodesTool screen_record duration guardrails", () => {
);
});
it("omits rawCommand when preparing wrapped argv execution", async () => {
nodeUtilsMocks.listNodes.mockResolvedValue([
{
nodeId: "node-1",
commands: ["system.run"],
},
]);
gatewayMocks.callGatewayTool.mockImplementation(async (_method, _opts, payload) => {
if (payload?.command === "system.run.prepare") {
return {
payload: {
plan: {
argv: ["bash", "-lc", "echo hi"],
cwd: null,
commandText: 'bash -lc "echo hi"',
commandPreview: "echo hi",
agentId: null,
sessionKey: null,
},
},
};
}
if (payload?.command === "system.run") {
return { payload: { ok: true } };
}
throw new Error(`unexpected command: ${String(payload?.command)}`);
});
it("rejects the removed run action", async () => {
const tool = createNodesTool();
await tool.execute("call-1", {
action: "run",
node: "macbook",
command: ["bash", "-lc", "echo hi"],
});
const prepareCall = gatewayMocks.callGatewayTool.mock.calls.find(
(call) => call[2]?.command === "system.run.prepare",
)?.[2];
expect(prepareCall).toBeTruthy();
expect(prepareCall?.params).toMatchObject({
command: ["bash", "-lc", "echo hi"],
agentId: "main",
});
expect(prepareCall?.params).not.toHaveProperty("rawCommand");
await expect(
tool.execute("call-1", {
action: "run",
node: "macbook",
}),
).rejects.toThrow("Unknown action: run");
});
it("returns camera snaps via details.media.mediaUrls", async () => {
gatewayMocks.callGatewayTool.mockResolvedValue({ payload: { ok: true } });
@@ -385,4 +344,16 @@ describe("createNodesTool screen_record duration guardrails", () => {
{ scopes: ["operator.write"] },
);
});
it("blocks invokeCommand system.run so exec stays the only shell path", async () => {
const tool = createNodesTool();
await expect(
tool.execute("call-1", {
action: "invoke",
node: "macbook",
invokeCommand: "system.run",
}),
).rejects.toThrow('invokeCommand "system.run" is reserved for shell execution');
});
});