From 9c9ab891c2236c8e3eafd5798da95adfcdf5853c Mon Sep 17 00:00:00 2001 From: Clawrence Date: Thu, 26 Feb 2026 16:43:03 -0600 Subject: [PATCH] fix(media-understanding): guard malformed attachments arrays --- .../attachments.guards.test.ts | 29 +++++++++++++++++++ src/media-understanding/attachments.ts | 18 +++++++----- .../runner.entries.guards.test.ts | 29 +++++++++++++++++++ src/media-understanding/runner.entries.ts | 16 +++++----- 4 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 src/media-understanding/attachments.guards.test.ts create mode 100644 src/media-understanding/runner.entries.guards.test.ts diff --git a/src/media-understanding/attachments.guards.test.ts b/src/media-understanding/attachments.guards.test.ts new file mode 100644 index 00000000000..68bdb379bc6 --- /dev/null +++ b/src/media-understanding/attachments.guards.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import type { MediaAttachment } from "./types.js"; +import { selectAttachments } from "./attachments.js"; + +describe("media-understanding selectAttachments guards", () => { + it("does not throw when attachments is undefined", () => { + const run = () => + selectAttachments({ + capability: "image", + attachments: undefined as unknown as MediaAttachment[], + policy: { prefer: "path" }, + }); + + expect(run).not.toThrow(); + expect(run()).toEqual([]); + }); + + it("does not throw when attachments is not an array", () => { + const run = () => + selectAttachments({ + capability: "audio", + attachments: { malformed: true } as unknown as MediaAttachment[], + policy: { prefer: "url" }, + }); + + expect(run).not.toThrow(); + expect(run()).toEqual([]); + }); +}); diff --git a/src/media-understanding/attachments.ts b/src/media-understanding/attachments.ts index ba09c96f28a..a42c3045c3e 100644 --- a/src/media-understanding/attachments.ts +++ b/src/media-understanding/attachments.ts @@ -169,23 +169,24 @@ function orderAttachments( attachments: MediaAttachment[], prefer?: MediaUnderstandingAttachmentsConfig["prefer"], ): MediaAttachment[] { + const list = Array.isArray(attachments) ? attachments : []; if (!prefer || prefer === "first") { - return attachments; + return list; } if (prefer === "last") { - return [...attachments].toReversed(); + return [...list].toReversed(); } if (prefer === "path") { - const withPath = attachments.filter((item) => item.path); - const withoutPath = attachments.filter((item) => !item.path); + const withPath = list.filter((item) => item.path); + const withoutPath = list.filter((item) => !item.path); return [...withPath, ...withoutPath]; } if (prefer === "url") { - const withUrl = attachments.filter((item) => item.url); - const withoutUrl = attachments.filter((item) => !item.url); + const withUrl = list.filter((item) => item.url); + const withoutUrl = list.filter((item) => !item.url); return [...withUrl, ...withoutUrl]; } - return attachments; + return list; } export function selectAttachments(params: { @@ -194,7 +195,8 @@ export function selectAttachments(params: { policy?: MediaUnderstandingAttachmentsConfig; }): MediaAttachment[] { const { capability, attachments, policy } = params; - const matches = attachments.filter((item) => { + const input = Array.isArray(attachments) ? attachments : []; + const matches = input.filter((item) => { // Skip already-transcribed audio attachments from preflight if (capability === "audio" && item.alreadyTranscribed) { return false; diff --git a/src/media-understanding/runner.entries.guards.test.ts b/src/media-understanding/runner.entries.guards.test.ts new file mode 100644 index 00000000000..d237038eef0 --- /dev/null +++ b/src/media-understanding/runner.entries.guards.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import type { MediaUnderstandingDecision } from "./types.js"; +import { formatDecisionSummary } from "./runner.entries.js"; + +describe("media-understanding formatDecisionSummary guards", () => { + it("does not throw when decision.attachments is undefined", () => { + const run = () => + formatDecisionSummary({ + capability: "image", + outcome: "skipped", + attachments: undefined as unknown as MediaUnderstandingDecision["attachments"], + }); + + expect(run).not.toThrow(); + expect(run()).toBe("image: skipped"); + }); + + it("does not throw when attachment attempts is malformed", () => { + const run = () => + formatDecisionSummary({ + capability: "video", + outcome: "skipped", + attachments: [{ attachmentIndex: 0, attempts: { bad: true } }], + } as unknown as MediaUnderstandingDecision); + + expect(run).not.toThrow(); + expect(run()).toBe("video: skipped (0/1)"); + }); +}); diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index 10d520402de..740310affcc 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -345,16 +345,18 @@ async function resolveProviderExecutionContext(params: { } export function formatDecisionSummary(decision: MediaUnderstandingDecision): string { - const total = decision.attachments.length; - const success = decision.attachments.filter( - (entry) => entry.chosen?.outcome === "success", - ).length; - const chosen = decision.attachments.find((entry) => entry.chosen)?.chosen; + const attachments = Array.isArray(decision.attachments) ? decision.attachments : []; + const total = attachments.length; + const success = attachments.filter((entry) => entry?.chosen?.outcome === "success").length; + const chosen = attachments.find((entry) => entry?.chosen)?.chosen; const provider = chosen?.provider?.trim(); const model = chosen?.model?.trim(); const modelLabel = provider ? (model ? `${provider}/${model}` : provider) : undefined; - const reason = decision.attachments - .flatMap((entry) => entry.attempts.map((attempt) => attempt.reason).filter(Boolean)) + const reason = attachments + .flatMap((entry) => { + const attempts = Array.isArray(entry?.attempts) ? entry.attempts : []; + return attempts.map((attempt) => attempt?.reason).filter(Boolean); + }) .find(Boolean); const shortReason = reason ? reason.split(":")[0]?.trim() : undefined; const countLabel = total > 0 ? ` (${success}/${total})` : "";