import { act, renderHook } from "@testing-library/react"; import type * as React from "react"; import { describe, expect, it, vi } from "vitest"; import { createToolcraftRendererPipelineRuntime, registerToolcraftRendererPipeline, type ToolcraftRendererPipelinePassContract, } from "../../rendering"; import { ToolcraftPipelineContext } from "./toolcraft-pipeline-context"; import { useToolcraftPipelinePass } from "./use-toolcraft-pipeline-pass"; 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: [ { cacheKey: ["value"], id: "preview", inputs: ["value"], invalidatedBy: ["value"], kind: "composite", lifecycle: { cache: "memoized", resourceScope: "renderer" }, output: "preview", quality: "full", runsOn: "main", }, ], runtimeId: "pipeline-hook-test-v1", }); } describe("useToolcraftPipelinePass", () => { it("bridges a synchronous pipeline result into the initial committed state", () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); const compute = vi.fn(() => "model-a"); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); const { result } = renderHook( () => useToolcraftPipelinePass( registration.getPass("preview"), { value: "a" }, compute, ), { wrapper }, ); expect(result.current).toEqual({ result: "model-a", status: "success", }); expect(compute).toHaveBeenCalledTimes(1); }); it("does not restart a pass when an inline compute callback is recreated by its result render", () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); let computeCalls = 0; let renders = 0; const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); const { result } = renderHook( () => { renders += 1; return useToolcraftPipelinePass( registration.getPass("preview"), { value: "inline" }, () => { computeCalls += 1; return "model-inline"; }, ); }, { wrapper }, ); expect(result.current).toEqual({ result: "model-inline", status: "success", }); expect(computeCalls).toBe(1); expect(renders).toBeLessThanOrEqual(2); expect(runtime.getSnapshot().passes.preview.executions).toBe(1); }); it("computes synchronously without a registered pipeline client", () => { const registration = createRegistration(); const compute = () => "fallback-model"; const { result } = renderHook(() => useToolcraftPipelinePass( registration.getPass("preview"), { value: "fallback" }, compute, ), ); expect(result.current).toEqual({ result: "fallback-model", status: "success", }); }); it("suppresses stale async results after request identity changes", async () => { const registration = createRegistration(); const first = createDeferred(); const second = createDeferred(); const { result, rerender } = renderHook( ({ cacheInput, compute }) => useToolcraftPipelinePass( registration.getPass("preview"), cacheInput, compute, ), { initialProps: { cacheInput: { value: "a" }, compute: () => first.promise, }, }, ); rerender({ cacheInput: { value: "b" }, compute: () => second.promise, }); await act(async () => first.resolve("stale-model")); expect(result.current.status).toBe("pending"); await act(async () => second.resolve("current-model")); expect(result.current).toEqual({ result: "current-model", status: "success", }); }); it("returns an explicit error result for rejected work", async () => { const registration = createRegistration(); const failure = new Error("render failed"); const compute = () => Promise.reject(failure); const { result } = renderHook(() => useToolcraftPipelinePass( registration.getPass("preview"), { value: "failure" }, compute, ), ); await act(() => Promise.resolve()); expect(result.current).toEqual({ error: failure, status: "error", }); }); it("keeps the resolved model when compute identity changes without descriptor changes", async () => { const registration = createRegistration(); const runtime = createToolcraftRendererPipelineRuntime(registration); const firstCompute = vi.fn(() => "cached-model"); const secondCompute = vi.fn(() => "unexpected-model"); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); const { result, rerender } = renderHook( ({ cacheInput, compute }) => useToolcraftPipelinePass( registration.getPass("preview"), cacheInput, compute, ), { initialProps: { cacheInput: { value: "same" }, compute: firstCompute, }, wrapper, }, ); rerender({ cacheInput: { value: "same" }, compute: secondCompute, }); await act(() => Promise.resolve()); expect(result.current).toEqual({ result: "cached-model", status: "success", }); expect(firstCompute).toHaveBeenCalledTimes(1); expect(secondCompute).not.toHaveBeenCalled(); expect(runtime.getSnapshot().passes.preview.cacheHits).toBe(0); expect(runtime.getSnapshot().passes.preview.executions).toBe(1); }); });