import * as React from "react"; import { act, cleanup, render, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { registerToolcraftRendererPipeline, type ToolcraftRendererPipelinePassContract, } from "../../rendering"; import { ToolcraftPipelineProvider, useToolcraftPipelineExportRetention, } from "./toolcraft-pipeline-context"; import { useToolcraftPipeline } from "./use-toolcraft-pipeline"; import { useToolcraftExportOwner } from "./toolcraft-export-context"; import { ToolcraftRoot } from "./toolcraft-root"; import { defineToolcraft } from "../../schema/define-toolcraft"; afterEach(cleanup); function registration(runtimeId: string) { return registerToolcraftRendererPipeline<{ output: ToolcraftRendererPipelinePassContract< string, { id: string; }, readonly [string] >; }>()({ interactionInvalidation: [], passes: [ { id: "output", inputs: ["source"], invalidatedBy: ["source"], cacheKey: ["source"], kind: "composite", lifecycle: { cache: "retained-resource", resourceScope: "renderer" }, output: "export", quality: "full", runsOn: "main", }, ], runtimeId, }); } describe("pipeline export retention integration", () => { it.each(["unmount", "replacement"])( "retains resources through provider %s until export settlement", async (mode) => { const first = registration("export-first"); const second = registration("export-second"); let lifetime!: NonNullable< ReturnType >; let client!: NonNullable>; function Probe() { lifetime = useToolcraftPipelineExportRetention()!; client = useToolcraftPipeline()!; return null; } const view = render( , ); const original = client; const cancel = vi.fn(); const dispose = vi.fn(); const release = lifetime.retain(cancel); await client.runPass( first.getPass("output"), { source: 1 }, async (context) => { await context.getOrCreateResource( ["gpu"], () => ({ id: "gpu" }), dispose, ); return "pixels"; }, ); if (mode === "replacement") { view.rerender( , ); expect(cancel).toHaveBeenCalledOnce(); expect(client).not.toBe(original); } else { view.unmount(); } await Promise.resolve(); expect(cancel).toHaveBeenCalledOnce(); expect(original.getSnapshot().disposed).toBe(false); expect(dispose).not.toHaveBeenCalled(); release(); release(); await waitFor(() => expect(dispose).toHaveBeenCalledOnce()); expect(original.getSnapshot().disposed).toBe(true); }, ); it("cancels a paused export when the root removes its pipeline registration", async () => { const pipeline = registration("removed-export"); const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: true }, panels: {}, }, modules: [], }); let owner!: ReturnType; let lifetime!: NonNullable< ReturnType >; function Probe() { owner = useToolcraftExportOwner(); lifetime = useToolcraftPipelineExportRetention()!; return null; } const view = render( , ); let resolve!: () => void; const held = new Promise((done) => { resolve = done; }); const downloaded = vi.fn(); const job = owner.start(async ({ signal }) => { const release = lifetime.retain(owner.cancel); try { await held; signal.throwIfAborted(); downloaded(); } finally { release(); } }); view.rerender( , ); if (job.status !== "started") throw new Error("Expected admission"); await act(async () => { resolve(); await expect(job.completion).resolves.toEqual({ status: "cancelled" }); }); expect(downloaded).not.toHaveBeenCalled(); expect(owner.getStatus().phase).toBe("idle"); }); });