From ae4dbc5c4845ff5a9aaeace44574004a03feb63f Mon Sep 17 00:00:00 2001 From: Mrxuba <56430542+Mrxuba@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:11:27 +0800 Subject: [PATCH] fix(media): guard SynchronizedMedia.frameDomain before reading frameRate verifySynchronizedMedia dereferenced item.frameDomain.frameRate (and .frameCount) right after confirming only that the top-level value is an object. When frameDomain is missing or not an object it threw a bare "Cannot read properties of undefined (reading 'frameRate')" TypeError instead of a stated validation error. That is what surfaces when an older or hand-edited result.json is read (see #388). Add the same object() guard the sibling verifiers already use so a malformed frame domain is reported as "SynchronizedMedia.frameDomain must be an object". Adds packages/media/test/identity.test.ts covering a well-formed value and missing / non-object frame domains. --- packages/media/src/identity.ts | 1 + packages/media/test/identity.test.ts | 36 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 packages/media/test/identity.test.ts diff --git a/packages/media/src/identity.ts b/packages/media/src/identity.ts index c7eb4accf..860b1ce97 100644 --- a/packages/media/src/identity.ts +++ b/packages/media/src/identity.ts @@ -169,6 +169,7 @@ export function synchronizedMediaSampleFrames(value: SynchronizedMedia): number export function verifySynchronizedMedia(value: unknown): asserts value is SynchronizedMedia { const item = object(value, "SynchronizedMedia") as unknown as SynchronizedMedia; + object(item.frameDomain, "SynchronizedMedia.frameDomain"); verifyRational(item.frameDomain.frameRate, "SynchronizedMedia.frameDomain.frameRate"); positiveInteger(item.frameDomain.frameCount, "SynchronizedMedia.frameDomain.frameCount"); synchronizedMediaSampleFrames(item); diff --git a/packages/media/test/identity.test.ts b/packages/media/test/identity.test.ts new file mode 100644 index 000000000..814136a2f --- /dev/null +++ b/packages/media/test/identity.test.ts @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { verifySynchronizedMedia } from "../src/index.js"; +import type { SynchronizedMedia } from "../src/index.js"; +import { fixtureResource } from "../../../test/fixture-resource.js"; + +const media: SynchronizedMedia = { + frameDomain: { frameRate: { numerator: 30, denominator: 1 }, frameCount: 60 }, + visual: { + artifact: { kind: "blob", resource: fixtureResource("identity:visual"), size: 1, mediaType: "video/mp4" }, + width: 1920, height: 1080, + }, + audio: { artifact: { kind: "blob", resource: fixtureResource("identity:audio"), size: 1, mediaType: "audio/wav" } }, +}; + +test("verifySynchronizedMedia accepts a well-formed value", () => { + assert.doesNotThrow(() => verifySynchronizedMedia(media)); +}); + +test("verifySynchronizedMedia rejects a missing frame domain with a stated error", () => { + const { frameDomain: _omitted, ...withoutFrameDomain } = media; + assert.throws( + () => verifySynchronizedMedia(withoutFrameDomain), + /SynchronizedMedia\.frameDomain must be an object/, + ); +}); + +test("verifySynchronizedMedia rejects a non-object frame domain with a stated error", () => { + for (const frameDomain of [null, "30/1", 30] as const) { + assert.throws( + () => verifySynchronizedMedia({ ...media, frameDomain }), + /SynchronizedMedia\.frameDomain must be an object/, + ); + } +});