import { describe, expect, it, vi } from "vitest"; import { registerToolcraftRendererPipeline, type ToolcraftRendererPipelinePassContract, } from "./renderer-pipeline-registration"; import { createToolcraftRendererPipelineRuntime } from "./renderer-pipeline-runtime"; type Resource = Readonly<{ id: string }>; function createDeferred() { let resolve!: (value: T) => void; const promise = new Promise((resolvePromise) => { resolve = resolvePromise; }); return { promise, resolve }; } function createRegistration() { return registerToolcraftRendererPipeline<{ preview: ToolcraftRendererPipelinePassContract< string, Resource, readonly [resourceId: string] >; }>()({ interactionInvalidation: [], passes: [ { cacheKey: ["source.asset"], id: "preview", inputs: ["source.asset"], invalidatedBy: ["source.asset"], kind: "composite", lifecycle: { cache: "retained-resource", resourceScope: "source" }, output: "preview", quality: "full", runsOn: "main", }, ], runtimeId: "pass-generation-test-v1", }); } describe("renderer pipeline pass generations", () => { it("returns a plain explicit invalidation object", async () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); const invalidation = runtime.invalidatePass(registration.getPass("preview")); expect(invalidation).not.toBeInstanceOf(Promise); expect(Object.keys(invalidation)).toEqual(["cleanup"]); await expect(invalidation.cleanup).resolves.toBeUndefined(); }); it("binds resource admission to the execution generation", async () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); const preview = registration.getPass("preview"); const oldWorkStarted = createDeferred(); const continueOldWork = createDeferred(); const oldCreate = vi.fn(() => ({ id: "old" })); const newCreate = vi.fn(() => ({ id: "new" })); const oldExecution = runtime.runPass( preview, { "source.asset": "old" }, async (context) => { oldWorkStarted.resolve(undefined); await continueOldWork.promise; await expect( context.getOrCreateResource(["gpu"], oldCreate, vi.fn()), ).rejects.toThrow( 'Renderer pipeline pass "preview" generation is retired.', ); return "old-preview"; }, ); await oldWorkStarted.promise; const invalidation = runtime.invalidatePass(preview); const newExecution = runtime.runPass( preview, { "source.asset": "new" }, async (context) => { const resource = await context.getOrCreateResource( ["gpu"], newCreate, vi.fn(), ); return resource.id; }, ); continueOldWork.resolve(undefined); await expect(oldExecution).resolves.toBe("old-preview"); await expect(newExecution).resolves.toBe("new"); await expect(invalidation.cleanup).resolves.toBeUndefined(); expect(oldCreate).not.toHaveBeenCalled(); expect(newCreate).toHaveBeenCalledTimes(1); }); it("lets async work call the public invalidation path without a cycle", async () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); const preview = registration.getPass("preview"); let cleanup!: Promise; const execution = runtime.runPass( preview, { "source.asset": "source" }, async () => { await Promise.resolve(); const invalidation = runtime.invalidatePass(preview); expect(invalidation).not.toBeInstanceOf(Promise); cleanup = invalidation.cleanup; return "preview"; }, ); await expect(execution).resolves.toBe("preview"); await expect(cleanup).resolves.toBeUndefined(); }); it("conservatively swaps a whole generation for a matching source", async () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); const preview = registration.getPass("preview"); const work = vi.fn((value: string) => value); await runtime.runPass( preview, { "source.asset": "source-a" }, async (context) => { await context.getOrCreateResource( ["source-a"], () => ({ id: "source-a" }), vi.fn(), ); return work("a"); }, ); await runtime.runPass(preview, { "source.asset": "source-b" }, () => work("b")); await runtime.invalidateSource("source-a").cleanup; await runtime.runPass(preview, { "source.asset": "source-b" }, () => work("b2")); expect(work).toHaveBeenCalledTimes(3); }); });