import { act, type ReactElement } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { useHarnessSelection, type HarnessSelectionValue } from './use-harness-selection'; function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }); } interface Scenario { harnesses?: unknown; catalog?: { body: unknown; status?: number }; selection?: unknown; /** When set, the PUT resolves only when this is called (for race tests). */ deferPut?: boolean; } interface Deferred { promise: Promise; resolve: (value: T) => void; } function defer(): Deferred { let resolve!: (value: T) => void; const promise = new Promise((r) => { resolve = r; }); return { promise, resolve }; } let putBodies: unknown[] = []; let putDeferred: Deferred | null = null; function installFetch(scenario: Scenario): ReturnType { putBodies = []; putDeferred = scenario.deferPut ? defer() : null; const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => { const url = String(input); const method = String(init?.method ?? 'GET').toUpperCase(); if (url === '/api/harnesses') return json(scenario.harnesses ?? []); if (url.startsWith('/api/harnesses/') && url.endsWith('/catalog')) { const spec = scenario.catalog ?? { body: { harnessId: 'pi', version: '1', fingerprint: 'f', models: [] }, }; return json(spec.body, spec.status ?? 200); } if (url === '/api/chat/preferences/selection' && method === 'GET') { return json({ selection: scenario.selection ?? null }); } if (url === '/api/chat/preferences/selection' && method === 'PUT') { putBodies.push(JSON.parse(String(init?.body))); const ok = json({ selection: JSON.parse(String(init?.body)) }); if (putDeferred) return putDeferred.promise; return ok; } return new Response('not found', { status: 404 }); }); vi.stubGlobal('fetch', fetchMock); return fetchMock; } let latest: HarnessSelectionValue | null = null; function Probe(): ReactElement | null { latest = useHarnessSelection(); return null; } let root: Root | null; let container: HTMLElement; beforeAll(() => { Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { configurable: true, value: true, }); }); afterAll(() => { Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT'); }); beforeEach(() => { latest = null; container = document.createElement('div'); document.body.append(container); root = createRoot(container); }); afterEach(async () => { await act(async () => { root?.unmount(); }); document.body.replaceChildren(); vi.unstubAllGlobals(); }); async function mount(): Promise { await act(async () => { root?.render(); }); await flush(); } async function flush(times = 5): Promise { for (let i = 0; i < times; i += 1) { await act(async () => { await Promise.resolve(); }); } } function value(): HarnessSelectionValue { if (!latest) throw new Error('hook value not captured'); return latest; } const PI_CATALOG = { harnessId: 'pi', version: '2026-08-11', fingerprint: 'fp', models: [ { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5', displayName: 'GPT-5', reasoningCapability: true, inputTypes: ['text'], authState: 'ready', availability: 'available', }, { harnessId: 'pi', providerId: 'anthropic', modelId: 'claude', displayName: 'Claude', reasoningCapability: true, inputTypes: ['text'], authState: 'ready', availability: 'available', }, ], }; describe('useHarnessSelection', () => { it('loads harnesses and, once a harness is chosen, the model options come only from its catalog', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: null, }); await mount(); expect(value().harnesses).toEqual([{ id: 'pi', displayName: 'Pi', capabilities: [] }]); expect(value().catalog).toBeNull(); await act(async () => { value().selectHarness('pi'); }); await flush(); expect(value().catalog?.harnessId).toBe('pi'); expect(value().catalog?.models.map((m) => m.modelId)).toEqual(['gpt-5', 'claude']); }); it('does not auto-select any catalog row when there is no persisted selection (no first-row fallback)', async () => { const fetchMock = installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: null, }); await mount(); await act(async () => { value().selectHarness('pi'); }); await flush(); expect(value().modelId).toBe(''); expect(value().persistedSelection).toBeNull(); expect(value().canSend).toBe(false); // Nothing was persisted — no PUT fired for an unset selection. const putCalls = fetchMock.mock.calls.filter( (c) => String((c[1] as RequestInit)?.method).toUpperCase() === 'PUT', ); expect(putCalls).toHaveLength(0); }); it('persists the structured tuple and only enables send AFTER the PUT resolves (no race ahead of persistence)', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: null, deferPut: true, }); await mount(); await act(async () => { value().selectHarness('pi'); }); await flush(); await act(async () => { value().selectProvider('openai'); }); await act(async () => { value().selectModel('openai', 'gpt-5'); }); await flush(); // PUT is in flight (deferred) — send MUST NOT be enabled yet. expect(value().canSend).toBe(false); await act(async () => { putDeferred?.resolve( json({ selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' } }), ); }); await flush(); expect(putBodies).toContainEqual({ harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' }); expect(value().persistedSelection).toEqual({ harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5', }); expect(value().canSend).toBe(true); // Task Five: the composer sends the nested `persistedSelection` tuple directly. // The Task-Four compat flat `projection` ({provider, modelId}) is removed — the // harnessId must never be dropped on the way to the wire. expect('projection' in value()).toBe(false); }); it('keeps a stale/unavailable persisted selection visibly displayed rather than silently dropping it', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: { harnessId: 'pi', providerId: 'openai', modelId: 'retired-model' }, }); await mount(); // The persisted tuple is displayed even though its model is gone from the catalog. expect(value().persistedSelection).toEqual({ harnessId: 'pi', providerId: 'openai', modelId: 'retired-model', }); expect(value().modelId).toBe('retired-model'); expect(value().isStale).toBe(true); // A stale model is not a valid catalog option, so send stays disabled. expect(value().canSend).toBe(false); }); it('disables send for an empty catalog (no viable model) and never fabricates one', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: { harnessId: 'pi', version: '1', fingerprint: 'f', models: [] } }, selection: null, }); await mount(); await act(async () => { value().selectHarness('pi'); }); await flush(); expect(value().catalog?.models ?? []).toHaveLength(0); expect(value().canSend).toBe(false); }); it('marks the catalog unavailable and disables send when the catalog request 404s', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: { code: 'adapter_unavailable', message: 'x', harnessId: 'pi' }, status: 404, }, selection: null, }); await mount(); await act(async () => { value().selectHarness('pi'); }); await flush(); expect(value().catalogUnavailable).toBe(true); expect(value().canSend).toBe(false); }); it('on a 422 persist, keeps the requested tuple visible, surfaces a typed error, and leaves send disabled', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: { ...PI_CATALOG, models: [{ ...PI_CATALOG.models[0], availability: 'unavailable' }], }, }, selection: null, }); // Override PUT to 422. const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => { const url = String(input); const method = String(init?.method ?? 'GET').toUpperCase(); if (url === '/api/harnesses') return json([{ id: 'pi', displayName: 'Pi', capabilities: [] }]); if (url.endsWith('/catalog')) return json(PI_CATALOG); if (url === '/api/chat/preferences/selection' && method === 'GET') return json({ selection: null }); if (url === '/api/chat/preferences/selection' && method === 'PUT') { return json( { code: 'model_unavailable', message: 'nope', selection: { harnessId: 'a', providerId: 'b', modelId: 'c' }, }, 422, ); } return new Response('nf', { status: 404 }); }); vi.stubGlobal('fetch', fetchMock); await mount(); await act(async () => { value().selectHarness('pi'); }); await flush(); await act(async () => { value().selectProvider('openai'); }); await act(async () => { value().selectModel('openai', 'gpt-5'); }); await flush(); expect(value().modelId).toBe('gpt-5'); expect(value().persistError?.code).toBe('model_unavailable'); expect(value().persistError?.requested).toEqual({ harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5', }); expect(value().persistedSelection).toBeNull(); expect(value().canSend).toBe(false); }); it('invalidates the model on a provider change and keeps send disabled until the new tuple persists', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: null, }); await mount(); await act(async () => { value().selectHarness('pi'); }); await flush(); await act(async () => { value().selectProvider('openai'); }); await act(async () => { value().selectModel('openai', 'gpt-5'); }); await flush(); // A valid provider-A tuple has persisted. expect(value().canSend).toBe(true); expect(value().persistedSelection).toEqual({ harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5', }); // Switching provider clears the model that no longer belongs to it. await act(async () => { value().selectProvider('anthropic'); }); expect(value().modelId).toBe(''); expect(value().canSend).toBe(false); // Send stays disabled until the new exact provider-B tuple persists. await act(async () => { value().selectModel('anthropic', 'claude'); }); await flush(); expect(value().canSend).toBe(true); expect(value().persistedSelection).toEqual({ harnessId: 'pi', providerId: 'anthropic', modelId: 'claude', }); // Task Five: no compat flat projection — the nested persistedSelection is the wire tuple. expect('projection' in value()).toBe(false); }); it('does not enable send on a model pick until the PUT for that exact new tuple resolves', async () => { installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' }, deferPut: true, }); await mount(); // The persisted, in-catalog tuple is sendable after mount (no PUT needed). expect(value().canSend).toBe(true); await act(async () => { value().selectProvider('anthropic'); }); expect(value().modelId).toBe(''); expect(value().canSend).toBe(false); await act(async () => { value().selectModel('anthropic', 'claude'); }); await flush(); // PUT for the new tuple is still in flight — send MUST stay disabled. expect(value().canSend).toBe(false); await act(async () => { putDeferred?.resolve( json({ selection: { harnessId: 'pi', providerId: 'anthropic', modelId: 'claude' } }), ); }); await flush(); expect(value().canSend).toBe(true); // Task Five: no compat flat projection — the nested persistedSelection is the wire tuple. expect('projection' in value()).toBe(false); }); it('never requests any /api/providers* endpoint across the whole flow', async () => { const fetchMock = installFetch({ harnesses: [{ id: 'pi', displayName: 'Pi', capabilities: [] }], catalog: { body: PI_CATALOG }, selection: { harnessId: 'pi', providerId: 'openai', modelId: 'gpt-5' }, }); await mount(); await act(async () => { value().selectProvider('anthropic'); }); await act(async () => { value().selectModel('anthropic', 'claude'); }); await flush(); for (const call of fetchMock.mock.calls) { expect(String(call[0])).not.toContain('/api/providers'); } }); });