import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { http } from 'msw' import { server } from '../../../../tests/msw/server' import { jsonResponse, errorResponse } from '../../../../tests/msw/helpers' import { renderWithProviders, screen, waitFor, userEvent } from '../../../../tests/helpers/render' import { seedBearer, clearBearer } from '../../../../tests/helpers/auth' import { AdminPage } from '..' // v2 admin — AI Recognition Engine panel. Covers GET → render telemetry, // edit value via stepper / input, APPLY → PATCH, RESET → discards draft, // PATCH 500 → inline error. // // Both AI and GPS panels render APPLY buttons; AI is the first one in DOM // order. We pick [0] from getAllByRole rather than coupling to internal markup. function aiApplyButton(): HTMLElement { return screen.getAllByRole('button', { name: /apply/i })[0] } function aiResetButton(): HTMLElement { return screen.getByRole('button', { name: /reset/i }) } beforeEach(() => { seedBearer() }) afterEach(() => { clearBearer() }) describe('AdminPage — AI Recognition Engine', () => { it('renders initial settings + telemetry from GET /api/admin/ai-settings', async () => { renderWithProviders() expect(await screen.findByText('YOLOV8-X · CKPT-241')).toBeInTheDocument() expect(screen.getByDisplayValue('4')).toBeInTheDocument() expect(screen.getByDisplayValue('25')).toBeInTheDocument() }) it('APPLY sends PATCH with edited settings and reflects telemetry refresh', async () => { const calls: { body: unknown }[] = [] server.use( http.patch('/api/admin/ai-settings', async ({ request }) => { const body = await request.json() calls.push({ body }) return jsonResponse({ settings: { framesToRecognize: 8, minSecondsBetween: 2, minConfidence: 25 }, telemetry: { model: 'YOLOV8-X', checkpoint: 'CKPT-242', lastRunAt: '2026-05-18T12:00:00Z', frames: 99, avgConfidence: 80, }, }) }), ) renderWithProviders() await screen.findByText('YOLOV8-X · CKPT-241') const framesInput = screen.getByDisplayValue('4') as HTMLInputElement await userEvent.clear(framesInput) await userEvent.type(framesInput, '8') await userEvent.click(aiApplyButton()) await waitFor(() => expect(calls.length).toBe(1)) expect((calls[0].body as { framesToRecognize: number }).framesToRecognize).toBe(8) expect(await screen.findByText(/CKPT-242/)).toBeInTheDocument() }) it('RESET reverts draft to the last persisted value (no PATCH)', async () => { const patchCalls: unknown[] = [] server.use( http.patch('/api/admin/ai-settings', () => { patchCalls.push({}) return jsonResponse({}) }), ) renderWithProviders() await screen.findByText('YOLOV8-X · CKPT-241') const framesInput = screen.getByDisplayValue('4') as HTMLInputElement await userEvent.clear(framesInput) await userEvent.type(framesInput, '9') expect(screen.getByDisplayValue('9')).toBeInTheDocument() await userEvent.click(aiResetButton()) expect(screen.getByDisplayValue('4')).toBeInTheDocument() expect(patchCalls.length).toBe(0) }) it('PATCH 500 surfaces an inline error', async () => { server.use( http.patch('/api/admin/ai-settings', () => errorResponse(500, 'boom')), ) renderWithProviders() await screen.findByText('YOLOV8-X · CKPT-241') await userEvent.click(aiApplyButton()) const alert = await screen.findByRole('alert') expect(alert.textContent ?? '').toMatch(/failed to save ai/i) }) })