fix(media-understanding): guard malformed attachments arrays

This commit is contained in:
Clawrence
2026-02-26 16:43:03 -06:00
committed by Peter Steinberger
parent f7c658efb9
commit 9c9ab891c2
4 changed files with 77 additions and 15 deletions

View File

@@ -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([]);
});
});

View File

@@ -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;

View File

@@ -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)");
});
});

View File

@@ -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})` : "";