From caf68872ecd5315f4e27d3afc9f3821fa4bc0616 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 01:48:17 -0300 Subject: [PATCH] test(playground): fix react-hooks/immutability lint in hook test harnesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add eslint-disable-next-line comment to the outer hookRef assignment inside mountHook in all 5 vitest UI test files — the assignment is intentional (test harness captures hook return via React ref) and safe. --- tests/unit/ui/use-improve-prompt.test.tsx | 32 ++++++------ tests/unit/ui/use-presets.test.tsx | 37 +++++++------- tests/unit/ui/use-stream-metrics.test.tsx | 52 ++++++++++--------- tests/unit/ui/use-structured-output.test.tsx | 54 ++++++++++---------- tests/unit/ui/use-tools-builder.test.tsx | 44 ++++++++-------- 5 files changed, 110 insertions(+), 109 deletions(-) diff --git a/tests/unit/ui/use-improve-prompt.test.tsx b/tests/unit/ui/use-improve-prompt.test.tsx index d81a106dd7..4193623348 100644 --- a/tests/unit/ui/use-improve-prompt.test.tsx +++ b/tests/unit/ui/use-improve-prompt.test.tsx @@ -2,8 +2,7 @@ // tests/unit/ui/use-improve-prompt.test.tsx // Runs via Vitest (vitest.config.ts) // Uses React DOM directly (no @testing-library/dom dep required). -import React from "react"; -import { act } from "react"; +import React, { act, useRef } from "react"; import { createRoot } from "react-dom/client"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { @@ -13,21 +12,22 @@ import type { ImprovePromptResult } from "../../../src/lib/playground/promptImpr // ─── Minimal hook test harness ──────────────────────────────────────────────── -interface HookCapture { - current: T; -} +type HookResult = { current: T }; function mountHook(useHook: () => T): { - result: HookCapture; + hookRef: HookResult; unmount: () => void; } { - const result: HookCapture = { current: undefined as unknown as T }; + const hookRef: HookResult = { current: undefined as unknown as T }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function HookComponent() { - result.current = useHook(); + const captureRef = useRef(undefined as unknown as T); + captureRef.current = useHook(); + // eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component + hookRef.current = captureRef.current; return null; } @@ -36,7 +36,7 @@ function mountHook(useHook: () => T): { }); return { - result, + hookRef, unmount: () => { act(() => root.unmount()); container.remove(); @@ -65,7 +65,7 @@ describe("useImprovePrompt", () => { }); it("starts with loading=false and no error", () => { - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); expect(result.current.loading).toBe(false); expect(result.current.error).toBeNull(); unmount(); @@ -81,7 +81,7 @@ describe("useImprovePrompt", () => { }), ); - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); let returned: ImprovePromptResult | null = null; await act(async () => { @@ -106,7 +106,7 @@ describe("useImprovePrompt", () => { }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); const req = { system: "You are helpful.", prompt: "Summarize this.", @@ -139,7 +139,7 @@ describe("useImprovePrompt", () => { }), ); - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); let returned: ImprovePromptResult | null = null; await act(async () => { @@ -158,7 +158,7 @@ describe("useImprovePrompt", () => { vi.fn().mockRejectedValue(new Error("Network failure")), ); - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); let returned: ImprovePromptResult | null = null; await act(async () => { @@ -189,7 +189,7 @@ describe("useImprovePrompt", () => { }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); // First call — fails await act(async () => { @@ -215,7 +215,7 @@ describe("useImprovePrompt", () => { }), ); - const { result, unmount } = mountHook(() => useImprovePrompt()); + const { hookRef: result, unmount } = mountHook(() => useImprovePrompt()); await act(async () => { await result.current.improve({ prompt: "test", model: "gpt-4o" }); diff --git a/tests/unit/ui/use-presets.test.tsx b/tests/unit/ui/use-presets.test.tsx index cff5f656b7..7439c46315 100644 --- a/tests/unit/ui/use-presets.test.tsx +++ b/tests/unit/ui/use-presets.test.tsx @@ -2,33 +2,32 @@ // tests/unit/ui/use-presets.test.tsx // Runs via Vitest (vitest.config.ts) // Uses React DOM directly (no @testing-library/dom dep required). -import React from "react"; -import { act } from "react"; +import React, { act, useRef } from "react"; import { createRoot } from "react-dom/client"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { usePresets, } from "../../../src/app/(dashboard)/dashboard/playground/hooks/usePresets"; -import type { UsePresets } from "../../../src/app/(dashboard)/dashboard/playground/hooks/usePresets"; import type { PlaygroundPresetListItem } from "../../../src/shared/schemas/playground"; // ─── Minimal hook test harness ──────────────────────────────────────────────── -interface HookCapture { - current: T; -} +type HookResult = { current: T }; function mountHook(useHook: () => T): { - result: HookCapture; + hookRef: HookResult; unmount: () => void; } { - const result: HookCapture = { current: undefined as unknown as T }; + const hookRef: HookResult = { current: undefined as unknown as T }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function HookComponent() { - result.current = useHook(); + const captureRef = useRef(undefined as unknown as T); + captureRef.current = useHook(); + // eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component + hookRef.current = captureRef.current; return null; } @@ -37,7 +36,7 @@ function mountHook(useHook: () => T): { }); return { - result, + hookRef, unmount: () => { act(() => root.unmount()); container.remove(); @@ -81,7 +80,7 @@ describe("usePresets", () => { const mockFetch = mockFetchOnce({ presets: [MOCK_PRESET] }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); await act(async () => { await result.current.list(); @@ -99,7 +98,7 @@ describe("usePresets", () => { const mockFetch = mockFetchOnce({ error: { message: "Unauthorized" } }, 401); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); await act(async () => { await result.current.list(); @@ -113,7 +112,7 @@ describe("usePresets", () => { it("sets error when fetch throws a network error", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error"))); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); await act(async () => { await result.current.list(); @@ -141,7 +140,7 @@ describe("usePresets", () => { }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); const input = { name: "Test Preset", endpoint: "chat.completions", @@ -176,7 +175,7 @@ describe("usePresets", () => { const mockFetch = mockFetchOnce({ error: { message: "Bad request" } }, 400); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); let created: PlaygroundPresetListItem | null = null; await act(async () => { @@ -206,7 +205,7 @@ describe("usePresets", () => { }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); const patch = { name: "Updated" }; let res: PlaygroundPresetListItem | null = null; @@ -240,7 +239,7 @@ describe("usePresets", () => { }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); await act(async () => { await result.current.remove(MOCK_PRESET.id); @@ -260,7 +259,7 @@ describe("usePresets", () => { const mockFetch = mockFetchOnce({ error: { message: "Not found" } }, 404); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); await act(async () => { await result.current.remove("nonexistent-id"); @@ -276,7 +275,7 @@ describe("usePresets", () => { const mockFetch = mockFetchOnce({ presets: [] }); vi.stubGlobal("fetch", mockFetch); - const { result, unmount } = mountHook(() => usePresets()); + const { hookRef: result, unmount } = mountHook(() => usePresets()); await act(async () => { await result.current.list(); diff --git a/tests/unit/ui/use-stream-metrics.test.tsx b/tests/unit/ui/use-stream-metrics.test.tsx index e6ef597390..7d5bedc67f 100644 --- a/tests/unit/ui/use-stream-metrics.test.tsx +++ b/tests/unit/ui/use-stream-metrics.test.tsx @@ -2,8 +2,7 @@ // tests/unit/ui/use-stream-metrics.test.tsx // Runs via Vitest (vitest.config.ts — includes tests/unit/**/*.test.tsx) // Uses React DOM directly (no @testing-library/dom dep required). -import React from "react"; -import { act } from "react"; +import React, { act, useRef } from "react"; import { createRoot } from "react-dom/client"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { @@ -11,23 +10,28 @@ import { } from "../../../src/app/(dashboard)/dashboard/playground/hooks/useStreamMetrics"; import type { UseStreamMetrics } from "../../../src/app/(dashboard)/dashboard/playground/hooks/useStreamMetrics"; -// ─── Minimal hook test harness (no @testing-library/dom) ───────────────────── +// ─── Minimal hook test harness ──────────────────────────────────────────────── +// Uses a React ref to capture hook values from inside the component — avoids +// react-hooks/immutability lint error (cannot write to outer const from inside component). -interface HookCapture { - current: T; -} +type HookResult = { current: T }; function mountHook(useHook: () => T): { - result: HookCapture; + hookRef: HookResult; unmount: () => void; } { - const result: HookCapture = { current: undefined as unknown as T }; + const hookRef: HookResult = { current: undefined as unknown as T }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function HookComponent() { - result.current = useHook(); + const captureRef = useRef(undefined as unknown as T); + captureRef.current = useHook(); + // Expose the captured value through the outer hookRef via the React ref. + // This is safe because captureRef lives inside the component. + // eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component + hookRef.current = captureRef.current; return null; } @@ -36,11 +40,9 @@ function mountHook(useHook: () => T): { }); return { - result, + hookRef, unmount: () => { - act(() => { - root.unmount(); - }); + act(() => root.unmount()); container.remove(); }, }; @@ -58,7 +60,7 @@ describe("useStreamMetrics", () => { }); it("starts with initial zero/null metrics", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); expect(result.current.metrics.ttftMs).toBeNull(); expect(result.current.metrics.totalMs).toBeNull(); expect(result.current.metrics.tps).toBeNull(); @@ -69,7 +71,7 @@ describe("useStreamMetrics", () => { }); it("start() resets timing refs and updates metrics state", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -84,7 +86,7 @@ describe("useStreamMetrics", () => { }); it("onFirstChunk() sets ttftMs relative to start", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -101,7 +103,7 @@ describe("useStreamMetrics", () => { }); it("onFirstChunk() is idempotent — only records first call", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -123,7 +125,7 @@ describe("useStreamMetrics", () => { }); it("finish() finalizes metrics with accumulated chunk tokens", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -135,7 +137,7 @@ describe("useStreamMetrics", () => { result.current.onFirstChunk(); }); - // onChunk does not flush to state — accumulates in ref + // onChunk accumulates in ref without flushing state result.current.onChunk(5); result.current.onChunk(5); result.current.onChunk(5); @@ -156,7 +158,7 @@ describe("useStreamMetrics", () => { }); it("finish(usage) overrides tokensIn + tokensOut with upstream values", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -185,7 +187,7 @@ describe("useStreamMetrics", () => { }); it("finish(usage) with only prompt_tokens partial override", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -212,7 +214,7 @@ describe("useStreamMetrics", () => { it("computes costUsd when pricing is provided", () => { const pricing = { inUsdPer1k: 0.003, outUsdPer1k: 0.015 }; - const { result, unmount } = mountHook(() => useStreamMetrics(pricing)); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics(pricing)); act(() => { vi.setSystemTime(1000); @@ -235,7 +237,7 @@ describe("useStreamMetrics", () => { }); it("costUsd is null when no pricing provided", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -251,7 +253,7 @@ describe("useStreamMetrics", () => { }); it("reset() zeros all metrics", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); @@ -281,7 +283,7 @@ describe("useStreamMetrics", () => { }); it("start() after previous run resets all accumulated state", () => { - const { result, unmount } = mountHook(() => useStreamMetrics()); + const { hookRef: result, unmount } = mountHook(() => useStreamMetrics()); act(() => { vi.setSystemTime(1000); diff --git a/tests/unit/ui/use-structured-output.test.tsx b/tests/unit/ui/use-structured-output.test.tsx index e81d7a5e6e..c9eb7748bb 100644 --- a/tests/unit/ui/use-structured-output.test.tsx +++ b/tests/unit/ui/use-structured-output.test.tsx @@ -2,8 +2,7 @@ // tests/unit/ui/use-structured-output.test.tsx // Runs via Vitest (vitest.config.ts) // Uses React DOM directly (no @testing-library/dom dep required). -import React from "react"; -import { act } from "react"; +import React, { act, useRef } from "react"; import { createRoot } from "react-dom/client"; import { describe, it, expect } from "vitest"; import { @@ -12,21 +11,22 @@ import { // ─── Minimal hook test harness ──────────────────────────────────────────────── -interface HookCapture { - current: T; -} +type HookResult = { current: T }; function mountHook(useHook: () => T): { - result: HookCapture; + hookRef: HookResult; unmount: () => void; } { - const result: HookCapture = { current: undefined as unknown as T }; + const hookRef: HookResult = { current: undefined as unknown as T }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function HookComponent() { - result.current = useHook(); + const captureRef = useRef(undefined as unknown as T); + captureRef.current = useHook(); + // eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component + hookRef.current = captureRef.current; return null; } @@ -35,7 +35,7 @@ function mountHook(useHook: () => T): { }); return { - result, + hookRef, unmount: () => { act(() => root.unmount()); container.remove(); @@ -73,7 +73,7 @@ const VALID_SCHEMA_NO_REQUIRED = { describe("useStructuredOutput", () => { describe("initial state", () => { it("starts with enabled=false, schema=null, error=null", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); expect(result.current.enabled).toBe(false); expect(result.current.schema).toBeNull(); expect(result.current.error).toBeNull(); @@ -83,7 +83,7 @@ describe("useStructuredOutput", () => { describe("setEnabled()", () => { it("sets enabled to true", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setEnabled(true); @@ -94,7 +94,7 @@ describe("useStructuredOutput", () => { }); it("toggles enabled back to false", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setEnabled(true); @@ -108,7 +108,7 @@ describe("useStructuredOutput", () => { describe("setSchema()", () => { it("accepts a valid schema and clears error", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA); @@ -120,7 +120,7 @@ describe("useStructuredOutput", () => { }); it("sets error when schema name is empty", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema({ name: "", schema: { type: "object" } }); @@ -132,7 +132,7 @@ describe("useStructuredOutput", () => { }); it("sets error when name is too long (>64 chars)", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema({ name: "a".repeat(65), schema: { type: "object" } }); @@ -143,7 +143,7 @@ describe("useStructuredOutput", () => { }); it("clears error after previously invalid schema when valid one is set", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema({ name: "", schema: {} }); @@ -159,7 +159,7 @@ describe("useStructuredOutput", () => { }); it("accepts schema with strict=false", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema({ ...VALID_SCHEMA, strict: false }); @@ -171,7 +171,7 @@ describe("useStructuredOutput", () => { }); it("accepts schema without strict field", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA_NO_REQUIRED); @@ -185,7 +185,7 @@ describe("useStructuredOutput", () => { describe("validateResponse()", () => { it("returns { valid: false, error } when no schema is set", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); const res = result.current.validateResponse({ temperature: 25, condition: "sunny" }); @@ -195,7 +195,7 @@ describe("useStructuredOutput", () => { }); it("parses JSON string and validates correctly", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA); @@ -209,7 +209,7 @@ describe("useStructuredOutput", () => { }); it("returns { valid: false } when content is not valid JSON string", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA); @@ -223,7 +223,7 @@ describe("useStructuredOutput", () => { }); it("validates object directly (no JSON.parse needed)", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA); @@ -235,7 +235,7 @@ describe("useStructuredOutput", () => { }); it("returns { valid: false } when required field is missing", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA); @@ -248,7 +248,7 @@ describe("useStructuredOutput", () => { }); it("returns { valid: false } when content is null", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA_NO_REQUIRED); @@ -260,7 +260,7 @@ describe("useStructuredOutput", () => { }); it("returns { valid: false } when content is an array", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA_NO_REQUIRED); @@ -272,7 +272,7 @@ describe("useStructuredOutput", () => { }); it("passes validation for schema without required field", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema(VALID_SCHEMA_NO_REQUIRED); @@ -284,7 +284,7 @@ describe("useStructuredOutput", () => { }); it("passes validation for schema with no properties field", () => { - const { result, unmount } = mountHook(() => useStructuredOutput()); + const { hookRef: result, unmount } = mountHook(() => useStructuredOutput()); act(() => { result.current.setSchema({ name: "AnyObj", schema: { type: "object" } }); diff --git a/tests/unit/ui/use-tools-builder.test.tsx b/tests/unit/ui/use-tools-builder.test.tsx index 5479165197..e7fa6dc73f 100644 --- a/tests/unit/ui/use-tools-builder.test.tsx +++ b/tests/unit/ui/use-tools-builder.test.tsx @@ -2,8 +2,7 @@ // tests/unit/ui/use-tools-builder.test.tsx // Runs via Vitest (vitest.config.ts) // Uses React DOM directly (no @testing-library/dom dep required). -import React from "react"; -import { act } from "react"; +import React, { act, useRef } from "react"; import { createRoot } from "react-dom/client"; import { describe, it, expect } from "vitest"; import { @@ -13,21 +12,22 @@ import type { ToolDefinition } from "../../../src/lib/playground/codeExport"; // ─── Minimal hook test harness ──────────────────────────────────────────────── -interface HookCapture { - current: T; -} +type HookResult = { current: T }; function mountHook(useHook: () => T): { - result: HookCapture; + hookRef: HookResult; unmount: () => void; } { - const result: HookCapture = { current: undefined as unknown as T }; + const hookRef: HookResult = { current: undefined as unknown as T }; const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); function HookComponent() { - result.current = useHook(); + const captureRef = useRef(undefined as unknown as T); + captureRef.current = useHook(); + // eslint-disable-next-line react-hooks/immutability -- test harness: intentionally writes to outer capture object from inside component + hookRef.current = captureRef.current; return null; } @@ -36,7 +36,7 @@ function mountHook(useHook: () => T): { }); return { - result, + hookRef, unmount: () => { act(() => root.unmount()); container.remove(); @@ -73,7 +73,7 @@ const VALID_TOOL_2: ToolDefinition = { describe("useToolsBuilder", () => { describe("initial state", () => { it("starts with empty tools and empty errors", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); expect(result.current.tools).toHaveLength(0); expect(result.current.errors.size).toBe(0); unmount(); @@ -82,7 +82,7 @@ describe("useToolsBuilder", () => { describe("add()", () => { it("returns { ok: false, error } when tool has empty name", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); const invalidTool = { type: "function" as const, @@ -101,7 +101,7 @@ describe("useToolsBuilder", () => { }); it("returns { ok: false } when type is not 'function'", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); const invalidTool = { type: "not-function" as unknown as "function", @@ -119,7 +119,7 @@ describe("useToolsBuilder", () => { }); it("adds a valid tool and returns { ok: true }", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); let outcome: ReturnType | undefined; act(() => { @@ -133,7 +133,7 @@ describe("useToolsBuilder", () => { }); it("adds multiple valid tools", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -145,7 +145,7 @@ describe("useToolsBuilder", () => { }); it("does not add to tools when validation fails", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -159,7 +159,7 @@ describe("useToolsBuilder", () => { describe("remove()", () => { it("removes tool at given index", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -176,7 +176,7 @@ describe("useToolsBuilder", () => { }); it("is a no-op when index is out of bounds", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -191,7 +191,7 @@ describe("useToolsBuilder", () => { }); it("re-indexes errors after remove", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -219,7 +219,7 @@ describe("useToolsBuilder", () => { describe("update()", () => { it("updates a tool at given index after validation", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -242,7 +242,7 @@ describe("useToolsBuilder", () => { }); it("returns { ok: false, error } and stores error when validation fails", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -264,7 +264,7 @@ describe("useToolsBuilder", () => { }); it("clears error for that index on successful update", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL); @@ -285,7 +285,7 @@ describe("useToolsBuilder", () => { describe("clear()", () => { it("removes all tools and clears all errors", () => { - const { result, unmount } = mountHook(() => useToolsBuilder()); + const { hookRef: result, unmount } = mountHook(() => useToolsBuilder()); act(() => { result.current.add(VALID_TOOL);