The desktop Photos sync needs to map awaiting assets back to Photos items, query only awaiting uploads, and report deletions without a full relisting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
274 lines
7.4 KiB
TypeScript
274 lines
7.4 KiB
TypeScript
import type { ClientMediaItem } from "@rakazo/contracts";
|
|
import { describe, expect, it } from "vitest";
|
|
import type {
|
|
MediaAsset as MediaAssetRow,
|
|
MediaMark as MediaMarkRow,
|
|
MediaRender as MediaRenderRow,
|
|
MediaUpload as MediaUploadRow,
|
|
} from "./client.js";
|
|
import {
|
|
clientMediaItemToSourceItem,
|
|
decodeKeysetCursor,
|
|
encodeKeysetCursor,
|
|
InvalidMediaInputError,
|
|
likeContains,
|
|
mapMediaAsset,
|
|
mapMediaMark,
|
|
mapMediaRender,
|
|
mapMediaSegment,
|
|
mapMediaUpload,
|
|
pageLimit,
|
|
toPage,
|
|
} from "./media.js";
|
|
|
|
const at = new Date("2026-01-01T08:00:00.000Z");
|
|
|
|
describe("media keyset cursors", () => {
|
|
it("round-trips dated and undated positions as opaque base64url", () => {
|
|
for (const cursor of [
|
|
{ at, id: "asset-1" },
|
|
{ at: null, id: "asset-2" },
|
|
]) {
|
|
const token = encodeKeysetCursor(cursor);
|
|
expect(token).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
expect(decodeKeysetCursor(token)).toEqual(cursor);
|
|
}
|
|
});
|
|
|
|
it("rejects tampered or malformed cursors", () => {
|
|
const bad = [
|
|
"not json",
|
|
Buffer.from("{}").toString("base64url"),
|
|
Buffer.from(JSON.stringify(["2026-01-01T00:00:00Z"])).toString("base64url"),
|
|
Buffer.from(JSON.stringify(["yesterday", "id"])).toString("base64url"),
|
|
Buffer.from(JSON.stringify([null, ""])).toString("base64url"),
|
|
Buffer.from(JSON.stringify([42, "id"])).toString("base64url"),
|
|
];
|
|
for (const token of bad) {
|
|
expect(() => decodeKeysetCursor(token), token).toThrow(InvalidMediaInputError);
|
|
}
|
|
});
|
|
|
|
it("slices a limit+1 result into a page with the last row as cursor", () => {
|
|
const rows = [
|
|
{ id: "c", at },
|
|
{ id: "b", at: null },
|
|
{ id: "a", at: null },
|
|
];
|
|
const page = toPage(
|
|
rows,
|
|
2,
|
|
(row) => ({ at: row.at, id: row.id }),
|
|
(row) => row.id,
|
|
);
|
|
expect(page.items).toEqual(["c", "b"]);
|
|
expect(decodeKeysetCursor(page.nextCursor as string)).toEqual({ at: null, id: "b" });
|
|
expect(
|
|
toPage(
|
|
rows,
|
|
3,
|
|
(row) => ({ at: row.at, id: row.id }),
|
|
(row) => row.id,
|
|
).nextCursor,
|
|
).toBe(null);
|
|
});
|
|
|
|
it("clamps page sizes and escapes LIKE wildcards", () => {
|
|
expect(pageLimit(undefined)).toBe(100);
|
|
expect(pageLimit(0)).toBe(1);
|
|
expect(pageLimit(10_000)).toBe(500);
|
|
expect(likeContains("50%_off\\")).toBe("%50\\%\\_off\\\\%");
|
|
});
|
|
});
|
|
|
|
describe("media DTO mappers", () => {
|
|
const asset: MediaAssetRow = {
|
|
id: "asset-1",
|
|
spaceId: "space-1",
|
|
sourceId: "source-1",
|
|
externalId: "a.mp4",
|
|
revision: "r1",
|
|
contentHash: "hash",
|
|
kind: "video",
|
|
storage: "referenced",
|
|
storagePath: "originals/a.mp4",
|
|
fileName: "a.mp4",
|
|
sizeBytes: 10n,
|
|
capturedAt: at,
|
|
durationMs: 1000,
|
|
width: 1920,
|
|
height: 1080,
|
|
fps: 30,
|
|
codec: "h264",
|
|
orientation: 0,
|
|
gpsLat: 48.21,
|
|
gpsLon: 16.37,
|
|
deviceModel: "Camera",
|
|
albumNames: ["Trip"],
|
|
isFavorite: true,
|
|
proxyPath: "proxy.mp4",
|
|
thumbPath: null,
|
|
spritePath: null,
|
|
qualityScore: 0.5,
|
|
analysisVersion: 1,
|
|
ingestState: "proxied",
|
|
ingestError: null,
|
|
createdAt: at,
|
|
updatedAt: at,
|
|
};
|
|
|
|
it("maps assets without leaking paths or hashes", () => {
|
|
const dto = mapMediaAsset(asset);
|
|
expect(dto).toEqual({
|
|
id: "asset-1",
|
|
sourceId: "source-1",
|
|
externalId: asset.externalId,
|
|
kind: "video",
|
|
storage: "referenced",
|
|
capturedAt: at.toISOString(),
|
|
durationMs: 1000,
|
|
width: 1920,
|
|
height: 1080,
|
|
fps: 30,
|
|
gpsLat: 48.21,
|
|
gpsLon: 16.37,
|
|
deviceModel: "Camera",
|
|
albumNames: ["Trip"],
|
|
isFavorite: true,
|
|
ingestState: "proxied",
|
|
hasProxy: true,
|
|
hasThumbnail: false,
|
|
qualityScore: 0.5,
|
|
createdAt: at.toISOString(),
|
|
});
|
|
expect(JSON.stringify(dto)).not.toContain("proxy.mp4");
|
|
expect(JSON.stringify(dto)).not.toContain("originals");
|
|
});
|
|
|
|
it("keeps only numeric segment scores", () => {
|
|
const segment = mapMediaSegment({
|
|
id: "seg-1",
|
|
spaceId: "space-1",
|
|
assetId: "asset-1",
|
|
kind: "shot",
|
|
startMs: 0,
|
|
endMs: 100,
|
|
scores: { motion: 0.4, faces: "many", sharpness: null },
|
|
teaserScore: null,
|
|
transcriptExcerpt: null,
|
|
labels: [],
|
|
paidOnly: true,
|
|
analysisVersion: 1,
|
|
createdAt: at,
|
|
});
|
|
expect(segment.scores).toEqual({ motion: 0.4 });
|
|
expect(segment.paidOnly).toBe(true);
|
|
});
|
|
|
|
it("drops internal render output paths and invalid reports", () => {
|
|
const row: MediaRenderRow = {
|
|
id: "render-1",
|
|
spaceId: "space-1",
|
|
projectId: "project-1",
|
|
planId: "plan-1",
|
|
profileId: "example-short-vertical",
|
|
quality: "draft",
|
|
status: "done",
|
|
progress: 1.5,
|
|
outputs: [
|
|
{ kind: "video", artifactId: null, fileName: "a.mp4", sizeBytes: 1, path: "/abs/a.mp4" },
|
|
{ kind: "nonsense" },
|
|
],
|
|
gateReport: { passed: "maybe" },
|
|
ffprobe: null,
|
|
autoFixRound: 0,
|
|
error: null,
|
|
createdBy: "user-1",
|
|
createdAt: at,
|
|
updatedAt: at,
|
|
};
|
|
const dto = mapMediaRender(row);
|
|
expect(dto.progress).toBe(1);
|
|
expect(dto.outputs).toEqual([
|
|
{ kind: "video", artifactId: null, fileName: "a.mp4", sizeBytes: 1 },
|
|
]);
|
|
expect(dto.gateReport).toBeNull();
|
|
});
|
|
|
|
it("sorts upload chunks and converts sizes", () => {
|
|
const row: MediaUploadRow = {
|
|
id: "upload-1",
|
|
spaceId: "space-1",
|
|
assetId: "asset-1",
|
|
userId: "user-1",
|
|
sizeBytes: 21n * 1024n * 1024n * 1024n,
|
|
chunkBytes: 8,
|
|
sha256: "a".repeat(64),
|
|
fileName: "a.mov",
|
|
receivedChunks: [3, 0, 2],
|
|
status: "open",
|
|
createdAt: at,
|
|
updatedAt: at,
|
|
};
|
|
expect(mapMediaUpload(row)).toEqual({
|
|
id: "upload-1",
|
|
assetId: "asset-1",
|
|
sizeBytes: 21 * 1024 * 1024 * 1024,
|
|
chunkBytes: 8,
|
|
receivedChunks: [0, 2, 3],
|
|
status: "open",
|
|
});
|
|
});
|
|
|
|
it("maps mark prompts and drops malformed ones", () => {
|
|
const row: MediaMarkRow = {
|
|
id: "mark-1",
|
|
spaceId: "space-1",
|
|
kind: "tattoo",
|
|
label: null,
|
|
personId: null,
|
|
assetId: "asset-1",
|
|
prompt: { tMs: 10, point: { x: 0.5, y: 0.5 } },
|
|
treatment: "blur",
|
|
maskDir: "masks/mark-1",
|
|
qualityScore: null,
|
|
suggestedFallback: null,
|
|
createdAt: at,
|
|
updatedAt: at,
|
|
};
|
|
expect(mapMediaMark(row).prompt).toEqual({ tMs: 10, point: { x: 0.5, y: 0.5 } });
|
|
expect(mapMediaMark({ ...row, prompt: { tMs: -1 } }).prompt).toBeNull();
|
|
expect(mapMediaMark(row)).not.toHaveProperty("maskDir");
|
|
});
|
|
});
|
|
|
|
describe("client media items", () => {
|
|
const item: ClientMediaItem = {
|
|
externalId: "local-1",
|
|
kind: "video",
|
|
capturedAt: at.toISOString(),
|
|
durationMs: 1000,
|
|
width: 1080,
|
|
height: 1920,
|
|
fps: 30,
|
|
gpsLat: 48.2085,
|
|
gpsLon: 16.3721,
|
|
deviceModel: "Phone",
|
|
albumNames: ["Trip"],
|
|
isFavorite: false,
|
|
fileName: "IMG_0001.MOV",
|
|
};
|
|
|
|
it("derives a revision that changes with any metadata change", () => {
|
|
const base = clientMediaItemToSourceItem(item);
|
|
expect(base).toMatchObject({ externalId: "local-1", capturedAt: at, sizeBytes: null });
|
|
expect(clientMediaItemToSourceItem({ ...item }).revision).toBe(base.revision);
|
|
expect(clientMediaItemToSourceItem({ ...item, isFavorite: true }).revision).not.toBe(
|
|
base.revision,
|
|
);
|
|
expect(clientMediaItemToSourceItem({ ...item, albumNames: [] }).revision).not.toBe(
|
|
base.revision,
|
|
);
|
|
});
|
|
});
|