Spaces:
Sleeping
Sleeping
Download apps/server/src/composition/composition-service.integration.test.ts from phucsd/Oloka-Video-Dev: direct link, hf CLI and curl.
- Browser
- Download file 37.2 kB
-
https://huggingface.co/spaces/phucsd/Oloka-Video-Dev/resolve/main/apps/server/src/composition/composition-service.integration.test.ts
- Command line
-
hf download hf://spaces/phucsd/Oloka-Video-Dev/apps/server/src/composition/composition-service.integration.test.ts
-
curl -L -o composition-service.integration.test.ts https://huggingface.co/spaces/phucsd/Oloka-Video-Dev/resolve/main/apps/server/src/composition/composition-service.integration.test.ts
37.2 kB
| import type { CompositionDocumentV1 } from "@oloka/contracts"; | |
| import { mkdtemp, rm } from "node:fs/promises"; | |
| import { tmpdir } from "node:os"; | |
| import { join } from "node:path"; | |
| import { pathToFileURL } from "node:url"; | |
| import { afterEach, describe, expect, it } from "vitest"; | |
| import { sha256Hex } from "../kernel/canonical-json.js"; | |
| import { SqliteSystemDatabase } from "../database/sqlite-system-database.js"; | |
| import type { AuthenticatedSession } from "../identity/identity-service.js"; | |
| import { BaselineQuotaPolicyResolver } from "../quota/quota-policy.js"; | |
| import { FilesystemObjectStorage } from "../storage/filesystem-object-storage.js"; | |
| import { CompositionService } from "./composition-service.js"; | |
| import { PreviewArtifactMaintenanceService } from "./preview-artifact-maintenance-service.js"; | |
| import { PreviewArtifactMaintenanceRuntime } from "./preview-artifact-maintenance-runtime.js"; | |
| const directories: string[] = []; | |
| afterEach(async () => { | |
| await Promise.all( | |
| directories | |
| .splice(0) | |
| .map((directory) => rm(directory, { recursive: true, force: true })), | |
| ); | |
| }); | |
| describe("Composition service", () => { | |
| it("creates an immutable version and advances the Project pointer atomically", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "composition-create-1", | |
| ); | |
| expect(created).toMatchObject({ | |
| replayed: false, | |
| composition: { versionNumber: 1, status: "valid" }, | |
| }); | |
| const state = fixture.database.transactions.run( | |
| "read", | |
| ({ database }) => ({ | |
| project: database | |
| .prepare( | |
| "SELECT current_composition_version_id, version FROM projects WHERE id = ?", | |
| ) | |
| .get(fixture.projectId), | |
| version: database | |
| .prepare( | |
| "SELECT version_number, canonical_hash_sha256, semantic_request_hash_sha256 FROM composition_versions", | |
| ) | |
| .get(), | |
| }), | |
| ); | |
| expect(state.project).toEqual({ | |
| current_composition_version_id: created.composition.id, | |
| version: 2, | |
| }); | |
| expect(state.version).toMatchObject({ | |
| version_number: 1, | |
| semantic_request_hash_sha256: null, | |
| }); | |
| expect(() => | |
| fixture.database.transactions.run("immediate", ({ database }) => | |
| database | |
| .prepare( | |
| "UPDATE composition_versions SET status = 'invalid' WHERE id = ?", | |
| ) | |
| .run(created.composition.id), | |
| ), | |
| ).toThrow(/immutable/i); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("reuses one immutable preview identity and cleans failed staging", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "preview-identity-create", | |
| ); | |
| const first = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-identity-a", | |
| ); | |
| const sameKey = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-identity-a", | |
| ); | |
| const differentKey = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-identity-b", | |
| ); | |
| expect(first.replayed).toBe(false); | |
| expect(sameKey.replayed).toBe(true); | |
| expect(differentKey.replayed).toBe(false); | |
| expect(differentKey.preview.id).toBe(first.preview.id); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare("SELECT COUNT(*) AS count FROM preview_artifacts") | |
| .get(), | |
| ), | |
| ).toEqual({ count: 1 }); | |
| expect( | |
| (await fixture.storage.listForReconciliation()).durable, | |
| ).toHaveLength(1); | |
| const derived = fixture.service.derive( | |
| fixture.actor, | |
| created.composition.id, | |
| { | |
| edits: [ | |
| { | |
| type: "sceneText", | |
| sceneId: compositionFixture().scenes[0]!.id, | |
| text: "Cleanup fixture", | |
| }, | |
| ], | |
| expectedProjectVersion: 2, | |
| }, | |
| "preview-cleanup-derive", | |
| ); | |
| const originalAppend = fixture.storage.appendAtOffset.bind( | |
| fixture.storage, | |
| ); | |
| fixture.storage.appendAtOffset = () => { | |
| throw new Error("injected preview append failure"); | |
| }; | |
| await expect( | |
| fixture.service.requestPreview( | |
| fixture.actor, | |
| derived.composition.id, | |
| "preview-cleanup-failure", | |
| ), | |
| ).rejects.toThrow(); | |
| fixture.storage.appendAtOffset = originalAppend; | |
| const leftovers = await fixture.storage.listForReconciliation(); | |
| expect(leftovers.staging).toHaveLength(0); | |
| expect(leftovers.durable).toHaveLength(1); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("keeps canonical, command, generator, and artifact hashes semantically distinct", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "semantic-lineage-create", | |
| ); | |
| const preview = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "semantic-lineage-preview", | |
| ); | |
| const hashes = fixture.database.transactions.run( | |
| "read", | |
| ({ database }) => ({ | |
| composition: database | |
| .prepare( | |
| "SELECT canonical_hash_sha256, semantic_request_hash_sha256 FROM composition_versions WHERE id = ?", | |
| ) | |
| .get(created.composition.id) as { | |
| canonical_hash_sha256: string; | |
| semantic_request_hash_sha256: string | null; | |
| }, | |
| command: database | |
| .prepare( | |
| "SELECT semantic_request_hash_sha256 FROM idempotency_records WHERE operation = 'composition.create'", | |
| ) | |
| .get() as { semantic_request_hash_sha256: string }, | |
| artifact: database | |
| .prepare( | |
| "SELECT byte_checksum_sha256 FROM preview_artifacts WHERE id = ?", | |
| ) | |
| .get(preview.preview.id) as { byte_checksum_sha256: string }, | |
| }), | |
| ); | |
| expect(hashes.composition.semantic_request_hash_sha256).toBeNull(); | |
| expect(hashes.composition.canonical_hash_sha256).toBe( | |
| created.composition.canonicalHashSha256, | |
| ); | |
| expect(hashes.artifact.byte_checksum_sha256).toBe( | |
| preview.preview.byteChecksumSha256, | |
| ); | |
| expect( | |
| new Set([ | |
| hashes.composition.canonical_hash_sha256, | |
| hashes.command.semantic_request_hash_sha256, | |
| hashes.artifact.byte_checksum_sha256, | |
| ]), | |
| ).toHaveLength(3); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("rejects 16 MiB plus one before opening an Asset stream", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const asset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000098", | |
| projectId: fixture.projectId, | |
| ownerUserId: fixture.actor.user.id, | |
| kind: "image", | |
| }); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare("UPDATE assets SET byte_size = ? WHERE id = ?") | |
| .run(16 * 1024 * 1024 + 1, asset.id); | |
| }); | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: withVisualAsset(asset.id), expectedProjectVersion: 1 }, | |
| "preview-limit-create", | |
| ); | |
| let opened = 0; | |
| const originalOpenRange = fixture.storage.openRange.bind(fixture.storage); | |
| fixture.storage.openRange = (...args) => { | |
| opened += 1; | |
| return originalOpenRange(...args); | |
| }; | |
| await expect( | |
| fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-limit-request", | |
| ), | |
| ).rejects.toThrow(/preview_embedded_media_limit/i); | |
| expect(opened).toBe(0); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("revalidates current Asset availability without mutating historical validation", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const asset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000099", | |
| projectId: fixture.projectId, | |
| ownerUserId: fixture.actor.user.id, | |
| kind: "image", | |
| }); | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { | |
| document: withVisualAsset(asset.id), | |
| expectedProjectVersion: 1, | |
| }, | |
| "validate-current-asset-create", | |
| ); | |
| const storedBefore = fixture.database.transactions.run( | |
| "read", | |
| ({ database }) => | |
| database | |
| .prepare( | |
| "SELECT validation_json FROM composition_versions WHERE id = ?", | |
| ) | |
| .get(created.composition.id) as { validation_json: string }, | |
| ); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE assets SET lifecycle_status = 'soft_deleted', deleted_at = 2, purge_after = 3, version = version + 1 WHERE id = ?", | |
| ) | |
| .run(asset.id); | |
| }); | |
| expect(() => | |
| fixture.service.validate( | |
| fixture.actor, | |
| created.composition.id, | |
| "validate-current-asset", | |
| ), | |
| ).toThrow(/composition_asset_unavailable/i); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare( | |
| "SELECT validation_json FROM composition_versions WHERE id = ?", | |
| ) | |
| .get(created.composition.id), | |
| ), | |
| ).toEqual(storedBefore); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("rejects one validation idempotency key reused for another Composition", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "validate-conflict-create", | |
| ); | |
| const derived = fixture.service.derive( | |
| fixture.actor, | |
| created.composition.id, | |
| { | |
| edits: [ | |
| { | |
| type: "sceneText", | |
| sceneId: compositionFixture().scenes[0]!.id, | |
| text: "A distinct canonical composition", | |
| }, | |
| ], | |
| expectedProjectVersion: 2, | |
| }, | |
| "validate-conflict-derive", | |
| ); | |
| expect( | |
| fixture.service.validate( | |
| fixture.actor, | |
| created.composition.id, | |
| "validate-conflict-key", | |
| ), | |
| ).toMatchObject({ replayed: false }); | |
| expect(() => | |
| fixture.service.validate( | |
| fixture.actor, | |
| derived.composition.id, | |
| "validate-conflict-key", | |
| ), | |
| ).toThrow(/composition_idempotency_conflict/i); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("purges expired preview bytes asynchronously while retaining row evidence", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "preview-retention-create", | |
| ); | |
| const preview = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-retention-request", | |
| ); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE preview_artifacts SET purge_after = created_at + 1 WHERE id = ?", | |
| ) | |
| .run(preview.preview.id); | |
| }); | |
| const maintenance = new PreviewArtifactMaintenanceService({ | |
| transactions: fixture.database.transactions, | |
| storage: fixture.storage, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| }); | |
| await expect(maintenance.run(1_700_000_000_002, 10)).resolves.toEqual({ | |
| scheduled: 1, | |
| purged: 1, | |
| failed: 0, | |
| }); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare("SELECT status FROM preview_artifacts WHERE id = ?") | |
| .get(preview.preview.id), | |
| ), | |
| ).toEqual({ status: "purged" }); | |
| expect((await fixture.storage.listForReconciliation()).durable).toEqual( | |
| [], | |
| ); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("resumes a scheduled preview purge after storage failure and restart", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "preview-purge-restart-create", | |
| ); | |
| const preview = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-purge-restart-request", | |
| ); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE preview_artifacts SET purge_after = created_at + 1 WHERE id = ?", | |
| ) | |
| .run(preview.preview.id); | |
| }); | |
| const originalDelete = fixture.storage.delete.bind(fixture.storage); | |
| fixture.storage.delete = () => | |
| Promise.reject(new Error("injected preview purge failure")); | |
| const firstMaintenance = new PreviewArtifactMaintenanceService({ | |
| transactions: fixture.database.transactions, | |
| storage: fixture.storage, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| }); | |
| await expect( | |
| firstMaintenance.run(1_700_000_000_002, 10), | |
| ).resolves.toEqual({ scheduled: 1, purged: 0, failed: 1 }); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare("SELECT status FROM preview_artifacts WHERE id = ?") | |
| .get(preview.preview.id), | |
| ), | |
| ).toEqual({ status: "purge_scheduled" }); | |
| fixture.storage.delete = originalDelete; | |
| const restartedMaintenance = new PreviewArtifactMaintenanceService({ | |
| transactions: fixture.database.transactions, | |
| storage: fixture.storage, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| }); | |
| await expect( | |
| restartedMaintenance.run(1_700_000_000_003, 10), | |
| ).resolves.toEqual({ scheduled: 0, purged: 1, failed: 0 }); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare( | |
| "SELECT COUNT(*) AS count, MIN(status) AS status FROM preview_artifacts", | |
| ) | |
| .get(), | |
| ), | |
| ).toEqual({ count: 1, status: "purged" }); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("rehydrates the same purged PreviewArtifact identity", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "preview-rehydrate-create", | |
| ); | |
| const first = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-rehydrate-first", | |
| ); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE preview_artifacts SET status = 'purge_scheduled' WHERE id = ?", | |
| ) | |
| .run(first.preview.id); | |
| }); | |
| const maintenance = new PreviewArtifactMaintenanceService({ | |
| transactions: fixture.database.transactions, | |
| storage: fixture.storage, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| }); | |
| await maintenance.run(1_700_000_000_010, 10); | |
| const rehydrated = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "preview-rehydrate-second", | |
| ); | |
| expect(rehydrated.preview).toMatchObject({ | |
| id: first.preview.id, | |
| byteChecksumSha256: first.preview.byteChecksumSha256, | |
| status: "ready", | |
| }); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare("SELECT COUNT(*) AS count FROM preview_artifacts") | |
| .get(), | |
| ), | |
| ).toEqual({ count: 1 }); | |
| expect( | |
| (await fixture.storage.listForReconciliation()).durable, | |
| ).toHaveLength(1); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("converges concurrent rehydration on one row and durable object", async () => { | |
| const fixture = await createFixture(); | |
| const secondDatabase = await SqliteSystemDatabase.connect( | |
| fixture.databaseUrl, | |
| { appKey: Buffer.alloc(32, 7) }, | |
| ); | |
| const secondStorage = new FilesystemObjectStorage(fixture.objectRoot); | |
| const secondService = new CompositionService({ | |
| transactions: secondDatabase.transactions, | |
| storage: secondStorage, | |
| applicationKey: Buffer.alloc(32, 8), | |
| clock: { now: () => 1_700_000_000_000 }, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| quotaPolicyResolver: new BaselineQuotaPolicyResolver(), | |
| }); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "concurrent-rehydrate-create", | |
| ); | |
| const first = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "concurrent-rehydrate-first", | |
| ); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE preview_artifacts SET status = 'purge_scheduled' WHERE id = ?", | |
| ) | |
| .run(first.preview.id); | |
| }); | |
| await new PreviewArtifactMaintenanceService({ | |
| transactions: fixture.database.transactions, | |
| storage: fixture.storage, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| }).run(1_700_000_000_010, 10); | |
| const [left, right] = await Promise.all([ | |
| fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "concurrent-rehydrate-left", | |
| ), | |
| secondService.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "concurrent-rehydrate-right", | |
| ), | |
| ]); | |
| expect(left.preview.id).toBe(first.preview.id); | |
| expect(right.preview.id).toBe(first.preview.id); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare( | |
| "SELECT COUNT(*) AS count, MIN(status) AS status FROM preview_artifacts", | |
| ) | |
| .get(), | |
| ), | |
| ).toEqual({ count: 1, status: "ready" }); | |
| expect( | |
| (await fixture.storage.listForReconciliation()).durable, | |
| ).toHaveLength(1); | |
| } finally { | |
| await secondStorage.close(); | |
| await secondDatabase.close(); | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("single-flights purge ticks so no stale delete removes rehydrated bytes", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "single-flight-purge-create", | |
| ); | |
| const first = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "single-flight-purge-preview", | |
| ); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE preview_artifacts SET purge_after = created_at + 1 WHERE id = ?", | |
| ) | |
| .run(first.preview.id); | |
| }); | |
| let deleteCalls = 0; | |
| let releaseDelete: (() => void) | undefined; | |
| let deletionStarted: (() => void) | undefined; | |
| const started = new Promise<void>((resolve) => { | |
| deletionStarted = resolve; | |
| }); | |
| const release = new Promise<void>((resolve) => { | |
| releaseDelete = resolve; | |
| }); | |
| const originalDelete = fixture.storage.delete.bind(fixture.storage); | |
| fixture.storage.delete = async (...args) => { | |
| deleteCalls += 1; | |
| deletionStarted?.(); | |
| await release; | |
| return originalDelete(...args); | |
| }; | |
| const runtime = new PreviewArtifactMaintenanceRuntime( | |
| new PreviewArtifactMaintenanceService({ | |
| transactions: fixture.database.transactions, | |
| storage: fixture.storage, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| }), | |
| { | |
| pollIntervalMs: 60_000, | |
| clock: { now: () => 1_700_000_000_010 }, | |
| }, | |
| ); | |
| const passA = runtime.start(); | |
| await started; | |
| const passB = runtime.runOnce(); | |
| await Promise.resolve(); | |
| expect(deleteCalls).toBe(1); | |
| releaseDelete?.(); | |
| await Promise.all([passA, passB]); | |
| const rehydrated = await fixture.service.requestPreview( | |
| fixture.actor, | |
| created.composition.id, | |
| "single-flight-purge-rehydrate", | |
| ); | |
| await runtime.stop(); | |
| await Promise.resolve(); | |
| const state = fixture.database.transactions.run( | |
| "read", | |
| ({ database }) => ({ | |
| preview: database | |
| .prepare( | |
| "SELECT COUNT(*) AS count, MIN(status) AS status, MIN(storage_key) AS storage_key, MIN(byte_checksum_sha256) AS byte_checksum_sha256 FROM preview_artifacts", | |
| ) | |
| .get() as { | |
| count: number; | |
| status: string; | |
| storage_key: string; | |
| byte_checksum_sha256: string; | |
| }, | |
| purgeAudits: database | |
| .prepare( | |
| "SELECT COUNT(*) AS count FROM audit_events WHERE action = 'preview.purged'", | |
| ) | |
| .get(), | |
| }), | |
| ); | |
| expect(state.preview).toMatchObject({ count: 1, status: "ready" }); | |
| expect(state.purgeAudits).toEqual({ count: 1 }); | |
| expect(deleteCalls).toBe(1); | |
| expect(rehydrated.preview.id).toBe(first.preview.id); | |
| expect(await fixture.storage.createHash(state.preview.storage_key)).toBe( | |
| state.preview.byte_checksum_sha256, | |
| ); | |
| expect( | |
| (await fixture.storage.listForReconciliation()).durable, | |
| ).toHaveLength(1); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("fails closed for unauthorized, unavailable, mismatched, over-quota, and deleted inputs", async () => { | |
| const fixture = await createFixture(); | |
| try { | |
| const otherProjectId = "00000000-0000-4000-8000-000000000003"; | |
| const otherUserId = "00000000-0000-4000-8000-000000000004"; | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| `INSERT INTO users | |
| (id,email_normalized,display_name,role,status,approved_at,created_at,updated_at,version) | |
| VALUES (?,?,'Other','member','active',1,1,1,1)`, | |
| ) | |
| .run(otherUserId, "other@example.test"); | |
| database | |
| .prepare( | |
| `INSERT INTO projects | |
| (id,owner_user_id,name,description,favorite,status,created_at,updated_at,version) | |
| VALUES (?,?,'Other project',NULL,0,'active',1,1,1)`, | |
| ) | |
| .run(otherProjectId, otherUserId); | |
| }); | |
| const crossProjectAsset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000001", | |
| projectId: otherProjectId, | |
| ownerUserId: otherUserId, | |
| kind: "image", | |
| }); | |
| const crossProjectDocument = withVisualAsset(crossProjectAsset.id); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: crossProjectDocument, expectedProjectVersion: 1 }, | |
| "asset-cross-project-create", | |
| ), | |
| ).toThrow(/asset_unavailable/i); | |
| const crossOwnerAsset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000002", | |
| projectId: fixture.projectId, | |
| ownerUserId: otherUserId, | |
| kind: "image", | |
| }); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { | |
| document: withVisualAsset(crossOwnerAsset.id), | |
| expectedProjectVersion: 1, | |
| }, | |
| "asset-cross-owner-create", | |
| ), | |
| ).toThrow(/asset_unavailable/i); | |
| const processingAsset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000003", | |
| projectId: fixture.projectId, | |
| ownerUserId: fixture.actor.user.id, | |
| kind: "image", | |
| ingestionStatus: "processing", | |
| }); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { | |
| document: withVisualAsset(processingAsset.id), | |
| expectedProjectVersion: 1, | |
| }, | |
| "asset-processing-create", | |
| ), | |
| ).toThrow(/asset_unavailable/i); | |
| const deletedAsset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000004", | |
| projectId: fixture.projectId, | |
| ownerUserId: fixture.actor.user.id, | |
| kind: "image", | |
| lifecycleStatus: "soft_deleted", | |
| }); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { | |
| document: withVisualAsset(deletedAsset.id), | |
| expectedProjectVersion: 1, | |
| }, | |
| "asset-deleted-create", | |
| ), | |
| ).toThrow(/asset_unavailable/i); | |
| const audioAsset = await insertAsset(fixture, { | |
| id: "30000000-0000-4000-8000-000000000005", | |
| projectId: fixture.projectId, | |
| ownerUserId: fixture.actor.user.id, | |
| kind: "audio", | |
| }); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { | |
| document: withVisualAsset(audioAsset.id), | |
| expectedProjectVersion: 1, | |
| }, | |
| "asset-kind-mismatch-create", | |
| ), | |
| ).toThrow(/composition_asset_usage_invalid/i); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { | |
| document: { ...compositionFixture(), durationMs: 61_000 }, | |
| expectedProjectVersion: 1, | |
| }, | |
| "composition-over-quota", | |
| ), | |
| ).toThrow(/duration_quota_exceeded/i); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| "UPDATE projects SET status='soft_deleted', deleted_at=2, purge_after=3, version=2 WHERE id=?", | |
| ) | |
| .run(fixture.projectId); | |
| }); | |
| expect(() => | |
| fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 2 }, | |
| "deleted-project-create", | |
| ), | |
| ).toThrow(/project_not_active/i); | |
| } finally { | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| it("serializes stale derives and converges preview identity across independent connections", async () => { | |
| const fixture = await createFixture(); | |
| const secondDatabase = await SqliteSystemDatabase.connect( | |
| fixture.databaseUrl, | |
| { | |
| appKey: Buffer.alloc(32, 7), | |
| }, | |
| ); | |
| const secondStorage = new FilesystemObjectStorage(fixture.objectRoot); | |
| const secondService = new CompositionService({ | |
| transactions: secondDatabase.transactions, | |
| storage: secondStorage, | |
| applicationKey: Buffer.alloc(32, 8), | |
| clock: { now: () => 1_700_000_000_000 }, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++secondId).padStart(12, "0")}`, | |
| }, | |
| quotaPolicyResolver: new BaselineQuotaPolicyResolver(), | |
| }); | |
| try { | |
| const created = fixture.service.create( | |
| fixture.actor, | |
| fixture.projectId, | |
| { document: compositionFixture(), expectedProjectVersion: 1 }, | |
| "concurrency-create", | |
| ); | |
| const edits = { | |
| edits: [ | |
| { | |
| type: "sceneText" as const, | |
| sceneId: compositionFixture().scenes[0]!.id, | |
| text: "Concurrent derive", | |
| }, | |
| ], | |
| expectedProjectVersion: 2, | |
| }; | |
| const firstDerive = fixture.service.derive( | |
| fixture.actor, | |
| created.composition.id, | |
| edits, | |
| "concurrency-derive-a", | |
| ); | |
| expect(() => | |
| secondService.derive( | |
| fixture.actor, | |
| created.composition.id, | |
| edits, | |
| "concurrency-derive-b", | |
| ), | |
| ).toThrow(/version_stale|parent_stale/i); | |
| expect(firstDerive.composition.versionNumber).toBe(2); | |
| const [firstPreview, secondPreview] = await Promise.all([ | |
| fixture.service.requestPreview( | |
| fixture.actor, | |
| firstDerive.composition.id, | |
| "concurrency-preview-a", | |
| ), | |
| secondService.requestPreview( | |
| fixture.actor, | |
| firstDerive.composition.id, | |
| "concurrency-preview-b", | |
| ), | |
| ]); | |
| expect(firstPreview.preview.id).toBe(secondPreview.preview.id); | |
| expect( | |
| fixture.database.transactions.run("read", ({ database }) => | |
| database | |
| .prepare("SELECT COUNT(*) AS count FROM preview_artifacts") | |
| .get(), | |
| ), | |
| ).toEqual({ count: 1 }); | |
| } finally { | |
| await secondStorage.close(); | |
| await secondDatabase.close(); | |
| await fixture.storage.close(); | |
| await fixture.database.close(); | |
| } | |
| }); | |
| }); | |
| let secondId = 20_000; | |
| async function createFixture() { | |
| const directory = await mkdtemp(join(tmpdir(), "oloka-composition-")); | |
| directories.push(directory); | |
| const database = await SqliteSystemDatabase.connect( | |
| pathToFileURL(join(directory, "database.sqlite")).href, | |
| { appKey: Buffer.alloc(32, 7) }, | |
| ); | |
| await database.migrate(); | |
| const actor: AuthenticatedSession = { | |
| sessionId: "00000000-0000-4000-8000-000000000011", | |
| user: { | |
| id: "00000000-0000-4000-8000-000000000001", | |
| email: "owner@example.test", | |
| displayName: "Owner", | |
| avatarUrl: null, | |
| role: "member", | |
| status: "active", | |
| version: 1, | |
| }, | |
| }; | |
| const projectId = "00000000-0000-4000-8000-000000000002"; | |
| database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| `INSERT INTO users | |
| (id,email_normalized,display_name,role,status,approved_at,created_at,updated_at,version) | |
| VALUES (?,?,?,'member','active',1,1,1,1)`, | |
| ) | |
| .run(actor.user.id, actor.user.email, actor.user.displayName); | |
| database | |
| .prepare( | |
| `INSERT INTO projects | |
| (id,owner_user_id,name,description,favorite,status,created_at,updated_at,version) | |
| VALUES (?,?,?,NULL,0,'active',1,1,1)`, | |
| ) | |
| .run(projectId, actor.user.id, "Project"); | |
| }); | |
| let id = 100; | |
| const storage = new FilesystemObjectStorage(join(directory, "objects")); | |
| const service = new CompositionService({ | |
| transactions: database.transactions, | |
| storage, | |
| applicationKey: Buffer.alloc(32, 8), | |
| clock: { now: () => 1_700_000_000_000 }, | |
| idGenerator: { | |
| generate: () => | |
| `00000000-0000-4000-8000-${String(++id).padStart(12, "0")}`, | |
| }, | |
| quotaPolicyResolver: new BaselineQuotaPolicyResolver(), | |
| }); | |
| return { | |
| actor, | |
| database, | |
| databaseUrl: pathToFileURL(join(directory, "database.sqlite")).href, | |
| objectRoot: join(directory, "objects"), | |
| projectId, | |
| service, | |
| storage, | |
| }; | |
| } | |
| async function insertAsset( | |
| fixture: Awaited<ReturnType<typeof createFixture>>, | |
| options: { | |
| id: string; | |
| projectId: string; | |
| ownerUserId: string; | |
| kind: "image" | "audio"; | |
| ingestionStatus?: "ready" | "processing"; | |
| lifecycleStatus?: "active" | "soft_deleted"; | |
| }, | |
| ) { | |
| const bytes = Buffer.from("asset-bytes"); | |
| const checksum = sha256Hex(bytes); | |
| const storageKey = `v1/${options.id.slice(0, 2)}/${options.id}`; | |
| await fixture.storage.stage(`stage-${options.id}`); | |
| await fixture.storage.appendAtOffset(`stage-${options.id}`, 0, bytes); | |
| await fixture.storage.finalize(`stage-${options.id}`, storageKey); | |
| fixture.database.transactions.run("immediate", ({ database }) => { | |
| database | |
| .prepare( | |
| `INSERT INTO assets | |
| (id,project_id,owner_user_id,original_filename,kind,declared_mime,verified_mime,storage_key,byte_size,byte_checksum_sha256,metadata_json,ingestion_status,lifecycle_status,created_at,updated_at,deleted_at,purge_after,version) | |
| VALUES (?,?,?,'asset.bin',?, ?, ?, ?, ?, ?, '{}', ?, ?, 1, 1, ?, ?, 1)`, | |
| ) | |
| .run( | |
| options.id, | |
| options.projectId, | |
| options.ownerUserId, | |
| options.kind, | |
| options.kind === "audio" ? "audio/mpeg" : "image/png", | |
| options.kind === "audio" ? "audio/mpeg" : "image/png", | |
| storageKey, | |
| bytes.length, | |
| checksum, | |
| options.ingestionStatus ?? "ready", | |
| options.lifecycleStatus ?? "active", | |
| options.lifecycleStatus === "soft_deleted" ? 2 : null, | |
| options.lifecycleStatus === "soft_deleted" ? 3 : null, | |
| ); | |
| }); | |
| return { id: options.id }; | |
| } | |
| function withVisualAsset(assetId: string): CompositionDocumentV1 { | |
| const document = compositionFixture(); | |
| return { | |
| ...document, | |
| scenes: document.scenes.map((scene) => ({ | |
| ...scene, | |
| assetReferences: [{ assetId, usage: "visual" }], | |
| })), | |
| }; | |
| } | |
| function compositionFixture(): CompositionDocumentV1 { | |
| return { | |
| schemaVersion: 1, | |
| compositionId: "10000000-0000-4000-8000-000000000001", | |
| name: "Bản dựng tiếng Việt", | |
| aspectRatio: "16:9", | |
| width: 1920, | |
| height: 1080, | |
| fps: 30, | |
| durationMs: 4_000, | |
| background: "#102030", | |
| scenes: [ | |
| { | |
| id: "20000000-0000-4000-8000-000000000001", | |
| order: 0, | |
| startMs: 0, | |
| durationMs: 4_000, | |
| text: "Xin chào Việt Nam", | |
| narration: null, | |
| assetReferences: [], | |
| style: { | |
| layout: "center", | |
| foreground: "#ffffff", | |
| accent: "#ffd23f", | |
| paddingPercent: 8, | |
| gapPercent: 4, | |
| }, | |
| tracks: [], | |
| }, | |
| ], | |
| voiceConfig: null, | |
| captionConfig: { | |
| enabled: true, | |
| preset: "Clean", | |
| fontToken: "oloka-sans-v1", | |
| safeMarginPercent: 8, | |
| maxLines: 2, | |
| }, | |
| bgmConfig: null, | |
| manifests: { | |
| dependency: { schemaVersion: 1, hashSha256: "a".repeat(64) }, | |
| asset: { schemaVersion: 1, hashSha256: "b".repeat(64) }, | |
| font: { schemaVersion: 1, hashSha256: "c".repeat(64) }, | |
| caption: { schemaVersion: 1, hashSha256: "d".repeat(64) }, | |
| }, | |
| runtimeVersions: { | |
| materializer: "client-placeholder", | |
| renderer: "client-placeholder", | |
| renderProtocol: 1, | |
| hyperframes: "0.7.104", | |
| hyperframesRuntimeSha256: "e".repeat(64), | |
| templateRegistry: "client-placeholder", | |
| }, | |
| metadata: { | |
| locale: "vi-VN", | |
| title: "Bản dựng", | |
| safeAreaMode: "action", | |
| }, | |
| }; | |
| } | |