diff --git a/src/agents/openclaw-tools.camera.test.ts b/src/agents/openclaw-tools.camera.test.ts index 4cb6665a3e0..cfa89337a14 100644 --- a/src/agents/openclaw-tools.camera.test.ts +++ b/src/agents/openclaw-tools.camera.test.ts @@ -65,6 +65,32 @@ async function executeNodes( type NodesToolResult = Awaited>; type GatewayMockResult = Record | null | undefined; +function requireRecord(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`expected ${label}`); + } + return value as Record; +} + +function expectInvokeParams( + invokeParams: unknown, + expected: { + command: string; + nodeId?: string; + params?: Record; + }, +) { + const record = requireRecord(invokeParams, "node.invoke params"); + expect(record.command).toBe(expected.command); + if (expected.nodeId !== undefined) { + expect(record.nodeId).toBe(expected.nodeId); + } + const params = requireRecord(record.params, `${expected.command} params`); + for (const [key, value] of Object.entries(expected.params ?? {})) { + expect(params[key]).toEqual(value); + } +} + function mockNodeList(params?: { commands?: string[]; remoteIp?: string }) { return { nodes: [ @@ -98,15 +124,15 @@ function expectFirstMediaUrl(result: NodesToolResult): string { } function expectFirstTextContains(result: NodesToolResult, expectedText: string) { - expect(result.content?.[0]).toMatchObject({ - type: "text", - text: expect.stringContaining(expectedText), - }); + const first = result.content?.[0]; + expect(first?.type).toBe("text"); + const text = first?.type === "text" ? first.text : ""; + expect(text).toContain(expectedText); } function parseFirstTextJson(result: NodesToolResult): unknown { const first = result.content?.[0]; - expect(first).toMatchObject({ type: "text" }); + expect(first?.type).toBe("text"); const text = first?.type === "text" ? first.text : ""; return JSON.parse(text); } @@ -138,7 +164,7 @@ function setupPhotosLatestMock(params?: { remoteIp?: string }) { setupNodeInvokeMock({ ...(params?.remoteIp ? { remoteIp: params.remoteIp } : {}), onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { command: "photos.latest", params: PHOTOS_LATEST_DEFAULT_PARAMS, }); @@ -162,7 +188,7 @@ describe("nodes camera_snap", () => { it("uses front/high-quality defaults when params are omitted", async () => { setupNodeInvokeMock({ onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { command: "camera.snap", params: { facing: "front", @@ -224,7 +250,7 @@ describe("nodes camera_snap", () => { it("passes deviceId when provided", async () => { setupNodeInvokeMock({ onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { command: "camera.snap", params: { deviceId: "cam-123" }, }); @@ -343,7 +369,7 @@ describe("nodes photos_latest", () => { it("returns empty content/details when no photos are available", async () => { setupNodeInvokeMock({ onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { command: "photos.latest", params: { limit: 1, @@ -380,11 +406,9 @@ describe("nodes photos_latest", () => { expect(result.content ?? []).toStrictEqual([]); const details = (result.details as { photos?: Array> } | undefined)?.photos ?? []; - expect(details[0]).toMatchObject({ - width: 1, - height: 1, - createdAt: "2026-03-04T00:00:00Z", - }); + expect(details[0]?.width).toBe(1); + expect(details[0]?.height).toBe(1); + expect(details[0]?.createdAt).toBe("2026-03-04T00:00:00Z"); expect(expectFirstMediaUrl(result)).toMatch(/openclaw-camera-snap-.*\.jpg$/); }); @@ -403,7 +427,7 @@ describe("nodes notifications_list", () => { setupNodeInvokeMock({ commands: ["notifications.list"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "notifications.list", params: {}, @@ -425,7 +449,7 @@ describe("nodes notifications_list", () => { }); expectFirstTextContains(result, '"notifications"'); - expect(parseFirstTextJson(result)).toMatchObject({ + expect(parseFirstTextJson(result)).toStrictEqual({ enabled: true, connected: true, count: 1, @@ -439,7 +463,7 @@ describe("nodes notifications_action", () => { setupNodeInvokeMock({ commands: ["notifications.actions"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "notifications.actions", params: { @@ -459,7 +483,7 @@ describe("nodes notifications_action", () => { }); expectFirstTextContains(result, '"dismiss"'); - expect(parseFirstTextJson(result)).toMatchObject({ + expect(parseFirstTextJson(result)).toStrictEqual({ ok: true, key: "n1", action: "dismiss", @@ -470,7 +494,7 @@ describe("nodes notifications_action", () => { setupNodeInvokeMock({ commands: ["notifications.actions"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "notifications.actions", params: { @@ -491,7 +515,7 @@ describe("nodes notifications_action", () => { notificationReplyText: " On it ", }); - expect(parseFirstTextJson(result)).toMatchObject({ + expect(parseFirstTextJson(result)).toStrictEqual({ ok: true, key: "n2", action: "reply", @@ -504,7 +528,7 @@ describe("nodes location_get", () => { setupNodeInvokeMock({ commands: ["location.get"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "location.get", params: { @@ -532,7 +556,7 @@ describe("nodes location_get", () => { locationTimeoutMs: 4_500, }); - expect(parseFirstTextJson(result)).toMatchObject({ + expect(parseFirstTextJson(result)).toStrictEqual({ latitude: 37.3346, longitude: -122.009, accuracyMeters: 18, @@ -546,7 +570,7 @@ describe("nodes device_status and device_info", () => { setupNodeInvokeMock({ commands: ["device.status", "device.info"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "device.status", params: {}, @@ -571,7 +595,7 @@ describe("nodes device_status and device_info", () => { setupNodeInvokeMock({ commands: ["device.status", "device.info"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "device.info", params: {}, @@ -597,7 +621,7 @@ describe("nodes device_status and device_info", () => { setupNodeInvokeMock({ commands: ["device.permissions"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "device.permissions", params: {}, @@ -626,25 +650,21 @@ describe("nodes device_status and device_info", () => { }); expectFirstTextContains(result, '"permissions"'); - expect(parseFirstTextJson(result)).toMatchObject({ - permissions: { - sms: { - status: "denied", - promptable: true, - capabilities: { - send: { status: "denied", promptable: true }, - read: { status: "granted", promptable: false }, - }, - }, - }, - }); + const parsed = requireRecord(parseFirstTextJson(result), "device permissions payload"); + const permissions = requireRecord(parsed.permissions, "permissions"); + const sms = requireRecord(permissions.sms, "sms permissions"); + expect(sms.status).toBe("denied"); + expect(sms.promptable).toBe(true); + const capabilities = requireRecord(sms.capabilities, "sms capabilities"); + expect(capabilities.send).toStrictEqual({ status: "denied", promptable: true }); + expect(capabilities.read).toStrictEqual({ status: "granted", promptable: false }); }); it("invokes device.health and returns payload", async () => { setupNodeInvokeMock({ commands: ["device.health"], onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { nodeId: NODE_ID, command: "device.health", params: {}, @@ -671,7 +691,7 @@ describe("nodes invoke", () => { it("allows metadata-only camera.list via generic invoke", async () => { setupNodeInvokeMock({ onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { command: "camera.list", params: {}, }); @@ -689,10 +709,9 @@ describe("nodes invoke", () => { invokeCommand: "camera.list", }); - expect(result.details).toMatchObject({ - payload: { - devices: [{ id: "cam-back", name: "Back Camera" }], - }, + const details = requireRecord(result.details, "camera.list details"); + expect(details.payload).toStrictEqual({ + devices: [{ id: "cam-back", name: "Back Camera" }], }); }); @@ -710,7 +729,7 @@ describe("nodes invoke", () => { it("allows media invoke commands when explicitly enabled", async () => { setupNodeInvokeMock({ onInvoke: (invokeParams) => { - expect(invokeParams).toMatchObject({ + expectInvokeParams(invokeParams, { command: "photos.latest", params: { limit: 1 }, }); @@ -732,10 +751,9 @@ describe("nodes invoke", () => { { allowMediaInvokeCommands: true }, ); - expect(result.details).toMatchObject({ - payload: { - photos: [{ format: "jpg", base64: "aGVsbG8=", width: 1, height: 1 }], - }, + const details = requireRecord(result.details, "photos.latest invoke details"); + expect(details.payload).toStrictEqual({ + photos: [{ format: "jpg", base64: "aGVsbG8=", width: 1, height: 1 }], }); }); });