mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 12:20:28 +00:00
test(nodes): update coverage after exec consolidation
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user