summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations/__tests__
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-27 01:21:20 +0200
committercashmere <cashmere@autistici.org>2026-07-27 01:21:20 +0200
commitd55d2583a71ae35fa8d0d1bfbec975e67e66aafa (patch)
treec7a612f24b12c8602d7fbf139ac2709e4f2f4d9c /apps/web/src/services/storage/migrations/__tests__
parent2a372fdec256b8be890e181c221c2a012ead9efa (diff)
downloadopencut-d55d2583a71ae35fa8d0d1bfbec975e67e66aafa.tar.gz
feat: git-like project versioning (v1)
Linear per-project history per notes/project-versioning.md: named checkpoints (thumbnail via renderer.captureFrame) plus auto-checkpoints on save (60s throttle, ring of 20, named never rotate). Restore writes a safety checkpoint first, then swaps the document outside undo/redo. Media assets referenced by any version are pinned against deletion. Migration v31-v32 stamps the schema; the version store lives in its own IDB database with an in-memory fallback for tests. Versions popover in the editor header for jump/rename/delete. 12 new tests.
Diffstat (limited to 'apps/web/src/services/storage/migrations/__tests__')
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v31-to-v32.test.ts65
1 files changed, 65 insertions, 0 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/v31-to-v32.test.ts b/apps/web/src/services/storage/migrations/__tests__/v31-to-v32.test.ts
new file mode 100644
index 00000000..e3f1af8c
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/__tests__/v31-to-v32.test.ts
@@ -0,0 +1,65 @@
+import { describe, expect, test } from "bun:test";
+import { transformProjectV31ToV32 } from "../transformers/v31-to-v32";
+import { asRecord, asRecordArray } from "./helpers";
+
+describe("V31 to V32 Migration", () => {
+ test("stamps a v31 project as v32 without changing its content", () => {
+ const result = transformProjectV31ToV32({
+ project: {
+ id: "project-v31",
+ version: 31,
+ metadata: { id: "project-v31", name: "My project" },
+ scenes: [
+ {
+ id: "scene-1",
+ tracks: {
+ main: {
+ id: "track-1",
+ type: "video",
+ elements: [{ id: "elem-1", type: "video", mediaId: "media-1" }],
+ },
+ overlay: [],
+ audio: [],
+ },
+ },
+ ],
+ },
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(32);
+ const scene = asRecordArray(result.project.scenes)[0];
+ const main = asRecord(asRecord(scene.tracks).main);
+ const element = asRecordArray(main.elements)[0];
+ expect(element).toMatchObject({
+ id: "elem-1",
+ type: "video",
+ mediaId: "media-1",
+ });
+ expect(asRecord(result.project.metadata).name).toBe("My project");
+ });
+
+ test("skips a project that is already v32", () => {
+ const project = { id: "p1", version: 32, scenes: [] };
+ const result = transformProjectV31ToV32({ project });
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v32");
+ expect(result.project).toBe(project);
+ });
+
+ test("skips a project that is not v31", () => {
+ const project = { id: "p1", version: 30, scenes: [] };
+ const result = transformProjectV31ToV32({ project });
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("not v31");
+ expect(result.project).toBe(project);
+ });
+
+ test("skips a project without an id", () => {
+ const project = { version: 31, scenes: [] };
+ const result = transformProjectV31ToV32({ project });
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ expect(result.project).toBe(project);
+ });
+});