import { EventEmitter } from 'node:events'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { describe, expect, it, vi } from 'vitest'; import { createAiHardwareRouteHandler } from '@electron/api/routes/ai-hardware'; function request(method: string, body?: unknown, headers: Record = {}): IncomingMessage { const req = new EventEmitter(); const raw = body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body); Object.assign(req, { method, headers: { ...(raw === undefined ? {} : { 'content-length': String(Buffer.byteLength(raw)) }), ...headers }, [Symbol.asyncIterator]: async function* () { if (raw !== undefined) yield Buffer.from(raw); }, }); return req as IncomingMessage; } function response() { const chunks: string[] = []; const res = new EventEmitter(); Object.assign(res, { statusCode: 0, setHeader: vi.fn(), end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }), }); return { res: res as unknown as ServerResponse, get status() { return (res as { statusCode: number }).statusCode; }, json: () => JSON.parse(chunks.join('')) as Record, }; } const overview = { status: 'active', agents: [{ id: 'a-1', name: 'Desk', config_revision: 0 }], devices: [] }; function jsonResponse(value: unknown, init: ResponseInit = {}): Response { const headers = new Headers(init.headers); if (!headers.has('content-type')) headers.set('content-type', 'application/json'); return new Response(JSON.stringify(value), { ...init, headers }); } function setup(fetchImpl = vi.fn().mockResolvedValue(jsonResponse(overview))) { const getAccessToken = vi.fn().mockResolvedValue('secret-token'); const handler = createAiHardwareRouteHandler({ fetchImpl, getAccessToken, apiBaseUrl: 'https://square.example', randomUuid: () => '11111111-1111-4111-8111-111111111111', timeoutMs: 10, }); return { handler, fetchImpl, getAccessToken }; } async function invoke(handler: ReturnType, method: string, path: string, body?: unknown, headers?: Record) { const target = response(); const handled = await handler(request(method, body, headers), target.res, new URL(`http://localhost${path}`), {} as never); return { handled, ...target, payload: target.json() }; } describe('AI hardware Host API route', () => { it('projects overview DTOs and drops unexpected sensitive fields', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ ...overview, token: 'must-not-leak', agents: [{ ...overview.agents[0], external_agent_id: 'external' }], })); const { handler } = setup(fetchImpl); const result = await invoke(handler, 'GET', '/api/works/ai-hardware'); expect(result.status).toBe(200); expect(result.payload).toEqual({ success: true, data: overview }); expect(JSON.stringify(result.payload)).not.toContain('must-not-leak'); expect(JSON.stringify(result.payload)).not.toContain('external'); }); it('owns authorization and idempotency headers without forwarding renderer headers', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ id: 'd-1', agent_id: 'a-1', assignment_revision: 0 }, { status: 201 })); const { handler } = setup(fetchImpl); await invoke(handler, 'POST', '/api/works/ai-hardware/device-bindings', { activation_code: '123456', agent_id: 'a-1', client_operation_id: '22222222-2222-4222-8222-222222222222' }, { authorization: 'Bearer renderer-token', 'idempotency-key': 'renderer-key', }); const init = fetchImpl.mock.calls[0][1] as RequestInit; expect(init.headers).toMatchObject({ Authorization: 'Bearer secret-token', 'Idempotency-Key': 'makelore-22222222-2222-4222-8222-222222222222' }); expect(init.body).toBe(JSON.stringify({ activation_code: '123456', agent_id: 'a-1' })); expect(fetchImpl.mock.calls[0][0]).toBe('https://square.example/api/ai-hardware/device-bindings'); }); it('validates a strong ETag and returns its numeric revision in the envelope', async () => { const config = { id: 'a-1', name: 'Desk', config_revision: 0, system_prompt: null, lang_code: null, language: null, asr_model_id: null, vad_model_id: null, llm_model_id: null, slm_model_id: null, vllm_model_id: null, tts_model_id: null, tts_voice_id: null, tts_language: null, tts_volume: null, tts_rate: null, tts_pitch: null, mem_model_id: null, intent_model_id: null, chat_history_conf: null, }; const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(config, { headers: { etag: '"0"' } })); const { handler } = setup(fetchImpl); const result = await invoke(handler, 'GET', '/api/works/ai-hardware/agents/a-1'); expect(result.payload).toEqual({ success: true, data: config, revision: 0 }); }); it('accepts canonical weak numeric ETags for versioned responses and keeps outbound If-Match strong', async () => { const config = { id: 'a-1', name: 'Desk', config_revision: 0, system_prompt: null, lang_code: null, language: null, asr_model_id: null, vad_model_id: null, llm_model_id: null, slm_model_id: null, vllm_model_id: null, tts_model_id: null, tts_voice_id: null, tts_language: null, tts_volume: null, tts_rate: null, tts_pitch: null, mem_model_id: null, intent_model_id: null, chat_history_conf: null, }; const updatedConfig = { ...config, config_revision: 1, system_prompt: 'hello' }; const assignment = { id: 'd-1', agent_id: 'a-1', assignment_revision: 0 }; const updatedAssignment = { ...assignment, assignment_revision: 1 }; const fetchImpl = vi.fn() .mockResolvedValueOnce(jsonResponse(config, { headers: { etag: 'W/"0"' } })) .mockResolvedValueOnce(jsonResponse(updatedConfig, { headers: { etag: 'W/"1"' } })) .mockResolvedValueOnce(jsonResponse(assignment, { headers: { etag: 'W/"0"' } })) .mockResolvedValueOnce(jsonResponse(updatedAssignment, { headers: { etag: 'W/"1"' } })); const { handler } = setup(fetchImpl); const getConfig = await invoke(handler, 'GET', '/api/works/ai-hardware/agents/a-1'); expect(getConfig.payload).toEqual({ success: true, data: config, revision: 0 }); const patchConfig = await invoke(handler, 'PATCH', '/api/works/ai-hardware/agents/a-1', { revision: 0, system_prompt: 'hello', }); expect(patchConfig.payload).toEqual({ success: true, data: updatedConfig, revision: 1 }); expect((fetchImpl.mock.calls[1][1] as RequestInit).headers).toMatchObject({ 'If-Match': '"0"' }); const getAssignment = await invoke(handler, 'GET', '/api/works/ai-hardware/devices/d-1/agent-assignment'); expect(getAssignment.payload).toEqual({ success: true, data: assignment, revision: 0 }); const putAssignment = await invoke(handler, 'PUT', '/api/works/ai-hardware/devices/d-1/agent-assignment', { revision: 0, agent_id: 'a-1', }); expect(putAssignment.payload).toEqual({ success: true, data: updatedAssignment, revision: 1 }); expect((fetchImpl.mock.calls[3][1] as RequestInit).headers).toMatchObject({ 'If-Match': '"0"' }); }); it.each([ 'w/"0"', 'W/ "0"', 'W/"00"', 'W/"-1"', 'W/"1.0"', 'W/"revision"', 'W/"9007199254740992"', ])('rejects non-canonical weak ETag %s without exposing upstream data', async (etag) => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ id: 'd-1', agent_id: 'a-1', assignment_revision: 0, token: 'secret', }, { headers: { etag } })); const { handler } = setup(fetchImpl); const result = await invoke(handler, 'GET', '/api/works/ai-hardware/devices/d-1/agent-assignment'); expect(result.status).toBe(200); expect(result.payload).toMatchObject({ success: false, status: 502, code: 'AI_HARDWARE_INVALID_ETAG', retryable: true }); expect(JSON.stringify(result.payload)).not.toContain('secret'); }); it('rejects config and assignment DTO revisions that disagree with the canonical ETag', async () => { const config = { id: 'a-1', name: 'Desk', config_revision: 2, system_prompt: null, lang_code: null, language: null, asr_model_id: null, vad_model_id: null, llm_model_id: null, slm_model_id: null, vllm_model_id: null, tts_model_id: null, tts_voice_id: null, tts_language: null, tts_volume: null, tts_rate: null, tts_pitch: null, mem_model_id: null, intent_model_id: null, chat_history_conf: null, }; const configHandler = setup(vi.fn().mockResolvedValue(jsonResponse(config, { headers: { etag: 'W/"1"' } }))); const configResult = await invoke(configHandler.handler, 'GET', '/api/works/ai-hardware/agents/a-1'); expect(configResult.payload).toMatchObject({ success: false, status: 502, code: 'AI_HARDWARE_INVALID_RESPONSE' }); const deviceHandler = setup(vi.fn().mockResolvedValue(jsonResponse( { id: 'd-1', agent_id: 'a-1', assignment_revision: 3 }, { headers: { etag: 'W/"2"' } }, ))); const deviceResult = await invoke(deviceHandler.handler, 'GET', '/api/works/ai-hardware/devices/d-1/agent-assignment'); expect(deviceResult.payload).toMatchObject({ success: false, status: 502, code: 'AI_HARDWARE_INVALID_RESPONSE' }); }); it('constructs If-Match and a fresh idempotency key from a PATCH body revision', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ id: 'a-1', name: 'Desk', config_revision: 1, system_prompt: 'hello', lang_code: null, language: null, asr_model_id: null, vad_model_id: null, llm_model_id: null, slm_model_id: null, vllm_model_id: null, tts_model_id: null, tts_voice_id: null, tts_language: null, tts_volume: null, tts_rate: null, tts_pitch: null, mem_model_id: null, intent_model_id: null, chat_history_conf: null, }, { headers: { etag: '"1"' } })); const { handler } = setup(fetchImpl); await invoke(handler, 'PATCH', '/api/works/ai-hardware/agents/a-1', { revision: 0, system_prompt: 'hello' }); const init = fetchImpl.mock.calls[0][1] as RequestInit; expect(init.headers).toMatchObject({ 'If-Match': '"0"', 'Idempotency-Key': 'makelore-11111111-1111-4111-8111-111111111111' }); expect(init.body).toBe(JSON.stringify({ system_prompt: 'hello' })); }); it('accepts integer chat history and rejects invalid clear field combinations locally', async () => { const updated = { id: 'a-1', name: 'Desk', config_revision: 1, system_prompt: null, lang_code: null, language: null, asr_model_id: null, vad_model_id: null, llm_model_id: null, slm_model_id: null, vllm_model_id: null, tts_model_id: null, tts_voice_id: null, tts_language: null, tts_volume: -100, tts_rate: 0, tts_pitch: 100, mem_model_id: null, intent_model_id: null, chat_history_conf: 2, }; const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(updated, { headers: { etag: '"1"' } })); const { handler } = setup(fetchImpl); const accepted = await invoke(handler, 'PATCH', '/api/works/ai-hardware/agents/a-1', { revision: 0, chat_history_conf: 2 }); expect(accepted.payload).toEqual({ success: true, data: updated, revision: 1 }); expect((fetchImpl.mock.calls[0][1] as RequestInit).body).toBe(JSON.stringify({ chat_history_conf: 2 })); for (const body of [ { revision: 0, chat_history_conf: 3 }, { revision: 0, clear_fields: ['chat_history_conf'] }, { revision: 0, clear_fields: ['agent_name'] }, { revision: 0, clear_fields: ['system_prompt', 'system_prompt'] }, { revision: 0, system_prompt: 'set', clear_fields: ['system_prompt'] }, { revision: 0, system_prompt: ' ' }, { revision: 0, clear_fields: [] }, ]) { const rejected = await invoke(handler, 'PATCH', '/api/works/ai-hardware/agents/a-1', body); expect(rejected.payload).toMatchObject({ success: false, status: 400, code: 'AI_HARDWARE_INVALID_REQUEST' }); } expect(fetchImpl).toHaveBeenCalledTimes(1); }); it('retries one 401 with a forced token refresh and preserves idempotency', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(new Response(null, { status: 401 })) .mockResolvedValueOnce(jsonResponse({ id: 'a-1', name: 'Desk', config_revision: 0 }, { status: 201 })); const { handler, getAccessToken } = setup(fetchImpl); getAccessToken.mockResolvedValueOnce('old').mockResolvedValueOnce('new'); await invoke(handler, 'POST', '/api/works/ai-hardware/agents', { agent_name: 'Desk' }); expect(getAccessToken).toHaveBeenNthCalledWith(2, expect.objectContaining({ forceRefresh: true })); expect(fetchImpl).toHaveBeenCalledTimes(2); expect((fetchImpl.mock.calls[0][1] as RequestInit).headers).toMatchObject({ Authorization: 'Bearer old', 'Idempotency-Key': 'makelore-11111111-1111-4111-8111-111111111111' }); expect((fetchImpl.mock.calls[1][1] as RequestInit).headers).toMatchObject({ Authorization: 'Bearer new', 'Idempotency-Key': 'makelore-11111111-1111-4111-8111-111111111111' }); }); it('returns stable disabled and redacted error envelopes over local HTTP 200', async () => { const disabled = setup(vi.fn().mockResolvedValue(new Response('secret upstream body', { status: 404 }))); const result = await invoke(disabled.handler, 'GET', '/api/works/ai-hardware'); expect(result.status).toBe(200); expect(result.payload).toEqual({ success: false, status: 404, code: 'AI_HARDWARE_DISABLED', error: 'AI hardware module is not enabled', retryable: false }); const failed = setup(vi.fn().mockResolvedValue(new Response(JSON.stringify({ detail: { message: 'activation 123456 and token abc' } }), { status: 429, headers: { 'retry-after': '7' } }))); const failure = await invoke(failed.handler, 'POST', '/api/works/ai-hardware/agents', { agent_name: 'Desk' }); expect(failure.status).toBe(200); expect(failure.payload).toEqual({ success: false, status: 429, code: 'AI_HARDWARE_RATE_LIMITED', error: 'AI hardware service is busy; retry later', retryable: true, retry_after_seconds: 7, operation_id: '11111111-1111-4111-8111-111111111111' }); expect(JSON.stringify(failure.payload)).not.toContain('123456'); expect(JSON.stringify(failure.payload)).not.toContain('abc'); }); it('preserves only allowlisted upstream hardware codes so 409 conflicts remain distinguishable', async () => { const conflict = setup(vi.fn().mockResolvedValue(jsonResponse({ detail: { error_code: 'ai_hardware_operation_in_progress', message: 'secret operation body', retryable: true }, }, { status: 409 }))); const result = await invoke(conflict.handler, 'POST', '/api/works/ai-hardware/agents', { agent_name: 'Desk' }); expect(result.payload).toEqual({ success: false, status: 409, code: 'ai_hardware_operation_in_progress', error: 'AI hardware operation is still in progress', retryable: true, operation_id: '11111111-1111-4111-8111-111111111111', }); expect(JSON.stringify(result.payload)).not.toContain('secret operation body'); const revision = setup(vi.fn().mockResolvedValue(jsonResponse({ detail: { error_code: 'ai_hardware_revision_conflict', message: 'secret revision', retryable: false }, }, { status: 409 }))); const revisionResult = await invoke(revision.handler, 'PATCH', '/api/works/ai-hardware/agents/a-1', { revision: 0, agent_name: 'Desk' }); expect(revisionResult.payload).toMatchObject({ code: 'ai_hardware_revision_conflict', status: 409 }); }); it('proxies credential recovery with no upstream business body', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(overview)); const { handler } = setup(fetchImpl); const operationId = '88888888-8888-4888-8888-888888888888'; const result = await invoke(handler, 'POST', '/api/works/ai-hardware/credential-recovery', { client_operation_id: operationId, }); expect(result.payload).toEqual({ success: true, data: overview }); expect(fetchImpl).toHaveBeenCalledWith( 'https://square.example/api/ai-hardware/credential-recovery', expect.objectContaining({ method: 'POST', body: undefined, headers: expect.objectContaining({ 'Idempotency-Key': `makelore-${operationId}` }), }), ); }); it('preserves credential-recovery-unavailable instead of treating it as a revision conflict', async () => { const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ detail: { error_code: 'ai_hardware_credential_recovery_unavailable', message: 'internal state detail', retryable: false, } }, { status: 409 })); const { handler } = setup(fetchImpl); const result = await invoke(handler, 'POST', '/api/works/ai-hardware/credential-recovery', { client_operation_id: '99999999-9999-4999-8999-999999999999', }); expect(result.payload).toMatchObject({ code: 'ai_hardware_credential_recovery_unavailable', error: 'AI hardware credential recovery is not currently available', status: 409, retryable: false, operation_id: '99999999-9999-4999-8999-999999999999', }); }); it('retries operation-in-progress once with the same operation identity', async () => { vi.useFakeTimers(); try { const fetchImpl = vi.fn() .mockResolvedValueOnce(jsonResponse({ detail: { error_code: 'ai_hardware_operation_in_progress', message: 'pending', retryable: true }, }, { status: 409, headers: { 'retry-after': '1' } })) .mockResolvedValueOnce(jsonResponse({ id: 'a-1', name: 'Desk', config_revision: 0 }, { status: 201 })); const { handler } = setup(fetchImpl); const pending = invoke(handler, 'POST', '/api/works/ai-hardware/agents', { agent_name: 'Desk', client_operation_id: '66666666-6666-4666-8666-666666666666', }); await vi.advanceTimersByTimeAsync(1_000); await expect(pending).resolves.toMatchObject({ payload: { success: true } }); expect(fetchImpl).toHaveBeenCalledTimes(2); for (const call of fetchImpl.mock.calls) { expect((call[1] as RequestInit).headers).toMatchObject({ 'Idempotency-Key': 'makelore-66666666-6666-4666-8666-666666666666', }); } } finally { vi.useRealTimers(); } }); it('does not parse or expose non-JSON upstream error bodies', async () => { const cancel = vi.fn(); const sensitiveBody = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('{"detail":{"error_code":"ai_hardware_revision_conflict","message":"token-secret"}}')); }, cancel, }); const failed = setup(vi.fn().mockResolvedValue(new Response(sensitiveBody, { status: 409, headers: { 'content-type': 'text/plain' }, }))); const result = await invoke(failed.handler, 'PATCH', '/api/works/ai-hardware/agents/a-1', { revision: 0, agent_name: 'Desk' }); expect(result.payload).toEqual({ success: false, status: 409, code: 'AI_HARDWARE_REVISION_CONFLICT', error: 'AI hardware data changed; refresh and retry', retryable: false, operation_id: '11111111-1111-4111-8111-111111111111', }); expect(JSON.stringify(result.payload)).not.toContain('token-secret'); expect(cancel).toHaveBeenCalledOnce(); }); it('maps timeout and response overflow to safe local-200 failures', async () => { const timeoutFetch = vi.fn((_input, init) => new Promise((_resolve, reject) => { init?.signal?.addEventListener('abort', () => reject(new DOMException('secret body', 'AbortError'))); })); const timed = setup(timeoutFetch); const timeoutResult = await invoke(timed.handler, 'GET', '/api/works/ai-hardware'); expect(timeoutResult.payload).toMatchObject({ success: false, status: 504, code: 'AI_HARDWARE_TIMEOUT', retryable: true }); const huge = setup(vi.fn().mockResolvedValue(new Response('x'.repeat(256 * 1024 + 1), { headers: { 'content-type': 'application/json' } }))); const hugeResult = await invoke(huge.handler, 'GET', '/api/works/ai-hardware'); expect(hugeResult.payload).toMatchObject({ success: false, status: 502, code: 'AI_HARDWARE_RESPONSE_TOO_LARGE' }); }); it('keeps the upstream deadline active while reading a stalled configuration body', async () => { let bodyController: ReadableStreamDefaultController | undefined; const fetchImpl = vi.fn((_input, init) => { const body = new ReadableStream({ start(controller) { bodyController = controller; controller.enqueue(new TextEncoder().encode('{"id":"a-1"')); init?.signal?.addEventListener('abort', () => { controller.error(new DOMException('secret stalled body', 'AbortError')); }, { once: true }); }, }); return Promise.resolve(new Response(body, { headers: { 'content-type': 'application/json', etag: '"0"' }, })); }); const { handler } = setup(fetchImpl); const pending = invoke(handler, 'GET', '/api/works/ai-hardware/agents/a-1'); const result = await Promise.race([ pending, new Promise((resolve) => setTimeout(() => resolve(null), 100)), ]); if (result === null) { bodyController?.close(); await pending; } expect(result?.payload).toMatchObject({ success: false, status: 504, code: 'AI_HARDWARE_TIMEOUT', retryable: true, }); }); it('bounds request bodies and distinguishes unrelated and unknown hardware routes', async () => { const { handler, fetchImpl } = setup(); const unrelated = response(); expect(await handler(request('GET'), unrelated.res, new URL('http://localhost/api/works'), {} as never)).toBe(false); const unknown = await invoke(handler, 'DELETE', '/api/works/ai-hardware/agents/a-1'); expect(unknown.status).toBe(404); expect(unknown.payload).toMatchObject({ code: 'AI_HARDWARE_ROUTE_NOT_FOUND' }); const oversized = await invoke(handler, 'POST', '/api/works/ai-hardware/agents', '{}', { 'content-length': String(64 * 1024 + 1) }); expect(oversized.status).toBe(200); expect(oversized.payload).toMatchObject({ status: 413, code: 'AI_HARDWARE_REQUEST_TOO_LARGE' }); expect(fetchImpl).not.toHaveBeenCalled(); }); it('classifies malformed percent-encoded resource IDs as safe client errors', async () => { const { handler, fetchImpl } = setup(); for (const path of [ '/api/works/ai-hardware/agents/%E0%A4%A', '/api/works/ai-hardware/devices/%ZZ/agent-assignment', ]) { const result = await invoke(handler, 'GET', path); expect(result.status).toBe(200); expect(result.payload).toEqual({ success: false, status: 400, code: 'AI_HARDWARE_INVALID_REQUEST', error: 'Invalid AI hardware request', retryable: false, }); } expect(fetchImpl).not.toHaveBeenCalled(); }); });