import { afterEach, describe, expect, it, vi } from "vitest"; import { registerToolcraftRendererPipeline, type ToolcraftRendererPipelinePassContract, } from "./renderer-pipeline-registration"; import { createToolcraftRendererPipelineRuntimeOwner } from "./renderer-pipeline-runtime-owner"; import { getRetainedRendererPipelineRuntimeGenerationCount } from "./renderer-pipeline-runtime-owner-inspection"; function createDeferred() { let reject!: (error: unknown) => void; let resolve!: (value: T) => void; const promise = new Promise((resolvePromise, rejectPromise) => { reject = rejectPromise; resolve = resolvePromise; }); return { promise, reject, resolve }; } function createRegistration() { return registerToolcraftRendererPipeline<{ preview: ToolcraftRendererPipelinePassContract; }>()({ interactionInvalidation: [], passes: [ { id: "preview", inputs: [], invalidatedBy: [], kind: "composite", lifecycle: { cache: "none", resourceScope: "call" }, output: "preview", quality: "full", runsOn: "main", }, ], runtimeId: "owner-test-v1", }); } function createResourceRegistration() { return registerToolcraftRendererPipeline<{ preview: ToolcraftRendererPipelinePassContract< string, Readonly<{ id: string }>, readonly [resourceId: string] >; }>()({ interactionInvalidation: [], passes: [ { cacheKey: ["source"], id: "preview", inputs: ["source"], invalidatedBy: ["source"], kind: "composite", lifecycle: { cache: "retained-resource", resourceScope: "renderer" }, output: "preview", quality: "full", runsOn: "main", }, ], runtimeId: "owner-resource-test-v1", }); } let resourceSourceId = 0; async function createOwnedResource( owner: ReturnType, registration: ReturnType, id: string, dispose: (resource: Readonly<{ id: string }>) => PromiseLike | void, ): Promise { await owner.client.runPass( registration.getPass("preview"), { source: ++resourceSourceId }, async (context) => { await context.getOrCreateResource(["gpu"], () => ({ id }), dispose); return id; }, ); } afterEach(() => vi.unstubAllGlobals()); describe("Toolcraft renderer pipeline runtime owner", () => { it("holds a transient lease until lease-free async pass work settles", async () => { const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); const disposeResource = vi.fn(); await expect( owner.client.runPass( registration.getPass("preview"), { source: "async" }, async (context) => { await Promise.resolve(); const resource = await context.getOrCreateResource( ["gpu"], () => ({ id: "gpu" }), disposeResource, ); return resource.id; }, ), ).resolves.toBe("gpu"); await Promise.resolve(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(true); expect(disposeResource).toHaveBeenCalledTimes(1); }); it("holds transient leases through invalidation cleanup and subscription", async () => { const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); const disposal = createDeferred(); const releaseResourceOwner = owner.acquire(); await createOwnedResource(owner, registration, "gpu", () => disposal.promise); const invalidation = owner.client.invalidatePass( registration.getPass("preview"), ); releaseResourceOwner(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(false); disposal.resolve(undefined); await invalidation.cleanup; const unsubscribe = owner.client.subscribe(() => undefined); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(false); unsubscribe(); await Promise.resolve(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(true); }); it("releases settled runtime generations across repeated reconnects", async () => { const registration = createRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); for (let index = 0; index < 8; index += 1) { const release = owner.acquire(); await owner.client.runPass( registration.getPass("preview"), undefined, () => `preview-${index}`, ); release(); await Promise.resolve(); await Promise.resolve(); await vi.waitFor(() => expect( getRetainedRendererPipelineRuntimeGenerationCount(owner), ).toBe(0), ); } await expect(owner.dispose()).resolves.toBeUndefined(); }); it("retires a runtime created by a lease-free client operation", async () => { const registration = createRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); await owner.client.runPass( registration.getPass("preview"), undefined, () => "preview", ); await Promise.resolve(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(true); }); it("cancels a probe release when the same owner is immediately reacquired", async () => { const owner = createToolcraftRendererPipelineRuntimeOwner(createRegistration()); const releaseProbe = owner.acquire(); releaseProbe(); const releaseMounted = owner.acquire(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(false); releaseMounted(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(true); }); it("opens a fresh runtime generation after a completed last-lease retirement", async () => { const registration = createRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); const preview = registration.getPass("preview"); const releaseFirst = owner.acquire(); await owner.client.runPass(preview, undefined, () => "first"); releaseFirst(); await Promise.resolve(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(true); const releaseSecond = owner.acquire(); expect(owner.client.getSnapshot()).toMatchObject({ disposed: false }); expect(owner.client.getSnapshot().passes.preview.executions).toBe(0); await expect( owner.client.runPass(preview, undefined, () => "second"), ).resolves.toBe("second"); releaseSecond(); await Promise.resolve(); await Promise.resolve(); expect(owner.client.getSnapshot().disposed).toBe(true); }); it("reconnects while an older generation disposes and terminally awaits every generation", async () => { const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); const firstDisposal = createDeferred(); const secondDisposal = createDeferred(); const releaseFirst = owner.acquire(); await createOwnedResource(owner, registration, "first", () => firstDisposal.promise, ); releaseFirst(); await Promise.resolve(); const releaseSecond = owner.acquire(); await createOwnedResource(owner, registration, "second", () => secondDisposal.promise, ); const terminalDisposal = owner.dispose(); let terminalComplete = false; void terminalDisposal.then(() => { terminalComplete = true; }); await Promise.resolve(); expect(terminalComplete).toBe(false); firstDisposal.resolve(undefined); await Promise.resolve(); expect(terminalComplete).toBe(false); secondDisposal.resolve(undefined); await expect(terminalDisposal).resolves.toBeUndefined(); expect(terminalComplete).toBe(true); expect(() => owner.acquire()).toThrow( 'Renderer pipeline runtime owner "owner-resource-test-v1" is disposed.', ); releaseSecond(); }); it("disposes explicitly once and rejects later acquisition", async () => { const owner = createToolcraftRendererPipelineRuntimeOwner(createRegistration()); await Promise.all([owner.dispose(), owner.dispose()]); expect(owner.client.getSnapshot().disposed).toBe(true); expect(() => owner.acquire()).toThrow( 'Renderer pipeline runtime owner "owner-test-v1" is disposed.', ); expect(owner.client).not.toHaveProperty("dispose"); }); it("reports automatic disposal failures through the injected reporter", async () => { const failure = new Error("dispose failed"); const onDisposeError = vi.fn(); const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration, { onDisposeError, }); const release = owner.acquire(); await createOwnedResource(owner, registration, "gpu", () => Promise.reject(failure), ); release(); await vi.waitFor(() => expect(onDisposeError).toHaveBeenCalledTimes(1)); expect(onDisposeError.mock.calls[0]?.[0]).toBeInstanceOf(AggregateError); expect(owner.client.getSnapshot().disposed).toBe(true); }); it("reports an overlapping automatic and terminal cleanup failure once", async () => { const failure = new Error("dispose failed"); const disposal = createDeferred(); const onDisposeError = vi.fn(); const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration, { onDisposeError, }); const release = owner.acquire(); await createOwnedResource(owner, registration, "gpu", () => disposal.promise); release(); await Promise.resolve(); owner.disposeAutomatically(); disposal.reject(failure); await vi.waitFor(() => expect(onDisposeError).toHaveBeenCalledTimes(1)); await Promise.resolve(); expect(onDisposeError).toHaveBeenCalledTimes(1); }); it("uses reportError for automatic disposal failures by default", async () => { const reportError = vi.fn(); vi.stubGlobal("reportError", reportError); const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration); const release = owner.acquire(); await createOwnedResource(owner, registration, "gpu", () => Promise.reject(new Error("dispose failed")), ); release(); await vi.waitFor(() => expect(reportError).toHaveBeenCalledTimes(1)); }); it("preserves cleanup and injected reporter failures for the fallback reporter", async () => { const cleanupFailure = new Error("cleanup failed"); const reporterFailure = new Error("reporter failed"); const reportError = vi.fn(); vi.stubGlobal("reportError", reportError); const registration = createResourceRegistration(); const owner = createToolcraftRendererPipelineRuntimeOwner(registration, { onDisposeError: () => { throw reporterFailure; }, }); const release = owner.acquire(); await createOwnedResource(owner, registration, "gpu", () => Promise.reject(cleanupFailure), ); release(); await vi.waitFor(() => expect(reportError).toHaveBeenCalledTimes(1)); const reported = reportError.mock.calls[0]?.[0]; expect(reported).toBeInstanceOf(AggregateError); expect((reported as AggregateError).errors).toEqual([ expect.objectContaining({ errors: [ expect.objectContaining({ errors: [expect.objectContaining({ cause: cleanupFailure })], }), ], }), reporterFailure, ]); }); });