444 lines
23 KiB
TypeScript
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();
|
|
});
|
|
});
|