Files
makelore/tests/unit/ai-hardware-routes.test.ts

444 lines
23 KiB
TypeScript

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<string, string> = {}): 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<string, unknown>,
};
}
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<typeof fetch>().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<typeof createAiHardwareRouteHandler>, method: string, path: string, body?: unknown, headers?: Record<string, string>) {
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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>()
.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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>()
.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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>()
.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<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode('{"detail":{"error_code":"ai_hardware_revision_conflict","message":"token-secret"}}'));
},
cancel,
});
const failed = setup(vi.fn<typeof fetch>().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<typeof fetch>((_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<typeof fetch>().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<Uint8Array> | undefined;
const fetchImpl = vi.fn<typeof fetch>((_input, init) => {
const body = new ReadableStream<Uint8Array>({
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<null>((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();
});
});