Files
Moltbot/src/agents/tools/nodes-tool.test.ts
2026-03-12 22:27:52 +00:00

140 lines
4.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const gatewayMocks = vi.hoisted(() => ({
callGatewayTool: vi.fn(),
readGatewayCallOptions: vi.fn(() => ({})),
}));
const nodeUtilsMocks = vi.hoisted(() => ({
resolveNodeId: vi.fn(async () => "node-1"),
listNodes: vi.fn(async () => [] as Array<{ nodeId: string; commands?: string[] }>),
resolveNodeIdFromList: vi.fn(() => "node-1"),
}));
const screenMocks = vi.hoisted(() => ({
parseScreenRecordPayload: vi.fn(() => ({
base64: "ZmFrZQ==",
format: "mp4",
durationMs: 300_000,
fps: 10,
screenIndex: 0,
hasAudio: true,
})),
screenRecordTempPath: vi.fn(() => "/tmp/screen-record.mp4"),
writeScreenRecordToFile: vi.fn(async () => ({ path: "/tmp/screen-record.mp4" })),
}));
vi.mock("./gateway.js", () => ({
callGatewayTool: gatewayMocks.callGatewayTool,
readGatewayCallOptions: gatewayMocks.readGatewayCallOptions,
}));
vi.mock("./nodes-utils.js", () => ({
resolveNodeId: nodeUtilsMocks.resolveNodeId,
listNodes: nodeUtilsMocks.listNodes,
resolveNodeIdFromList: nodeUtilsMocks.resolveNodeIdFromList,
}));
vi.mock("../../cli/nodes-screen.js", () => ({
parseScreenRecordPayload: screenMocks.parseScreenRecordPayload,
screenRecordTempPath: screenMocks.screenRecordTempPath,
writeScreenRecordToFile: screenMocks.writeScreenRecordToFile,
}));
import { createNodesTool } from "./nodes-tool.js";
describe("createNodesTool screen_record duration guardrails", () => {
beforeEach(() => {
gatewayMocks.callGatewayTool.mockReset();
gatewayMocks.readGatewayCallOptions.mockReset();
gatewayMocks.readGatewayCallOptions.mockReturnValue({});
nodeUtilsMocks.resolveNodeId.mockClear();
screenMocks.parseScreenRecordPayload.mockClear();
screenMocks.writeScreenRecordToFile.mockClear();
});
it("marks nodes as owner-only", () => {
const tool = createNodesTool();
expect(tool.ownerOnly).toBe(true);
});
it("caps durationMs schema at 300000", () => {
const tool = createNodesTool();
const schema = tool.parameters as {
properties?: {
durationMs?: {
maximum?: number;
};
};
};
expect(schema.properties?.durationMs?.maximum).toBe(300_000);
});
it("clamps screen_record durationMs argument to 300000 before gateway invoke", async () => {
gatewayMocks.callGatewayTool.mockResolvedValue({ payload: { ok: true } });
const tool = createNodesTool();
await tool.execute("call-1", {
action: "screen_record",
node: "macbook",
durationMs: 900_000,
});
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
"node.invoke",
{},
expect.objectContaining({
params: expect.objectContaining({
durationMs: 300_000,
}),
}),
);
});
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)}`);
});
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");
});
});