evOo-local/packages/adapters/src/media-ingest-cluster.test.ts
Claude b416ff0fdf
feat(media): ingest job handlers with bounded concurrency and disk backpressure
media.sync, media.proxy, media.analyze, and media.cluster for Media Studio:
incremental metadata sync with deletion detection, 720p CFR proxies with
1 s keyframes, tone mapping, posters and sprites, sidecar analyses stored per
model version with scored segments, and sessions, themes, and person
suggestions. Proxy and analysis work is pumped in a small per-space window,
jobs step aside when their GPU or CPU permit is taken, and a full disk or an
unreachable sidecar pauses work instead of failing assets. Re-clustering
keeps collections the user merged or split. Fixes photo posters, which
never rendered because ffmpeg yields no frame when seeking a single image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
2026-10-03 18:50:30 +00:00

78 lines
3.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { reconcileUserEdits } from "./media-ingest-cluster.js";
const computed = [
{ key: "session:a1", assetIds: ["a1", "a2", "a3"] },
{ key: "session:b1", assetIds: ["b1", "b2"] },
{ key: "session:c1", assetIds: ["c1", "c2", "c3"] },
];
const keys = (clusters: Array<{ key: string; assetIds: string[] }>) =>
clusters.map((cluster) => [cluster.key, cluster.assetIds]);
describe("reconcileUserEdits", () => {
it("passes computed clusters through when the user edited nothing", () => {
expect(reconcileUserEdits(computed, [], "session")).toEqual(computed);
});
it("never re-adds the assets of a merged collection to computed clusters", () => {
// The user merged a1.. and b1.. into the a1 collection.
const merged = { clusterKey: "session:a1", assetIds: ["a1", "a2", "a3", "b1", "b2"] };
expect(keys(reconcileUserEdits(computed, [merged], "session"))).toEqual([
["session:c1", ["c1", "c2", "c3"]],
]);
});
it("keeps both halves of a split and places new assets outside them", () => {
// Split c1 | c2 c3; a later sync adds c4 to the computed session.
const grown = [
...computed.slice(0, 2),
{ key: "session:c1", assetIds: ["c1", "c2", "c3", "c4"] },
];
const kept = { clusterKey: "session:c1", assetIds: ["c1"] };
const splitOff = { clusterKey: null, assetIds: ["c2", "c3"] };
expect(keys(reconcileUserEdits(grown, [kept, splitOff], "session"))).toEqual([
["session:a1", ["a1", "a2", "a3"]],
["session:b1", ["b1", "b2"]],
["session:c1", ["c1", "c4"]],
]);
// When the kept half is not a subset any more it freezes; c4 gets its own key.
const moved = [...computed.slice(0, 2), { key: "session:c0", assetIds: ["c0", "c4"] }];
expect(
keys(reconcileUserEdits(moved, [{ ...kept, assetIds: ["c1", "c9"] }], "session")),
).toEqual([
["session:a1", ["a1", "a2", "a3"]],
["session:b1", ["b1", "b2"]],
["session:c0", ["c0", "c4"]],
]);
});
it("lets a renamed collection follow its cluster, even when the cluster key changes", () => {
const renamed = { clusterKey: "session:a1", assetIds: ["a1", "a2"] };
const earlier = [
{ key: "session:a0", assetIds: ["a0", "a1", "a2", "a3"] },
...computed.slice(1),
];
expect(keys(reconcileUserEdits(earlier, [renamed], "session"))).toEqual([
["session:a1", ["a0", "a1", "a2", "a3"]],
["session:b1", ["b1", "b2"]],
["session:c1", ["c1", "c2", "c3"]],
]);
});
it("re-keys a cluster that lost its first asset to a split-off collection", () => {
const splitOff = { clusterKey: null, assetIds: ["a1"] };
expect(keys(reconcileUserEdits(computed, [splitOff], "theme"))).toEqual([
["theme:a2", ["a2", "a3"]],
["session:b1", ["b1", "b2"]],
["session:c1", ["c1", "c2", "c3"]],
]);
});
it("ignores empty user collections", () => {
expect(
reconcileUserEdits(computed, [{ clusterKey: "session:a1", assetIds: [] }], "session"),
).toEqual(computed);
});
});