test: tighten camera tool assertions

This commit is contained in:
Peter Steinberger
2026-05-09 19:41:33 +01:00
parent fec1c3f696
commit 7db96894c2

View File

@@ -65,6 +65,32 @@ async function executeNodes(
type NodesToolResult = Awaited<ReturnType<typeof executeNodes>>;
type GatewayMockResult = Record<string, unknown> | null | undefined;
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`expected ${label}`);
}
return value as Record<string, unknown>;
}
function expectInvokeParams(
invokeParams: unknown,
expected: {
command: string;
nodeId?: string;
params?: Record<string, unknown>;
},
) {
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<Record<string, unknown>> } | 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 }],
});
});
});