refactor(nodes): dedupe camera payload and node resolve helpers

This commit is contained in:
Peter Steinberger
2026-03-02 23:32:34 +00:00
parent a282b459b9
commit bb60687b89
9 changed files with 130 additions and 92 deletions

View File

@@ -1,5 +1,8 @@
import * as fs from "node:fs/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
readFileUtf8AndCleanup,
stubFetchTextResponse,
} from "../test-utils/camera-url-test-helpers.js";
const { callGateway } = vi.hoisted(() => ({
callGateway: vi.fn(),
@@ -206,10 +209,7 @@ describe("nodes camera_snap", () => {
});
it("downloads camera_snap url payloads when node remoteIp is available", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("url-image", { status: 200 })),
);
stubFetchTextResponse("url-image");
setupNodeInvokeMock({
remoteIp: "198.51.100.42",
invokePayload: {
@@ -230,18 +230,11 @@ describe("nodes camera_snap", () => {
const mediaPath = String((result.content?.[0] as { text?: string } | undefined)?.text ?? "")
.replace(/^MEDIA:/, "")
.trim();
try {
await expect(fs.readFile(mediaPath, "utf8")).resolves.toBe("url-image");
} finally {
await fs.unlink(mediaPath).catch(() => {});
}
await expect(readFileUtf8AndCleanup(mediaPath)).resolves.toBe("url-image");
});
it("rejects camera_snap url payloads when node remoteIp is missing", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("url-image", { status: 200 })),
);
stubFetchTextResponse("url-image");
setupNodeInvokeMock({
invokePayload: {
format: "jpg",
@@ -263,10 +256,7 @@ describe("nodes camera_snap", () => {
describe("nodes camera_clip", () => {
it("downloads camera_clip url payloads when node remoteIp is available", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("url-clip", { status: 200 })),
);
stubFetchTextResponse("url-clip");
setupNodeInvokeMock({
remoteIp: "198.51.100.42",
invokePayload: {
@@ -285,18 +275,11 @@ describe("nodes camera_clip", () => {
const filePath = String((result.content?.[0] as { text?: string } | undefined)?.text ?? "")
.replace(/^FILE:/, "")
.trim();
try {
await expect(fs.readFile(filePath, "utf8")).resolves.toBe("url-clip");
} finally {
await fs.unlink(filePath).catch(() => {});
}
await expect(readFileUtf8AndCleanup(filePath)).resolves.toBe("url-clip");
});
it("rejects camera_clip url payloads when node remoteIp is missing", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("url-clip", { status: 200 })),
);
stubFetchTextResponse("url-clip");
setupNodeInvokeMock({
invokePayload: {
format: "mp4",

View File

@@ -7,8 +7,7 @@ import {
parseCameraClipPayload,
parseCameraSnapPayload,
writeCameraClipPayloadToFile,
writeBase64ToFile,
writeUrlToFile,
writeCameraPayloadToFile,
} from "../../cli/nodes-camera.js";
import { parseEnvPairs, parseTimeoutMs } from "../../cli/nodes-run.js";
import {
@@ -295,16 +294,12 @@ export function createNodesTool(options?: {
facing,
ext: isJpeg ? "jpg" : "png",
});
if (payload.url) {
if (!resolvedNode.remoteIp) {
throw new Error("camera URL payload requires node remoteIp");
}
await writeUrlToFile(filePath, payload.url, {
expectedHost: resolvedNode.remoteIp,
});
} else if (payload.base64) {
await writeBase64ToFile(filePath, payload.base64);
}
await writeCameraPayloadToFile({
filePath,
payload,
expectedHost: resolvedNode.remoteIp,
invalidPayloadMessage: "invalid camera.snap payload",
});
content.push({ type: "text", text: `MEDIA:${filePath}` });
if (payload.base64) {
content.push({

View File

@@ -1,6 +1,6 @@
import { parseNodeList, parsePairingList } from "../../shared/node-list-parse.js";
import type { NodeListNode } from "../../shared/node-list-types.js";
import { resolveNodeIdFromCandidates } from "../../shared/node-match.js";
import { resolveNodeFromNodeList, resolveNodeIdFromNodeList } from "../../shared/node-resolve.js";
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
export type { NodeListNode };
@@ -142,17 +142,10 @@ export function resolveNodeIdFromList(
query?: string,
allowDefault = false,
): string {
const q = String(query ?? "").trim();
if (!q) {
if (allowDefault) {
const picked = pickDefaultNode(nodes);
if (picked) {
return picked.nodeId;
}
}
throw new Error("node required");
}
return resolveNodeIdFromCandidates(nodes, q);
return resolveNodeIdFromNodeList(nodes, query, {
allowDefault,
pickDefaultNode: pickDefaultNode,
});
}
export async function resolveNodeId(
@@ -169,6 +162,8 @@ export async function resolveNode(
allowDefault = false,
): Promise<NodeListNode> {
const nodes = await loadNodes(opts);
const nodeId = resolveNodeIdFromList(nodes, query, allowDefault);
return nodes.find((node) => node.nodeId === nodeId) ?? { nodeId };
return resolveNodeFromNodeList(nodes, query, {
allowDefault,
pickDefaultNode: pickDefaultNode,
});
}