























@@ -38,6 +38,15 @@ const screenMocks = vi.hoisted(() => ({
3838})),
3939screenRecordTempPath: vi.fn(() => "/tmp/screen-record.mp4"),
4040writeScreenRecordToFile: vi.fn(async () => ({ path: "/tmp/screen-record.mp4" })),
41+parseScreenSnapshotPayload: vi.fn(() => ({
42+base64: "ZmFrZQ==",
43+format: "png",
44+screenIndex: 0,
45+width: 1920,
46+height: 1080,
47+})),
48+screenSnapshotTempPath: vi.fn(() => "/tmp/screen-snapshot.png"),
49+writeScreenSnapshotToFile: vi.fn(async () => ({ path: "/tmp/screen-snapshot.png" })),
4150}));
42514352vi.mock("./gateway.js", () => ({
@@ -62,6 +71,9 @@ vi.mock("../../cli/nodes-screen.js", () => ({
6271parseScreenRecordPayload: screenMocks.parseScreenRecordPayload,
6372screenRecordTempPath: screenMocks.screenRecordTempPath,
6473writeScreenRecordToFile: screenMocks.writeScreenRecordToFile,
74+parseScreenSnapshotPayload: screenMocks.parseScreenSnapshotPayload,
75+screenSnapshotTempPath: screenMocks.screenSnapshotTempPath,
76+writeScreenSnapshotToFile: screenMocks.writeScreenSnapshotToFile,
6577}));
66786779let createNodesTool: typeof import("./nodes-tool.js").createNodesTool;
@@ -123,6 +135,9 @@ describe("createNodesTool screen_record duration guardrails", () => {
123135nodeUtilsMocks.resolveNode.mockClear();
124136screenMocks.parseScreenRecordPayload.mockClear();
125137screenMocks.writeScreenRecordToFile.mockClear();
138+screenMocks.parseScreenSnapshotPayload.mockClear();
139+screenMocks.screenSnapshotTempPath.mockClear();
140+screenMocks.writeScreenSnapshotToFile.mockClear();
126141nodesCameraMocks.cameraTempPath.mockClear();
127142nodesCameraMocks.parseCameraSnapPayload.mockClear();
128143nodesCameraMocks.writeCameraPayloadToFile.mockClear();
@@ -258,6 +273,69 @@ describe("createNodesTool screen_record duration guardrails", () => {
258273expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
259274});
260275276+it("invokes screen.snapshot with validated params and returns file details", async () => {
277+gatewayMocks.callGatewayTool.mockResolvedValue({ payload: { ok: true } });
278+const tool = createNodesTool();
279+280+const result = await tool.execute("call-snapshot", {
281+action: "screen_snapshot",
282+node: "macbook",
283+screenIndex: 1,
284+maxWidth: "1200",
285+});
286+287+expect(gatewayMocks.callGatewayTool).toHaveBeenCalledTimes(1);
288+const call = gatewayMocks.callGatewayTool.mock.calls[0] as
289+| [
290+string,
291+unknown,
292+{ command?: string; params?: { screenIndex?: unknown; maxWidth?: unknown } },
293+]
294+| undefined;
295+expect(call?.[0]).toBe("node.invoke");
296+expect(call?.[2].command).toBe("screen.snapshot");
297+expect(call?.[2].params).toEqual({ screenIndex: 1, maxWidth: 1200 });
298+expect(screenMocks.parseScreenSnapshotPayload).toHaveBeenCalledWith({ ok: true });
299+expect(screenMocks.screenSnapshotTempPath).toHaveBeenCalledWith({ ext: "png" });
300+expect(screenMocks.writeScreenSnapshotToFile).toHaveBeenCalledWith(
301+"/tmp/screen-snapshot.png",
302+"ZmFrZQ==",
303+);
304+expect(result).toEqual({
305+content: [{ type: "text", text: "FILE:/tmp/screen-snapshot.png" }],
306+details: {
307+path: "/tmp/screen-snapshot.png",
308+format: "png",
309+screenIndex: 0,
310+width: 1920,
311+height: 1080,
312+media: {
313+mediaUrl: "/tmp/screen-snapshot.png",
314+},
315+},
316+});
317+});
318+319+it("rejects unsupported screen.snapshot response formats before writing", async () => {
320+gatewayMocks.callGatewayTool.mockResolvedValue({ payload: { ok: true } });
321+screenMocks.parseScreenSnapshotPayload.mockReturnValueOnce({
322+base64: "ZmFrZQ==",
323+format: "webp",
324+screenIndex: 0,
325+width: 1920,
326+height: 1080,
327+});
328+const tool = createNodesTool();
329+330+await expect(
331+tool.execute("call-snapshot", {
332+action: "screen_snapshot",
333+node: "macbook",
334+}),
335+).rejects.toThrow("unsupported screen.snapshot format: webp");
336+expect(screenMocks.writeScreenSnapshotToFile).not.toHaveBeenCalled();
337+});
338+261339it("rejects the removed run action", async () => {
262340const tool = createNodesTool();
263341@@ -387,6 +465,8 @@ describe("createNodesTool screen_record duration guardrails", () => {
387465["photos_latest", { quality: -0.1 }, "quality must be between 0 and 1"],
388466["screen_record", { fps: 0 }, "fps must be greater than 0"],
389467["screen_record", { screenIndex: 1.5 }, "screenIndex must be a non-negative integer"],
468+["screen_snapshot", { maxWidth: 0 }, "maxWidth must be a positive integer"],
469+["screen_snapshot", { screenIndex: -1 }, "screenIndex must be a non-negative integer"],
390470])("rejects invalid %s numeric params %s", async (action, params, message) => {
391471const tool = createNodesTool();
392472@@ -561,6 +641,38 @@ describe("createNodesTool screen_record duration guardrails", () => {
561641);
562642});
563643644+it("blocks raw screen.snapshot invoke to prevent base64 context bloat", async () => {
645+const tool = createNodesTool();
646+647+await expect(
648+tool.execute("call-1", {
649+action: "invoke",
650+node: "macbook",
651+invokeCommand: "screen.snapshot",
652+}),
653+).rejects.toThrow('use action="screen_snapshot"');
654+expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
655+});
656+657+it("preserves explicitly enabled raw screen.snapshot invoke", async () => {
658+gatewayMocks.callGatewayTool.mockResolvedValue({
659+payload: { format: "png", base64: "ZmFrZQ==" },
660+});
661+const tool = createNodesTool({ allowMediaInvokeCommands: true });
662+663+await tool.execute("call-1", {
664+action: "invoke",
665+node: "macbook",
666+invokeCommand: "screen.snapshot",
667+});
668+669+expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
670+"node.invoke",
671+{},
672+expect.objectContaining({ command: "screen.snapshot" }),
673+);
674+});
675+564676it("keeps invoke pairing guidance for scope upgrade rejections", async () => {
565677gatewayMocks.callGatewayTool.mockRejectedValueOnce(
566678new Error("scope upgrade pending approval (requestId: req-123)"),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。