Same bytes now give the server's record in five shapes where the local reader was stricter or kept different tags (PR #277 final review I1, I2, M1, M2, M4): - Invalid UTF-8 in a tag value is replaced as ffprobe's default string_validation=replace does (av_utf8_decode, one U+FFFD per failing decode step), instead of failing the read. Key names and TagNames that do not decode are no carrier and are skipped. - An ilst item whose keys index is 0 or out of range is skipped, as ffmpeg does, instead of failing the read. - Only the first keys box is read, as ffmpeg does. - A first per-key workflow over 4 MiB keeps its place in the carrier choice as an unread marker, so the server's carrier (per-key, workflow limited) is chosen; when the server's first probe would overflow, the re-probe leaves it out and its own bound applies. - An envelope integer of more than 4300 digits fails the Python json port, so that envelope is skipped as CPython's json.loads skips it. Each shape was checked against ffprobe 9.0.2 and read_inspection_file; the real-ffprobe parity test now builds them, and a 600-case random-byte comparison of the replacement against ffprobe matched.
590 lines
45 KiB
JavaScript
590 lines
45 KiB
JavaScript
// Run with: node --test tests/js/
|
|
// The browser video tag readers mirror what the server reads through ffprobe
|
|
// (utils/prompt_inspector.py _video_metadata) and the carrier rule of
|
|
// utils/video_metadata.py select_video_carrier. Each rule names its server twin;
|
|
// tests/test_local_inspect_parity.py proves the two agree on the committed fixtures
|
|
// and on recorded tag maps.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import * as v from "./helpers/video-bytes.mjs";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const FIXTURES = path.join(here, "..", "fixtures", "local-inspect");
|
|
const RECORDED = JSON.parse(fs.readFileSync(path.join(FIXTURES, "video-tags-v1.json"), "utf8")).fixtures;
|
|
const m = await import("../../web/js/workspace/local-video.mjs").catch((error) => {
|
|
if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
return {};
|
|
});
|
|
const { readVideoTags, selectVideoCarrier, pythonJsonDumps, pythonJsonLoads } = m;
|
|
|
|
const MiB = 1024 * 1024;
|
|
const GiB = 1024 * MiB;
|
|
const ROLE_BUDGET = 2 * MiB;
|
|
const WORKFLOW_BUDGET = 4 * MiB;
|
|
const TAG_READ_BYTES = 12 * MiB + 64 * 1024;
|
|
const PER_KEY = new Set(["prompt", "workflow", "parameters", "comfyassets_prompt_pairs"]);
|
|
const GRAPH = '{"3":{"class_type":"KSampler","inputs":{"seed":7}}}';
|
|
const WORKFLOW = '{"nodes":[],"version":0.4}';
|
|
|
|
function knownTags(map) {
|
|
return Object.fromEntries(Object.entries(map).filter(([name]) => PER_KEY.has(name.toLowerCase()) || name === "comment" || name === "description"));
|
|
}
|
|
|
|
function fixture(name) {
|
|
return new Uint8Array(fs.readFileSync(path.join(FIXTURES, name)));
|
|
}
|
|
|
|
async function read(bytes, options = {}) {
|
|
return readVideoTags(v.blobOf(bytes), options);
|
|
}
|
|
|
|
function fill(byte, count) {
|
|
return Buffer.alloc(count, byte);
|
|
}
|
|
|
|
function complete(tags, extra = {}) {
|
|
return { status: "complete", tags, limited: [], ...extra };
|
|
}
|
|
|
|
test("contract: the readers, the carrier rule and the json port are exported", () => {
|
|
assert.equal(typeof readVideoTags, "function");
|
|
assert.equal(typeof selectVideoCarrier, "function");
|
|
assert.equal(typeof pythonJsonDumps, "function");
|
|
assert.equal(typeof pythonJsonLoads, "function");
|
|
});
|
|
|
|
test("iso-bmff reader finds per-key tags with moov after mdat", async () => {
|
|
for (const [name, recorded] of Object.entries(RECORDED)) {
|
|
const blob = v.recordingBlob(fixture(name));
|
|
const result = await readVideoTags(blob, { prefix: fixture(name).subarray(0, 32) });
|
|
assert.equal(result.status, "complete", name);
|
|
assert.deepEqual(result.tags, knownTags(recorded), name);
|
|
assert.equal(result.container, name.endsWith(".mkv") || name.endsWith(".webm") ? "matroska" : "iso-bmff", name);
|
|
assert.ok(["isom", "qt", "M4V", "matroska", "webm"].includes(result.format), `${name}: ${result.format}`);
|
|
assert.deepEqual(result.limited, [], name);
|
|
assert.ok(blob.ranges.length > 0, name);
|
|
}
|
|
// core-moov-end.mp4 behind a 3 GiB mdat: only the box headers and the tag bytes are sliced.
|
|
const bytes = fixture("core-moov-end.mp4");
|
|
const boxes = v.topLevelBoxes(bytes);
|
|
const mdat = boxes.find((box) => box.type === "mdat");
|
|
const moov = boxes.find((box) => box.type === "moov");
|
|
const mdatSize = 3 * GiB + 16;
|
|
const header = v.concat(v.u32(1), v.latin1("mdat"), new Uint8Array(8));
|
|
new DataView(header.buffer).setBigUint64(8, BigInt(mdatSize));
|
|
const moovOffset = mdat.start + mdatSize;
|
|
const blob = v.sparseBlob(
|
|
[
|
|
{ offset: 0, bytes: bytes.subarray(0, mdat.start) },
|
|
{ offset: mdat.start, bytes: header },
|
|
{ offset: moovOffset, bytes: bytes.subarray(moov.start, moov.end) },
|
|
],
|
|
moovOffset + (moov.end - moov.start),
|
|
);
|
|
const result = await readVideoTags(blob, { prefix: bytes.subarray(0, 32) });
|
|
assert.deepEqual(result.tags, knownTags(RECORDED["core-moov-end.mp4"]));
|
|
const inside = blob.ranges.filter(([start]) => start >= mdat.start && start < moovOffset);
|
|
assert.ok(inside.every(([start, end]) => start === mdat.start && end - start <= 16), JSON.stringify(inside));
|
|
const total = blob.ranges.reduce((sum, [start, end]) => sum + (end - start), 0);
|
|
assert.ok(total <= m.VIDEO_READ_CEILING && total < 64 * 1024, `${total}`);
|
|
});
|
|
|
|
test("iso-bmff: legacy ilst atoms, moov/meta, sticky mdta handler and data types", async () => {
|
|
// ©cmt and desc map to comment and description, as ffprobe reports them.
|
|
const legacy = await read(v.mp4(v.legacyUdta({ comment: '{"prompt":"{}"}', description: "note" })));
|
|
assert.deepEqual(legacy, complete({ comment: '{"prompt":"{}"}', description: "note" }, { container: "iso-bmff", format: "isom" }));
|
|
// moov/meta (ISO style) is read like moov/udta/meta.
|
|
const isoMeta = await read(v.mp4(v.box("meta", v.u32(0), v.hdlr("mdta"), v.keys(["prompt"]), v.ilst(v.item(1, v.data(GRAPH))))));
|
|
assert.deepEqual(isoMeta.tags, { prompt: GRAPH });
|
|
// meta without the version/flags word (QuickTime) still finds hdlr.
|
|
const qtMeta = await read(v.mp4(v.udta(v.box("meta", v.hdlr("mdta"), v.keys(["prompt"]), v.ilst(v.item(1, v.data(GRAPH)))))));
|
|
assert.deepEqual(qtMeta.tags, { prompt: GRAPH });
|
|
// Index items need an mdta handler and a keys box; with a legacy handler they are skipped.
|
|
const noKeys = await read(v.mp4(v.udta(v.meta(v.hdlr("mdir"), v.ilst(v.item(1, v.data(GRAPH)))))));
|
|
assert.deepEqual(noKeys.tags, {});
|
|
// The mdta handler is sticky across the file, as ffmpeg's found_hdlr_mdta.
|
|
const sticky = await read(v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"])), v.meta(v.hdlr("mdir"), v.ilst(v.item(1, v.data(GRAPH)))))));
|
|
assert.deepEqual(sticky.tags, { prompt: GRAPH });
|
|
// As ffprobe reports them: consecutive data children join with ";", integer data
|
|
// types print as decimals, other types are not tags, text types 0/1/4 are read, NUL
|
|
// truncates, and the run of data children stops at the first other child.
|
|
const mixed = await read(v.mp4(v.udta(v.meta(
|
|
v.hdlr("mdta"),
|
|
v.keys(["prompt", "workflow", "parameters", "comfyassets_prompt_pairs", "comment", "description"]),
|
|
v.ilst(
|
|
v.item(1, v.data("first"), v.data("second"), v.box("name", "x"), v.data("not read")),
|
|
v.item(2, v.data([0, 0, 0, 7], { type: 21 }), v.data([0xff, 0xfe], { type: 21 }), v.data([0xff, 0xfe], { type: 22 })),
|
|
v.item(3, v.data("cut\0off")),
|
|
v.item(4, v.data("sort", { type: 4 }), v.data("mac", { type: 0 })),
|
|
v.item(5, v.data([0, 0, 0, 0, 0, 0, 0], { type: 21 })),
|
|
v.item(6, v.data("float", { type: 23 })),
|
|
),
|
|
))));
|
|
assert.deepEqual(mixed.tags, { prompt: "first;second", workflow: "7;-2;65534", parameters: "cut", comfyassets_prompt_pairs: "sort;mac" });
|
|
// Keys outside the mdta namespace address nothing; unknown names are skipped unread.
|
|
const blob = v.recordingBlob(v.mp4(v.udta(v.meta(
|
|
v.hdlr("mdta"),
|
|
v.keys([["prompt", "udta"], "encoder", "workflow"]),
|
|
v.ilst(v.item(1, v.data(GRAPH)), v.item(2, v.data(fill(0x41, 5000))), v.item(3, v.data(WORKFLOW))),
|
|
))));
|
|
const result = await readVideoTags(blob);
|
|
assert.deepEqual(result.tags, { workflow: WORKFLOW });
|
|
assert.ok(blob.ranges.every(([start, end]) => end - start < 5000), "the encoder value was never sliced");
|
|
// Legacy fourccs are mapped even in mdta mode, and a repeated name joins with ";".
|
|
const repeated = await read(v.mp4(v.udta(v.meta(
|
|
v.hdlr("mdta"),
|
|
v.keys(["prompt", "prompt", "comment"]),
|
|
v.ilst(v.item(1, v.data("one")), v.item(2, v.data("two")), v.item(3, v.data("mdta")), v.legacyItem("©cmt", v.data("c"))),
|
|
))));
|
|
assert.deepEqual(repeated.tags, { prompt: "one;two", comment: "mdta;c" });
|
|
// Apple's namespaced keys are reported verbatim, so they are never an envelope.
|
|
const apple = await read(v.perKeyMp4({ "com.apple.quicktime.description": '{"prompt":"{}"}', prompt: GRAPH }));
|
|
assert.deepEqual(apple.tags, { prompt: GRAPH });
|
|
// An item whose first child is not a data atom, or a data atom shorter than its
|
|
// 16-byte header, is skipped as ffmpeg skips it.
|
|
const junk = await read(v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"]), v.ilst(v.item(1, v.box("name", "x"), v.data(GRAPH)))))));
|
|
assert.deepEqual(junk.tags, {});
|
|
const short = await read(v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"]), v.ilst(v.item(1, v.data(GRAPH, { size: 12 })))))));
|
|
assert.deepEqual(short.tags, {});
|
|
// A box declared with size 0 runs to the end of its parent; faststart puts moov first.
|
|
const fast = await read(v.perKeyMp4({ prompt: GRAPH }, { faststart: true, brand: "mp42" }));
|
|
assert.deepEqual(fast, complete({ prompt: GRAPH }, { container: "iso-bmff", format: "mp42" }));
|
|
const toEnd = await read(v.concat(v.ftyp(), v.mdat(), v.moov(v.mdtaUdta({ prompt: GRAPH }), v.opts({ size: 0 }))));
|
|
assert.deepEqual(toEnd.tags, { prompt: GRAPH });
|
|
});
|
|
|
|
test("matroska reads global Tags and skips clusters", async () => {
|
|
const { ID } = v;
|
|
// ffmpeg layout: SeekHead, Info, Tracks, Tags, Cluster, Cues; names upper-cased.
|
|
const plain = await read(v.perKeyMkv({ PROMPT: GRAPH, WORKFLOW, ENCODER: "Lavf" }, { docType: "webm" }));
|
|
assert.deepEqual(plain, complete({ PROMPT: GRAPH, WORKFLOW }, { container: "matroska", format: "webm" }));
|
|
// Tags after a 3 GiB Cluster, located through the SeekHead: no slice inside the cluster.
|
|
const tags = v.globalTags({ PROMPT: GRAPH });
|
|
const clusterSize = 3 * GiB;
|
|
// The Tags position (relative to the Segment payload) is a 4-byte uint either way,
|
|
// so the SeekHead's length can be measured with a placeholder first.
|
|
const seekLength = v.seekHead([[ID.Tags, clusterSize]]).length;
|
|
const seekHead = v.seekHead([[ID.Tags, seekLength + 12 + clusterSize]]);
|
|
assert.equal(seekHead.length, seekLength);
|
|
const head = v.concat(v.ebmlHeader(), v.idBytes(ID.Segment), v.vint(seekHead.length + 12 + clusterSize + tags.length, { width: 8 }), seekHead, v.clusterHeader(clusterSize));
|
|
const clusterStart = v.ebmlHeader().length + 12 + seekHead.length;
|
|
const tagsOffset = head.length + clusterSize;
|
|
const blob = v.sparseBlob([{ offset: 0, bytes: head }, { offset: tagsOffset, bytes: tags }], tagsOffset + tags.length);
|
|
const seeked = await readVideoTags(blob, { prefix: head.subarray(0, 32) });
|
|
assert.deepEqual(seeked.tags, { PROMPT: GRAPH });
|
|
const inside = blob.ranges.filter(([start]) => start >= clusterStart && start < tagsOffset);
|
|
assert.ok(inside.every(([start, end]) => start === clusterStart && end - start <= 16), JSON.stringify(inside));
|
|
assert.ok(blob.ranges.reduce((sum, [start, end]) => sum + (end - start), 0) <= m.VIDEO_READ_CEILING);
|
|
// ffprobe reads level-1 elements up to the first Cluster: Tags behind it are found
|
|
// only through a SeekHead, so without one they are not read (nor sliced).
|
|
const behind = v.recordingBlob(v.mkv([v.cluster(64), v.globalTags({ PROMPT: GRAPH }), el(ID.Cues, new Uint8Array(2))]));
|
|
const sequential = await readVideoTags(behind);
|
|
assert.deepEqual(sequential.tags, {});
|
|
assert.ok(behind.ranges.every(([, end]) => end <= v.ebmlHeader().length + 12 + 12 + 32), JSON.stringify(behind.ranges));
|
|
// Track-targeted Tags are ignored; chapter and attachment targets too.
|
|
const targeted = await read(v.mkv([
|
|
v.tagsEl(
|
|
v.tag([v.simpleTag("PROMPT", "track")], { trackUid: 1 }),
|
|
v.tag([v.simpleTag("WORKFLOW", WORKFLOW)], { targetType: 50 }),
|
|
v.tag([v.simpleTag("PROMPT", "zero uid is global")], { trackUid: 0 }),
|
|
),
|
|
v.cluster(),
|
|
]));
|
|
assert.deepEqual(targeted.tags, { WORKFLOW, PROMPT: "zero uid is global" });
|
|
// An unknown-size Cluster (live recordings) is the same stop; a SeekHead still works.
|
|
const hidden = await read(v.mkv([v.cluster(8, { unknown: true }), v.globalTags({ PROMPT: GRAPH })]));
|
|
assert.deepEqual(hidden, complete({}, { container: "matroska", format: "matroska" }));
|
|
const before = await read(v.mkv([v.globalTags({ PROMPT: GRAPH }), v.cluster(8, { unknown: true })]));
|
|
assert.deepEqual(before.tags, { PROMPT: GRAPH });
|
|
const info = el(ID.Info, new Uint8Array(4));
|
|
const clusterUnknown = v.cluster(8, { unknown: true });
|
|
const seekToTags = v.seekHead([[ID.Tags, 0x7f]]); // a one-byte position, like the real one
|
|
const viaSeek = await read(v.mkv([v.seekHead([[ID.Tags, seekToTags.length + info.length + clusterUnknown.length]]), info, clusterUnknown, v.globalTags({ PROMPT: GRAPH })]));
|
|
assert.deepEqual(viaSeek.tags, { PROMPT: GRAPH });
|
|
// An unknown-size Segment is fine (live recordings); other unknown sizes are invalid.
|
|
const liveSegment = await read(v.mkv([v.globalTags({ PROMPT: GRAPH }), v.cluster()], { unknown: true }));
|
|
assert.deepEqual(liveSegment.tags, { PROMPT: GRAPH });
|
|
const unknownTags = await read(v.mkv([el(ID.Tags, v.tag([v.simpleTag("PROMPT", GRAPH)]), v.opts({ unknown: true }))]));
|
|
assert.deepEqual(unknownTags, { status: "unreadable", reason: "invalid-container" });
|
|
// ffmpeg's tag naming (checked against ffprobe 9.0.2): a language other than und
|
|
// files the tag under NAME-lang only, unless TagDefault is set; nested SimpleTags
|
|
// become PARENT/CHILD (never ours); a string-less tag deletes the key; a repeated
|
|
// name keeps the last value; NUL ends the string; Void and CRC-32 are skipped.
|
|
const naming = await read(v.mkv([
|
|
v.voidEl(8),
|
|
v.tagsEl(
|
|
el(ID.CRC32, new Uint8Array(4)),
|
|
v.tag([
|
|
v.simpleTag("PROMPT", "eng only", { lang: "eng" }),
|
|
v.simpleTag("WORKFLOW", "eng only", { lang: "eng", def: 0 }),
|
|
v.simpleTag("workflow", "default and eng", { lang: "eng", def: 1 }),
|
|
v.simpleTag("PARAMETERS", "und", { lang: "und", nested: [v.simpleTag("prompt", "nested")] }),
|
|
v.simpleTag("comfyassets_prompt_pairs", "cut\0here"),
|
|
v.simpleTag("description", undefined, { binary: [1, 2, 3] }),
|
|
v.simpleTag("comment", "note"),
|
|
v.simpleTag("comment", undefined),
|
|
v.simpleTag("prompt", "first"),
|
|
v.simpleTag("prompt", "last", { binary: [1] }),
|
|
]),
|
|
),
|
|
v.cluster(),
|
|
]));
|
|
assert.deepEqual(naming.tags, { workflow: "default and eng", PARAMETERS: "und", comfyassets_prompt_pairs: "cut", prompt: "last" });
|
|
// Long names are never ours and are skipped without reading their strings.
|
|
const longName = v.recordingBlob(v.mkv([v.tagsEl(v.tag([v.simpleTag("N".repeat(300), fill(0x41, 4000)), v.simpleTag("PROMPT", GRAPH)])), v.cluster()]));
|
|
const skipped = await readVideoTags(longName);
|
|
assert.deepEqual(skipped.tags, { PROMPT: GRAPH });
|
|
assert.ok(longName.ranges.every(([start, end]) => end - start < 300));
|
|
function el(id, ...parts) {
|
|
return v.el(id, ...parts);
|
|
}
|
|
});
|
|
|
|
test("containers fail closed", async () => {
|
|
const { ID } = v;
|
|
const invalid = { status: "unreadable", reason: "invalid-container" };
|
|
const rows = [
|
|
["box size below its header", v.concat(v.ftyp(), v.box("free", v.opts({ size: 4 })), v.mdat(), v.moov(v.mdtaUdta({ prompt: GRAPH })))],
|
|
["child box past its parent", v.concat(v.ftyp(), v.mdat(), v.moov(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"]), v.ilst(v.item(1, v.data(GRAPH), v.opts({ size: 5000 })))))))],
|
|
["top-level box past the end of the file", v.concat(v.ftyp(), v.mdat(), v.box("moov", v.opts({ size: 4000 })))],
|
|
["64-bit size above MAX_SAFE_INTEGER", v.concat(v.ftyp(), v.box("mdat", v.opts({ largesize: 2 ** 60 })), v.moov(v.mdtaUdta({ prompt: GRAPH })))],
|
|
["64-bit size below its header", v.concat(v.ftyp(), v.box("mdat", v.opts({ largesize: 12 })), v.moov(v.mdtaUdta({ prompt: GRAPH })))],
|
|
["keys count larger than the entries", v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"], { count: 3 }), v.ilst(v.item(1, v.data(GRAPH))))))],
|
|
["keys entry overrunning the box", v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.box("keys", v.u32(0), v.u32(1), v.u32(90), v.latin1("mdta"), v.utf8("prompt")), v.ilst(v.item(1, v.data(GRAPH))))))],
|
|
["no moov box", v.concat(v.ftyp(), v.mdat(), v.free(32))],
|
|
["not a container at all", v.utf8("#EXTM3U\n#EXT-X-VERSION:3\n".padEnd(64, "x"))],
|
|
["bad VINT: zero ID byte", v.concat(v.ebmlHeader(), v.idBytes(ID.Segment), v.vint(20, { width: 8 }), [0x00, 0x81, 0x00], new Uint8Array(17))],
|
|
["bad VINT: zero size byte", v.concat(v.ebmlHeader(), v.idBytes(ID.Segment), v.vint(20, { width: 8 }), v.idBytes(ID.Info), [0x00], new Uint8Array(17))],
|
|
["element past its parent", v.mkv([v.el(ID.Tags, v.tag([v.simpleTag("PROMPT", GRAPH)]), v.opts({ size: 5000 }))])],
|
|
["element past the end of the file", v.concat(v.ebmlHeader(), v.idBytes(ID.Segment), v.vint(5000, { width: 8 }), v.globalTags({ PROMPT: GRAPH }))],
|
|
["unknown DocType", v.perKeyMkv({ PROMPT: GRAPH }, { docType: "avi" })],
|
|
["EBML header without a DocType", v.concat(v.el(ID.EBML, v.el(ID.EBMLVersion, [1])), v.segment([v.globalTags({ PROMPT: GRAPH })]))],
|
|
["Matroska bytes after a stray size", v.concat(v.ebmlHeader(), v.idBytes(ID.Segment), v.vint(3, { width: 8 }), [0xec, 0x82, 0x00])],
|
|
];
|
|
for (const [name, bytes] of rows) {
|
|
assert.deepEqual(await read(bytes), invalid, name);
|
|
}
|
|
// Trailing bytes shorter than a box header are ignored, as ffmpeg does.
|
|
assert.deepEqual((await read(v.concat(v.perKeyMp4({ prompt: GRAPH }), [1, 2, 3]))).tags, { prompt: GRAPH });
|
|
// 10,001 boxes stop the read.
|
|
const many = v.concat(v.ftyp(), ...Array.from({ length: 10001 }, () => v.free()), v.mdat(), v.moov(v.mdtaUdta({ prompt: GRAPH })));
|
|
assert.deepEqual(await read(many), { status: "limited", reason: "chunk-limit" });
|
|
const manyElements = v.mkv([...Array.from({ length: 10001 }, () => v.voidEl(1)), v.globalTags({ PROMPT: GRAPH })]);
|
|
assert.deepEqual(await read(manyElements), { status: "limited", reason: "chunk-limit" });
|
|
// Exactly 10,000 elements still read (ftyp, mdat, moov, udta, meta, hdlr, keys, its entry, ilst, the item and its data atom).
|
|
const exact = v.concat(v.ftyp(), ...Array.from({ length: 10000 - 11 }, () => v.free()), v.mdat(), v.moov(v.mdtaUdta({ prompt: GRAPH })));
|
|
assert.deepEqual((await read(exact)).tags, { prompt: GRAPH });
|
|
// A vanished file is a read failure, not a container error.
|
|
assert.deepEqual(await readVideoTags(v.failingBlob(4096), { prefix: v.ftyp() }), { status: "unreadable", reason: "read-failure" });
|
|
assert.deepEqual(await readVideoTags({}), { status: "unreadable", reason: "read-failure" });
|
|
});
|
|
|
|
// ffprobe's string_validation=replace (av_utf8_decode): one U+FFFD per failing
|
|
// decode step. A bad continuation or a truncated sequence fails one byte at a time;
|
|
// an overlong form, a surrogate, a code above U+10FFFF, U+FFFE/U+FFFF and 5/6-byte
|
|
// forms fail as one whole sequence. Each row was read through ffprobe 9.0.2.
|
|
const FFPROBE_REPLACEMENTS = [
|
|
["one Latin-1 byte", [0x63, 0x61, 0x66, 0xe9], "caf�"],
|
|
["truncated 4-byte sequence", [0x61, 0xf0, 0x9f, 0x22, 0x62], 'a��"b'],
|
|
["overlong 2-byte form", [0x61, 0xc0, 0xaf, 0x62], "a�b"],
|
|
["C1 overlong lead", [0x61, 0xc1, 0xbf, 0x62], "a�b"],
|
|
["overlong 3-byte form", [0x61, 0xe0, 0x80, 0x80, 0x62], "a�b"],
|
|
["overlong 4-byte form", [0x61, 0xf0, 0x80, 0x80, 0x80, 0x62], "a�b"],
|
|
["UTF-16 surrogate", [0x61, 0xed, 0xa0, 0x80, 0x62], "a�b"],
|
|
["U+FFFE", [0x61, 0xef, 0xbf, 0xbe, 0x62], "a�b"],
|
|
["U+FFFF", [0x61, 0xef, 0xbf, 0xbf, 0x62], "a�b"],
|
|
["U+FDD0 is kept", [0x61, 0xef, 0xb7, 0x90, 0x62], "ab"],
|
|
["5-byte form", [0x61, 0xf8, 0x88, 0x80, 0x80, 0x80, 0x62], "a�b"],
|
|
["6-byte form", [0x61, 0xfc, 0x84, 0x80, 0x80, 0x80, 0x80, 0x62], "a�b"],
|
|
["truncated 5-byte form", [0x61, 0xf8, 0x88, 0x62], "a��b"],
|
|
["above U+10FFFF", [0x61, 0xf4, 0x90, 0x80, 0x80, 0x62], "a�b"],
|
|
["0xFE", [0x61, 0xfe, 0x62], "a�b"],
|
|
["0xFF", [0x61, 0xff, 0x62], "a�b"],
|
|
["0xFE 0xFF", [0xfe, 0xff], "��"],
|
|
["lone continuation byte", [0x61, 0x80, 0x62], "a�b"],
|
|
["truncated at the end", [0x61, 0xe2, 0x82], "a��"],
|
|
["lead byte before a whole sequence", [0x61, 0xc3, 0xc3, 0xa9, 0x62], "a�éb"],
|
|
["valid emoji", [0x61, 0xf0, 0x9f, 0x98, 0x80, 0x62], "a\u{1f600}b"],
|
|
["control characters", [0x61, 0x01, 0x1f, 0x62], "a\u0001\u001fb"],
|
|
];
|
|
|
|
test("invalid UTF-8 reads as ffprobe replaces it; names that do not decode are not carriers", async () => {
|
|
for (const [name, bytes, expected] of FFPROBE_REPLACEMENTS) {
|
|
assert.deepEqual((await read(v.perKeyMp4({ prompt: bytes }))).tags, { prompt: expected }, name);
|
|
assert.deepEqual((await read(v.perKeyMkv({ PROMPT: bytes }))).tags, { PROMPT: expected }, `${name} (Matroska)`);
|
|
}
|
|
// A long value with one bad byte keeps every other byte.
|
|
const long = Buffer.concat([Buffer.from('{"a":"'), Buffer.alloc(100000, 0x78), Buffer.from([0xe9]), Buffer.from('"}')]);
|
|
assert.equal((await read(v.perKeyMp4({ workflow: long }))).tags.workflow, `{"a":"${"x".repeat(100000)}�"}`);
|
|
assert.deepEqual((await read(v.perKeyMp4({ prompt: [0x7b, 0xff, 0xfe, 0x7d] }))).tags, { prompt: "{��}" });
|
|
// A keys name with a bad byte is reported as "prom�pt", which is no carrier.
|
|
const badKey = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys([Uint8Array.from([0x70, 0x72, 0x6f, 0x6d, 0xff, 0x70, 0x74]), "prompt"]), v.ilst(v.item(1, v.data("junk")), v.item(2, v.data(GRAPH))))));
|
|
assert.deepEqual(await read(badKey), complete({ prompt: GRAPH }, { container: "iso-bmff", format: "isom" }));
|
|
const onlyBadKey = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys([Uint8Array.from([0xc3, 0x28])]), v.ilst(v.item(1, v.data(GRAPH))))));
|
|
assert.deepEqual((await read(onlyBadKey)).tags, {});
|
|
assert.deepEqual((await read(v.mkv([v.tagsEl(v.tag([v.simpleTag([0xff], GRAPH), v.simpleTag("WORKFLOW", WORKFLOW)]))]))).tags, { WORKFLOW: WORKFLOW });
|
|
assert.deepEqual((await read(v.mkv([v.tagsEl(v.tag([v.simpleTag("PROMPT", [0xff, 0xfe])]))]))).tags, { PROMPT: "��" });
|
|
});
|
|
|
|
test("ilst items with index 0 or past the keys are skipped; the first keys box wins", async () => {
|
|
const wf = '{"nodes":[]}';
|
|
const stray = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt", "workflow"]), v.ilst(v.item(1, v.data(GRAPH)), v.item(5, v.data("junk")), v.item(2, v.data(wf))))));
|
|
assert.deepEqual((await read(stray)).tags, { prompt: GRAPH, workflow: wf });
|
|
const zero = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt", "workflow"]), v.ilst(v.item(0, v.data("junk")), v.item(1, v.data(GRAPH)), v.item(2, v.data(wf))))));
|
|
assert.deepEqual((await read(zero)).tags, { prompt: GRAPH, workflow: wf });
|
|
assert.deepEqual((await read(v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"]), v.ilst(v.item(2, v.data(GRAPH)))))))).tags, {});
|
|
// ffmpeg reads one keys box: a second one (even malformed) addresses nothing.
|
|
const twoKeys = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"]), v.keys(["workflow"]), v.ilst(v.item(1, v.data(GRAPH))))));
|
|
assert.deepEqual((await read(twoKeys)).tags, { prompt: GRAPH });
|
|
const brokenSecond = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["prompt"]), v.keys(["workflow"], { count: 9 }), v.ilst(v.item(1, v.data(GRAPH))))));
|
|
assert.deepEqual((await read(brokenSecond)).tags, { prompt: GRAPH });
|
|
const emptyFirst = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys([]), v.keys(["prompt"]), v.ilst(v.item(1, v.data(GRAPH))))));
|
|
assert.deepEqual((await read(emptyFirst)).tags, {});
|
|
// A keys box too short for its count field is not read, so the next one is.
|
|
const shortFirst = v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.fullBox("keys"), v.keys(["prompt"]), v.ilst(v.item(1, v.data(GRAPH))))));
|
|
assert.deepEqual((await read(shortFirst)).tags, { prompt: GRAPH });
|
|
});
|
|
|
|
test("budgets: roles share 2 MiB, an oversized workflow is left unread, duplicates and the total are bounded", async () => {
|
|
const limited = { status: "limited", reason: "metadata-limit" };
|
|
// Role tags over 2 MiB together stop the read (the server fails the whole read).
|
|
assert.deepEqual(await read(v.perKeyMp4({ prompt: fill(0x41, ROLE_BUDGET + 1) })), limited);
|
|
assert.deepEqual(await read(v.perKeyMkv({ PROMPT: fill(0x41, ROLE_BUDGET - 10), PARAMETERS: fill(0x42, 11) })), limited);
|
|
const atRole = await read(v.perKeyMp4({ prompt: fill(0x41, ROLE_BUDGET - 10), comfyassets_prompt_pairs: fill(0x42, 10) }));
|
|
assert.equal(atRole.status, "complete");
|
|
assert.equal(atRole.tags.prompt.length, ROLE_BUDGET - 10);
|
|
// A per-key workflow over 4 MiB is not sliced; the result says the workflow is limited.
|
|
const big = v.recordingBlob(v.perKeyMp4({ prompt: GRAPH, workflow: fill(0x7b, WORKFLOW_BUDGET + 1) }));
|
|
const over = await readVideoTags(big);
|
|
// It stays in the tag map as a marker, so the carrier choice still sees a per-key workflow.
|
|
assert.deepEqual(over, { status: "complete", container: "iso-bmff", format: "isom", tags: { prompt: GRAPH, workflow: m.OVERSIZED_WORKFLOW }, limited: ["workflow"] });
|
|
assert.ok(big.ranges.every(([start, end]) => end - start < WORKFLOW_BUDGET), "the oversized workflow was never sliced");
|
|
const bigMkv = await read(v.perKeyMkv({ WORKFLOW: fill(0x7b, WORKFLOW_BUDGET + 1), PROMPT: GRAPH }));
|
|
assert.deepEqual([bigMkv.limited, Object.keys(bigMkv.tags)], [["workflow"], ["WORKFLOW", "PROMPT"]]);
|
|
const atWorkflow = await read(v.perKeyMp4({ workflow: fill(0x7b, WORKFLOW_BUDGET) }));
|
|
assert.deepEqual([atWorkflow.limited, atWorkflow.tags.workflow.length], [[], WORKFLOW_BUDGET]);
|
|
// Fold-duplicates draw from the duplicate budgets (2x each class), then stop.
|
|
const twice = await read(v.perKeyMkv({ PROMPT: fill(0x41, ROLE_BUDGET), prompt: fill(0x41, ROLE_BUDGET) }));
|
|
assert.deepEqual(Object.keys(twice.tags).sort(), ["PROMPT", "prompt"]);
|
|
const thrice = await read(v.perKeyMkv({ PROMPT: fill(0x41, ROLE_BUDGET), prompt: fill(0x41, ROLE_BUDGET), Prompt: fill(0x41, ROLE_BUDGET + 1) }));
|
|
assert.deepEqual(thrice, limited);
|
|
const workflowTwice = await read(v.perKeyMkv({ WORKFLOW: fill(0x7b, WORKFLOW_BUDGET), workflow: fill(0x7b, WORKFLOW_BUDGET) }));
|
|
assert.equal(workflowTwice.status, "complete");
|
|
// Envelopes count against the total only; the total is the server's probe bound.
|
|
const envelope = await read(v.mp4(v.legacyUdta({ comment: fill(0x41, 5 * MiB) })));
|
|
assert.equal(envelope.tags.comment.length, 5 * MiB);
|
|
const tooMuch = await read(v.mp4(v.legacyUdta({ comment: fill(0x41, TAG_READ_BYTES - 10), description: fill(0x42, 11) })));
|
|
assert.deepEqual(tooMuch, limited);
|
|
assert.equal(m.VIDEO_TAG_READ_BYTES, TAG_READ_BYTES);
|
|
});
|
|
|
|
test("an oversized per-key workflow keeps its place in the carrier choice, as in the server's first probe", async () => {
|
|
const big = fill(0x7b, WORKFLOW_BUDGET + 1);
|
|
const envelope = '{"workflow": {"nodes": [1]}}';
|
|
const beside = (comment) => v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["workflow"]), v.ilst(v.item(1, v.data(big)), v.legacyItem("©cmt", v.data(comment))))));
|
|
const read1 = await read(beside(envelope));
|
|
assert.deepEqual([read1.tags, read1.limited], [{ workflow: m.OVERSIZED_WORKFLOW, comment: envelope }, ["workflow"]]);
|
|
// The server picks the per-key carrier (no role on either side) and marks the workflow limited.
|
|
assert.deepEqual(selectVideoCarrier(read1.tags, { limited: read1.limited }), { status: "complete", records: {}, record_formats: { workflow: "limited" }, carrierText: null, limited: ["workflow"] });
|
|
// A role envelope still wins and clears it.
|
|
const roleEnvelope = '{"prompt": "{}", "workflow": {"a": 1}}';
|
|
const read2 = await read(beside(roleEnvelope));
|
|
assert.deepEqual(selectVideoCarrier(read2.tags, { limited: read2.limited }), { status: "complete", records: { prompt: "{}", workflow: '{"a": 1}' }, record_formats: { prompt: "original-text", workflow: "reserialized-json" }, carrierText: roleEnvelope, limited: [] });
|
|
// ISO-BMFF joins a repeat onto the oversized value, which stays over budget and unread.
|
|
const repeat = (extra) => v.mp4(v.udta(v.meta(v.hdlr("mdta"), v.keys(["workflow", "prompt"]), v.ilst(v.item(1, v.data(big)), v.item(1, v.data(extra)), v.item(2, v.data(GRAPH))))));
|
|
const repeatedBlob = v.recordingBlob(repeat(WORKFLOW));
|
|
const repeated = await readVideoTags(repeatedBlob);
|
|
assert.deepEqual([repeated.tags, repeated.limited], [{ workflow: m.OVERSIZED_WORKFLOW, prompt: GRAPH }, ["workflow"]]);
|
|
assert.ok(repeatedBlob.ranges.every(([start, end]) => end - start < 1024), "neither workflow part was sliced");
|
|
// When the first probe would overflow its bound, the server re-probes without the
|
|
// per-key workflow: then it takes no part in the choice ...
|
|
const dropped = await read(repeat(fill(0x41, TAG_READ_BYTES - WORKFLOW_BUDGET)));
|
|
assert.deepEqual([dropped.tags, dropped.limited], [{ prompt: GRAPH }, ["workflow"]]);
|
|
// ... and the re-probe's own bound (twice the role budget plus 64 KiB) applies to the rest.
|
|
const wide = '{"workflow": {"n": "' + "x".repeat(TAG_READ_BYTES - WORKFLOW_BUDGET) + '"}}';
|
|
assert.deepEqual(await read(beside(wide)), { status: "limited", reason: "metadata-limit" });
|
|
assert.equal(m.VIDEO_REPROBE_BYTES, 2 * ROLE_BUDGET + 64 * 1024);
|
|
// Matroska keeps the last value of a name, and a string-less SimpleTag deletes it.
|
|
const replaced = await read(v.mkv([v.tagsEl(v.tag([v.simpleTag("WORKFLOW", big), v.simpleTag("WORKFLOW", WORKFLOW)]))]));
|
|
assert.deepEqual([replaced.tags, replaced.limited], [{ WORKFLOW: WORKFLOW }, []]);
|
|
const deleted = await read(v.mkv([v.tagsEl(v.tag([v.simpleTag("WORKFLOW", big), v.simpleTag("WORKFLOW", undefined), v.simpleTag("PROMPT", GRAPH)]))]));
|
|
assert.deepEqual([deleted.tags, deleted.limited], [{ PROMPT: GRAPH }, []]);
|
|
});
|
|
|
|
test("avi and asf are recognised but not read", async () => {
|
|
const avi = v.concat(v.latin1("RIFF"), v.u32(1000), v.latin1("AVI LIST"), new Uint8Array(16));
|
|
const asf = v.concat([0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c], new Uint8Array(16));
|
|
for (const bytes of [avi, asf]) {
|
|
const blob = v.recordingBlob(v.concat(bytes, new Uint8Array(4096)));
|
|
assert.deepEqual(await readVideoTags(blob, { prefix: bytes }), { status: "unsupported", reason: "video-container-unsupported" });
|
|
assert.deepEqual(blob.ranges, []);
|
|
}
|
|
// The classifier's kind can be handed in instead of a prefix.
|
|
const blob = v.recordingBlob(new Uint8Array(64));
|
|
assert.deepEqual(await readVideoTags(blob, { kind: "avi" }), { status: "unsupported", reason: "video-container-unsupported" });
|
|
assert.deepEqual(blob.ranges, []);
|
|
// Without a prefix the reader slices the first 32 bytes itself.
|
|
const mp4 = v.recordingBlob(v.perKeyMp4({ prompt: GRAPH }));
|
|
assert.deepEqual((await readVideoTags(mp4)).tags, { prompt: GRAPH });
|
|
assert.deepEqual(mp4.ranges[0], [0, 32]);
|
|
});
|
|
|
|
test("abort during a moov walk settles once", async () => {
|
|
const unhandled = [];
|
|
const onUnhandled = (reason) => unhandled.push(reason);
|
|
process.on("unhandledRejection", onUnhandled);
|
|
try {
|
|
const bytes = v.concat(v.ftyp(), ...Array.from({ length: 50 }, () => v.free()), v.mdat(), v.moov(v.mdtaUdta({ prompt: GRAPH, workflow: WORKFLOW })));
|
|
const controller = new AbortController();
|
|
let slices = 0;
|
|
const blob = {
|
|
size: bytes.length,
|
|
slice(start, end) {
|
|
slices += 1;
|
|
if (slices === 3) controller.abort();
|
|
return { arrayBuffer: () => new Promise((resolve) => setTimeout(() => resolve(bytes.slice(start, end).buffer), 2)) };
|
|
},
|
|
};
|
|
await assert.rejects(readVideoTags(blob, { signal: controller.signal }), { name: "AbortError" });
|
|
const already = new AbortController();
|
|
already.abort();
|
|
await assert.rejects(readVideoTags(v.blobOf(bytes), { signal: already.signal }), { name: "AbortError" });
|
|
const slow = v.slowBlob(bytes, 1);
|
|
const during = new AbortController();
|
|
const pending = readVideoTags(slow, { signal: during.signal, yieldControl: async () => during.abort() });
|
|
await assert.rejects(pending, { name: "AbortError" });
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.deepEqual(unhandled, []);
|
|
} finally {
|
|
process.off("unhandledRejection", onUnhandled);
|
|
}
|
|
});
|
|
|
|
test("yieldControl is awaited while walking many boxes", async () => {
|
|
let yields = 0;
|
|
const many = v.concat(v.ftyp(), ...Array.from({ length: 3000 }, () => v.free()), v.mdat(), v.moov(v.mdtaUdta({ prompt: GRAPH })));
|
|
const result = await readVideoTags(v.slowBlob(many, 0), { yieldControl: async () => { yields += 1; } });
|
|
assert.equal(result.status, "complete");
|
|
assert.ok(yields > 0, "the reader handed control back");
|
|
});
|
|
|
|
test("carrier selection: per-key tags, then the comment envelope, then description, never merged", () => {
|
|
const ENVELOPE_WORKFLOW = '{"last_node_id": 9, "nodes": [{"id": 3, "type": "KSampler", "widgets_values": [7, "fixed", 20, 8.0, "euler", "normal", 1.0]}, {"id": 6, "type": "CLIPTextEncode", "widgets_values": ["a red fox in snow, detailed fur"]}, {"id": 7, "type": "CLIPTextEncode", "widgets_values": ["blurry, lowres"]}], "links": [], "version": 0.4}';
|
|
const original = (keys) => Object.fromEntries(keys.map((key) => [key, "original-text"]));
|
|
const text = (tags, keys) => Object.fromEntries(keys.map((key) => [key, tags[key] ?? tags[key.toUpperCase()]]));
|
|
// The recorded fixtures.
|
|
for (const name of ["core-faststart.mp4", "core-moov-end.mp4", "core.mov", "core.m4v", "core.mkv", "core-tags.webm"]) {
|
|
const tags = RECORDED[name];
|
|
const keys = Object.keys(tags).map((key) => key.toLowerCase()).filter((key) => PER_KEY.has(key));
|
|
assert.deepEqual(selectVideoCarrier(tags), { status: "complete", records: text(tags, keys), record_formats: original(keys), carrierText: null, limited: [] }, name);
|
|
}
|
|
const legacy = RECORDED["legacy-comment.mp4"];
|
|
const comment = pythonJsonLoads(legacy.comment);
|
|
assert.deepEqual(selectVideoCarrier(legacy), {
|
|
status: "complete",
|
|
records: { prompt: comment.get("prompt"), workflow: ENVELOPE_WORKFLOW },
|
|
record_formats: { prompt: "original-text", workflow: "reserialized-json" },
|
|
carrierText: legacy.comment,
|
|
limited: [],
|
|
});
|
|
assert.ok(comment.get("prompt").startsWith('{"3":{"class_type":"KSampler"'));
|
|
// Matroska upper-cases COMMENT, and envelope names are exact-case (ruling M1): nothing is read.
|
|
assert.deepEqual(selectVideoCarrier(RECORDED["legacy-comment.webm"]), { status: "complete", records: {}, record_formats: {}, carrierText: null, limited: [] });
|
|
// Synthetic maps.
|
|
const envelope = JSON.stringify({ prompt: '{"9":{}}', workflow: { v: 1 } });
|
|
const cases = [
|
|
["per-key wins over comment", { prompt: GRAPH, workflow: WORKFLOW, comment: envelope }, { records: { prompt: GRAPH, workflow: WORKFLOW }, record_formats: original(["prompt", "workflow"]), carrierText: null }],
|
|
["Matroska names fold", { PROMPT: GRAPH, Workflow: WORKFLOW, ENCODER: "Lavf" }, { records: { prompt: GRAPH, workflow: WORKFLOW }, record_formats: original(["prompt", "workflow"]), carrierText: null }],
|
|
["identical fold-duplicates are one record", { PROMPT: GRAPH, prompt: GRAPH }, { records: { prompt: GRAPH }, record_formats: original(["prompt"]), carrierText: null }],
|
|
["differing fold-duplicates fail", { PROMPT: GRAPH, prompt: '{"9":{}}' }, { status: "unreadable", reason: "duplicate-metadata" }],
|
|
["differing workflow duplicates fail even with a prompt", { Workflow: WORKFLOW, WORKFLOW: "{}", prompt: GRAPH }, { status: "unreadable", reason: "duplicate-metadata" }],
|
|
["envelope names never fold", { comment: envelope, COMMENT: "a free-text note" }, { records: { prompt: '{"9":{}}', workflow: '{"v": 1}' }, record_formats: { prompt: "original-text", workflow: "reserialized-json" }, carrierText: envelope }],
|
|
["non-JSON comment falls through to description", { comment: "not json", description: envelope }, { records: { prompt: '{"9":{}}', workflow: '{"v": 1}' }, record_formats: { prompt: "original-text", workflow: "reserialized-json" }, carrierText: envelope }],
|
|
["a JSON array comment is not an envelope", { comment: '["prompt"]', description: '{"workflow": {}}' }, { records: { workflow: "{}" }, record_formats: { workflow: "reserialized-json" }, carrierText: '{"workflow": {}}' }],
|
|
["a role envelope beats a workflow-only per-key carrier", { workflow: WORKFLOW, comment: envelope }, { records: { prompt: '{"9":{}}', workflow: '{"v": 1}' }, record_formats: { prompt: "original-text", workflow: "reserialized-json" }, carrierText: envelope }],
|
|
["role description beats workflow-only comment", { comment: '{"workflow": {"a": 1}}', description: '{"parameters": "cat"}' }, { records: { parameters: "cat" }, record_formats: { parameters: "original-text" }, carrierText: '{"parameters": "cat"}' }],
|
|
["workflow-only carriers: the first wins, never merged", { comment: '{"workflow": {"a": 1}}', description: '{"workflow": {"b": 2}}' }, { records: { workflow: '{"a": 1}' }, record_formats: { workflow: "reserialized-json" }, carrierText: '{"workflow": {"a": 1}}' }],
|
|
["description only", { description: envelope }, { records: { prompt: '{"9":{}}', workflow: '{"v": 1}' }, record_formats: { prompt: "original-text", workflow: "reserialized-json" }, carrierText: envelope }],
|
|
["no known key", { encoder: "Lavf", major_brand: "isom" }, { records: {}, record_formats: {}, carrierText: null }],
|
|
["envelope with only unknown keys is no carrier", { comment: '{"title": "x"}' }, { records: {}, record_formats: {}, carrierText: null }],
|
|
["null role marker is refused", { comment: '{"prompt": null, "workflow": {}}' }, { status: "unreadable", reason: "invalid-metadata" }],
|
|
["numeric role is refused", { description: '{"parameters": 5}' }, { status: "unreadable", reason: "invalid-metadata" }],
|
|
["a non-text non-object workflow is invalid but the roles stay", { comment: '{"prompt": "{}", "workflow": null}' }, { records: { prompt: "{}" }, record_formats: { prompt: "original-text", workflow: "invalid" }, carrierText: '{"prompt": "{}", "workflow": null}' }],
|
|
["an object prompt is reserialized", { comment: '{"prompt": {"1": {"class_type": "A"}}}' }, { records: { prompt: '{"1": {"class_type": "A"}}' }, record_formats: { prompt: "reserialized-json" }, carrierText: '{"prompt": {"1": {"class_type": "A"}}}' }],
|
|
["a non-text per-key role marker is refused", { prompt: 5, workflow: WORKFLOW }, { status: "unreadable", reason: "invalid-metadata" }],
|
|
["a non-text per-key workflow is invalid", { prompt: GRAPH, workflow: 5 }, { records: { prompt: GRAPH }, record_formats: { prompt: "original-text", workflow: "invalid" }, carrierText: null }],
|
|
["a workflow-only envelope with an invalid workflow is still the carrier", { comment: '{"workflow": 5}' }, { records: {}, record_formats: { workflow: "invalid" }, carrierText: '{"workflow": 5}' }],
|
|
["not a map", null, { records: {}, record_formats: {}, carrierText: null }],
|
|
];
|
|
for (const [name, tags, expected] of cases) {
|
|
const want = expected.status ? expected : { status: "complete", limited: [], ...expected };
|
|
assert.deepEqual(selectVideoCarrier(tags), want, name);
|
|
}
|
|
// The server's budget loop: a 4 MiB workflow is limited, roles over 2 MiB fail, exact sizes pass.
|
|
const bigWorkflow = "{" + "0".repeat(WORKFLOW_BUDGET);
|
|
assert.deepEqual(selectVideoCarrier({ prompt: GRAPH, workflow: bigWorkflow }), { status: "complete", records: { prompt: GRAPH }, record_formats: { prompt: "original-text", workflow: "limited" }, carrierText: null, limited: ["workflow"] });
|
|
assert.deepEqual(selectVideoCarrier({ workflow: bigWorkflow.slice(1) }).records, { workflow: bigWorkflow.slice(1) });
|
|
assert.deepEqual(selectVideoCarrier({ comment: JSON.stringify({ prompt: "x", workflow: { k: "0".repeat(WORKFLOW_BUDGET) } }) }).record_formats, { prompt: "original-text", workflow: "limited" });
|
|
assert.deepEqual(selectVideoCarrier({ prompt: "a".repeat(ROLE_BUDGET), parameters: "b" }), { status: "limited", reason: "metadata-limit" });
|
|
assert.deepEqual(selectVideoCarrier({ prompt: "é".repeat(ROLE_BUDGET / 2) }).records.prompt.length, ROLE_BUDGET / 2);
|
|
assert.deepEqual(selectVideoCarrier({ prompt: "é".repeat(ROLE_BUDGET / 2) + "a" }), { status: "limited", reason: "metadata-limit" });
|
|
assert.deepEqual(selectVideoCarrier({ prompt: "\ud800".repeat(ROLE_BUDGET / 3) + "aaa" }), { status: "limited", reason: "metadata-limit" });
|
|
assert.deepEqual(selectVideoCarrier({ prompt: "\u{1f600}".repeat(ROLE_BUDGET / 4) }).records.prompt.length, ROLE_BUDGET / 2);
|
|
assert.deepEqual(selectVideoCarrier({ prompt: "\u{1f600}".repeat(ROLE_BUDGET / 4) + "a" }), { status: "limited", reason: "metadata-limit" });
|
|
// The reader's limited workflow (the server's role-only re-probe): kept with the per-key
|
|
// carrier or no carrier, cleared when an envelope wins.
|
|
assert.deepEqual(selectVideoCarrier({ prompt: GRAPH }, { limited: ["workflow"] }).record_formats, { prompt: "original-text", workflow: "limited" });
|
|
assert.deepEqual(selectVideoCarrier({ encoder: "x" }, { limited: ["workflow"] }), { status: "complete", records: {}, record_formats: { workflow: "limited" }, carrierText: null, limited: ["workflow"] });
|
|
assert.deepEqual(selectVideoCarrier({ comment: envelope }, { limited: ["workflow"] }).limited, []);
|
|
assert.deepEqual(selectVideoCarrier({ comment: envelope }, { limited: ["workflow"] }).record_formats, { prompt: "original-text", workflow: "reserialized-json" });
|
|
});
|
|
|
|
test("pythonJsonLoads and pythonJsonDumps spell values as Python's json module does", () => {
|
|
const rows = [
|
|
["1.0", "1.0"], ["8.0", "8.0"], ["0.1", "0.1"], ["1e16", "1e+16"], ["1e15", "1000000000000000.0"],
|
|
["123456789012345678", "123456789012345678"], ["0.0001", "0.0001"], ["0.00001", "1e-05"], ["1e-7", "1e-07"],
|
|
["1.5e300", "1.5e+300"], ["1e400", "Infinity"], ["-0", "0"], ["-0.0", "-0.0"], ["1E2", "100.0"], ["2.5", "2.5"],
|
|
["100.0", "100.0"], ["1e21", "1e+21"], ["123456.789", "123456.789"], ["0.000123", "0.000123"], ["-1e-05", "-1e-05"],
|
|
["9007199254740993", "9007199254740993"], ["1.7976931348623157e308", "1.7976931348623157e+308"], ["5e-324", "5e-324"],
|
|
["NaN", "NaN"], ["-Infinity", "-Infinity"], ["Infinity", "Infinity"], ["[1.0e+2]", "[100.0]"], ["-1e400", "-Infinity"],
|
|
["[]", "[]"], ["{}", "{}"], ["[true, false, null]", "[true, false, null]"], ['{"k":[1,{"z":1}]}', '{"k": [1, {"z": 1}]}'],
|
|
[' \t\n\r{"a" : 1 } ', '{"a": 1}'],
|
|
['{"a":"é\\u0001\u007f\\"\\\\\\n\\t\\b\\f\\r\\/ 😀 \\ud800"}', '{"a": "\\u00e9\\u0001\\u007f\\"\\\\\\n\\t\\b\\f\\r/ \\ud83d\\ude00 \\ud800"}'],
|
|
['{"a":"\\ud83d\\ude00"}', '{"a": "\\ud83d\\ude00"}'],
|
|
['{"10":1,"3":2,"b":3,"a":4}', '{"10": 1, "3": 2, "b": 3, "a": 4}'],
|
|
['{"a":1,"b":2,"a":3}', '{"a": 3, "b": 2}'],
|
|
];
|
|
for (const [input, output] of rows) {
|
|
assert.equal(pythonJsonDumps(pythonJsonLoads(input)), output, input);
|
|
}
|
|
const bad = ['{"a":1} x', '{"a":"\u0001"}', '{"a":"\\x"}', "01", "1.", ".5", "+1", "[1,]", '{"a":1,}', "{a:1}", "'a'", "", " ", "[", "nul", "-", "1e", '"\\u12"', "[1 2]", "tru", "Infinit"];
|
|
for (const input of bad) {
|
|
assert.throws(() => pythonJsonLoads(input), { name: "PythonJsonError" }, JSON.stringify(input));
|
|
}
|
|
assert.throws(() => pythonJsonLoads("[".repeat(2000) + "]".repeat(2000)), { name: "PythonJsonError", recursion: true });
|
|
// CPython's int() refuses more than 4300 digits (sign excluded); floats have no such limit.
|
|
const digits = (count) => "1" + "0".repeat(count - 1);
|
|
assert.equal(pythonJsonDumps(pythonJsonLoads(digits(4300))), digits(4300));
|
|
assert.equal(pythonJsonDumps(pythonJsonLoads("-" + digits(4300))), "-" + digits(4300));
|
|
for (const input of [digits(4301), "-" + digits(4301), `[${digits(5000)}]`]) {
|
|
assert.throws(() => pythonJsonLoads(input), (error) => error.name === "PythonJsonError" && error.recursion === false, input.slice(0, 8));
|
|
}
|
|
assert.equal(pythonJsonLoads(digits(4301) + ".0").isFloat, true);
|
|
// So an envelope holding such an integer is skipped, as the server's _carriers does.
|
|
const hugeInt = `{"workflow": {"a": ${digits(4400)}}}`;
|
|
assert.deepEqual(selectVideoCarrier({ comment: hugeInt, description: '{"workflow": {"b": 2}}' }).carrierText, '{"workflow": {"b": 2}}');
|
|
assert.throws(() => pythonJsonDumps(undefined), { name: "PythonJsonError" });
|
|
assert.throws(() => pythonJsonLoads(5), { name: "PythonJsonError" });
|
|
assert.equal(pythonJsonDumps(pythonJsonLoads("[".repeat(500) + "]".repeat(500))), "[".repeat(500) + "]".repeat(500));
|
|
const parsed = pythonJsonLoads('{"s":"x","n":1,"f":1.5,"b":true,"z":null,"l":[1]}');
|
|
assert.ok(parsed instanceof Map);
|
|
assert.deepEqual([...parsed.keys()], ["s", "n", "f", "b", "z", "l"]);
|
|
assert.equal(parsed.get("s"), "x");
|
|
assert.equal(parsed.get("b"), true);
|
|
assert.equal(parsed.get("z"), null);
|
|
assert.ok(Array.isArray(parsed.get("l")));
|
|
});
|