Index photos and videos from the device library into a client-pushed ios_library source, upload originals in resumable chunks, and review, approve, and share project variants with expo-video and the native share sheet. Pure logic for chunking, resume, indexing, and variants is unit tested. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
300 lines
9.5 KiB
TypeScript
300 lines
9.5 KiB
TypeScript
import type { MediaEditPlan, MediaRender } from "@rakazo/contracts";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
assetThumbUrl,
|
|
blockers,
|
|
canApprove,
|
|
canShare,
|
|
formatDuration,
|
|
isRenderActive,
|
|
libraryAccess,
|
|
mediaNotificationRoute,
|
|
projectReadiness,
|
|
projectVariants,
|
|
renderFileUrl,
|
|
shareFileName,
|
|
shouldPollProject,
|
|
uploadsFromDevice,
|
|
variantState,
|
|
} from "./media-project";
|
|
|
|
function plan(
|
|
id: string,
|
|
profileId: string,
|
|
version: number,
|
|
createdAt = "2026-01-01T00:00:00.000Z",
|
|
) {
|
|
return { id, profileId, version, createdAt } as MediaEditPlan;
|
|
}
|
|
|
|
function render(id: string, planId: string, overrides: Partial<MediaRender> = {}): MediaRender {
|
|
return {
|
|
id,
|
|
planId,
|
|
projectId: "p1",
|
|
profileId: "example-short-vertical",
|
|
quality: "draft",
|
|
status: "done",
|
|
progress: 1,
|
|
outputs: [{ kind: "video", artifactId: null, fileName: "video.mp4", sizeBytes: 10 }],
|
|
gateReport: null,
|
|
ffprobe: null,
|
|
error: null,
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const profiles = [{ id: "short", name: "Short" }];
|
|
|
|
function variantFor(renders: MediaRender[]) {
|
|
const [variant] = projectVariants({
|
|
profileIds: ["short"],
|
|
plans: [plan("plan1", "short", 1)],
|
|
renders,
|
|
profiles,
|
|
});
|
|
if (!variant) throw new Error("no variant");
|
|
return variant;
|
|
}
|
|
|
|
describe("projectVariants", () => {
|
|
it("uses the newest plan version per profile and its renders only", () => {
|
|
const variants = projectVariants({
|
|
profileIds: ["short", "wide", "missing"],
|
|
plans: [plan("a1", "short", 1), plan("a2", "short", 2), plan("b1", "wide", 1)],
|
|
renders: [
|
|
render("r-old", "a1", { createdAt: "2026-01-02T00:00:00.000Z" }),
|
|
render("r-new", "a2"),
|
|
],
|
|
profiles,
|
|
});
|
|
expect(
|
|
variants.map((variant) => [variant.profileId, variant.profileName, variant.plan.id]),
|
|
).toEqual([
|
|
["short", "Short", "a2"],
|
|
["wide", "wide", "b1"],
|
|
]);
|
|
expect(variants[0]?.latest?.id).toBe("r-new");
|
|
expect(variants[1]?.latest).toBeNull();
|
|
});
|
|
|
|
it("previews the final render once it is done, else the draft", () => {
|
|
const draft = render("d", "plan1", { createdAt: "2026-01-01T00:00:00.000Z" });
|
|
const finalRunning = render("f", "plan1", {
|
|
quality: "final",
|
|
status: "rendering",
|
|
outputs: [],
|
|
createdAt: "2026-01-02T00:00:00.000Z",
|
|
});
|
|
expect(variantFor([draft, finalRunning]).preview?.id).toBe("d");
|
|
const finalDone = { ...finalRunning, status: "done" as const, outputs: draft.outputs };
|
|
expect(variantFor([draft, finalDone]).preview?.id).toBe("f");
|
|
});
|
|
});
|
|
|
|
describe("variant state and actions", () => {
|
|
it("is planned without renders and can be approved", () => {
|
|
const variant = variantFor([]);
|
|
expect(variantState(variant)).toBe("planned");
|
|
expect(canApprove(variant)).toBe(true);
|
|
expect(canShare(variant)).toBe(false);
|
|
});
|
|
|
|
it("is rendering while a draft runs and cannot be approved", () => {
|
|
const variant = variantFor([render("d", "plan1", { status: "gating", progress: 0.9 })]);
|
|
expect(variantState(variant)).toBe("rendering");
|
|
expect(canApprove(variant)).toBe(false);
|
|
});
|
|
|
|
it("approves a finished draft", () => {
|
|
const variant = variantFor([render("d", "plan1")]);
|
|
expect(variantState(variant)).toBe("draft_ready");
|
|
expect(canApprove(variant)).toBe(true);
|
|
expect(canShare(variant)).toBe(false);
|
|
});
|
|
|
|
it("shares only a finished final render", () => {
|
|
const running = variantFor([render("f", "plan1", { quality: "final", status: "queued" })]);
|
|
expect(variantState(running)).toBe("approved_rendering");
|
|
expect(canShare(running)).toBe(false);
|
|
const done = variantFor([render("f", "plan1", { quality: "final" })]);
|
|
expect(variantState(done)).toBe("ready");
|
|
expect(canShare(done)).toBe(true);
|
|
expect(canApprove(done)).toBe(false);
|
|
});
|
|
|
|
it("never shares a blocked render and lists its blockers", () => {
|
|
const blocked = render("f", "plan1", {
|
|
quality: "final",
|
|
status: "blocked",
|
|
gateReport: {
|
|
profileId: "short",
|
|
passed: false,
|
|
checkedAt: "2026-01-01T00:00:00.000Z",
|
|
sampledFrames: 10,
|
|
autoFixRound: 3,
|
|
issues: [
|
|
{
|
|
check: "consent",
|
|
severity: "block",
|
|
code: "consent_missing",
|
|
message: "Consent missing",
|
|
},
|
|
{ check: "text", severity: "warn", code: "long", message: "Long caption" },
|
|
],
|
|
},
|
|
});
|
|
const variant = variantFor([blocked]);
|
|
expect(variantState(variant)).toBe("blocked");
|
|
expect(canShare(variant)).toBe(false);
|
|
expect(canApprove(variant)).toBe(false);
|
|
expect(blockers(variant.latest).map((issue) => issue.code)).toEqual(["consent_missing"]);
|
|
});
|
|
|
|
it("allows another try after a failure", () => {
|
|
const variant = variantFor([render("d", "plan1", { status: "failed", outputs: [] })]);
|
|
expect(variantState(variant)).toBe("failed");
|
|
expect(canApprove(variant)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("polling", () => {
|
|
it("polls only while a render is queued, rendering, or gating", () => {
|
|
expect(isRenderActive(null)).toBe(false);
|
|
for (const status of ["queued", "rendering", "gating"] as const) {
|
|
expect(shouldPollProject([render("r", "p", { status })])).toBe(true);
|
|
}
|
|
for (const status of ["done", "blocked", "failed", "canceled"] as const) {
|
|
expect(shouldPollProject([render("r", "p", { status })])).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("projectReadiness", () => {
|
|
const device = "src-phone";
|
|
|
|
it("waits for this device's uploads first", () => {
|
|
expect(
|
|
projectReadiness(
|
|
[
|
|
{ sourceId: device, storage: "remote", ingestState: "awaiting_upload" },
|
|
{ sourceId: device, storage: "uploaded", ingestState: "analyzed" },
|
|
],
|
|
true,
|
|
device,
|
|
),
|
|
).toBe("uploading");
|
|
});
|
|
|
|
it("treats items another device must upload as processing", () => {
|
|
const assets = [
|
|
{ sourceId: "src-mac", storage: "remote" as const, ingestState: "awaiting_upload" as const },
|
|
];
|
|
expect(projectReadiness(assets, true, device)).toBe("processing");
|
|
expect(uploadsFromDevice(assets, device)).toEqual([]);
|
|
expect(uploadsFromDevice(assets, null)).toEqual([]);
|
|
});
|
|
|
|
it("waits for analysis when it is available", () => {
|
|
const assets = [
|
|
{ sourceId: device, storage: "uploaded" as const, ingestState: "proxied" as const },
|
|
];
|
|
expect(projectReadiness(assets, true, device)).toBe("processing");
|
|
expect(projectReadiness(assets, false, device)).toBe("ready");
|
|
});
|
|
|
|
it("ignores failed items unless all failed", () => {
|
|
expect(
|
|
projectReadiness(
|
|
[
|
|
{ sourceId: device, storage: "uploaded", ingestState: "failed" },
|
|
{ sourceId: device, storage: "uploaded", ingestState: "analyzed" },
|
|
],
|
|
true,
|
|
device,
|
|
),
|
|
).toBe("ready");
|
|
expect(
|
|
projectReadiness(
|
|
[{ sourceId: device, storage: "uploaded", ingestState: "failed" }],
|
|
true,
|
|
device,
|
|
),
|
|
).toBe("failed");
|
|
expect(projectReadiness([], true, device)).toBe("processing");
|
|
});
|
|
});
|
|
|
|
describe("urls and names", () => {
|
|
it("builds render and thumbnail routes", () => {
|
|
expect(renderFileUrl("https://api.example.test/", "r1", "video.mp4")).toBe(
|
|
"https://api.example.test/api/media/renders/r1/files/video.mp4",
|
|
);
|
|
expect(renderFileUrl("https://api.example.test", "r1", "video.mp4", true)).toBe(
|
|
"https://api.example.test/api/media/renders/r1/files/video.mp4?download=1",
|
|
);
|
|
expect(assetThumbUrl("https://api.example.test", "a 1")).toBe(
|
|
"https://api.example.test/api/media/assets/a%201/thumb",
|
|
);
|
|
});
|
|
|
|
it("names shared files from the render id", () => {
|
|
expect(shareFileName("r1", "video.MP4")).toBe("render-r1.mp4");
|
|
expect(shareFileName("../r1", "video")).toBe("render-r1.mp4");
|
|
});
|
|
|
|
it("formats durations", () => {
|
|
expect(formatDuration(0)).toBe("0:00");
|
|
expect(formatDuration(65_400)).toBe("1:05");
|
|
expect(formatDuration(3_725_000)).toBe("1:02:05");
|
|
});
|
|
});
|
|
|
|
describe("mediaNotificationRoute", () => {
|
|
it("opens the project of a render push", () => {
|
|
expect(
|
|
mediaNotificationRoute({
|
|
kind: "media.render",
|
|
projectId: "p1",
|
|
renderId: "r1",
|
|
status: "done",
|
|
}),
|
|
).toEqual({ pathname: "/media-project", params: { projectId: "p1" } });
|
|
});
|
|
|
|
it("ignores other pushes and malformed ids", () => {
|
|
expect(mediaNotificationRoute(null)).toBeNull();
|
|
expect(mediaNotificationRoute({ kind: "completion", threadId: "t1" })).toBeNull();
|
|
expect(mediaNotificationRoute({ kind: "media.render", projectId: "../x" })).toBeNull();
|
|
expect(mediaNotificationRoute({ kind: "media.render" })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("libraryAccess", () => {
|
|
it("distinguishes full, limited, undecided, denied, and blocked access", () => {
|
|
expect(
|
|
libraryAccess({
|
|
granted: true,
|
|
canAskAgain: true,
|
|
status: "granted",
|
|
accessPrivileges: "all",
|
|
}),
|
|
).toBe("all");
|
|
expect(
|
|
libraryAccess({
|
|
granted: true,
|
|
canAskAgain: true,
|
|
status: "granted",
|
|
accessPrivileges: "limited",
|
|
}),
|
|
).toBe("limited");
|
|
expect(libraryAccess({ granted: false, canAskAgain: true, status: "undetermined" })).toBe(
|
|
"undetermined",
|
|
);
|
|
expect(libraryAccess({ granted: false, canAskAgain: true, status: "denied" })).toBe("denied");
|
|
expect(libraryAccess({ granted: false, canAskAgain: false, status: "denied" })).toBe("blocked");
|
|
});
|
|
});
|