feat: add dynamic Robot configuration choices
This commit is contained in:
@@ -16,15 +16,17 @@ function request(method: string, body?: unknown, headers: Record<string, string>
|
||||
|
||||
function response() {
|
||||
const chunks: string[] = [];
|
||||
const headers = new Map<string, string>();
|
||||
const res = new EventEmitter();
|
||||
Object.assign(res, {
|
||||
statusCode: 0,
|
||||
setHeader: vi.fn(),
|
||||
setHeader: vi.fn((name: string, value: string) => headers.set(name.toLowerCase(), value)),
|
||||
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; },
|
||||
header: (name: string) => headers.get(name.toLowerCase()),
|
||||
json: () => JSON.parse(chunks.join('')) as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
@@ -56,6 +58,63 @@ async function invoke(handler: ReturnType<typeof createAiHardwareRouteHandler>,
|
||||
}
|
||||
|
||||
describe('AI hardware Host API route', () => {
|
||||
it('proxies only the fixed catalog query and projects its safe DTO', async () => {
|
||||
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({
|
||||
schema_version: 1,
|
||||
models: [{
|
||||
model_type: 'TTS', model_id: 'TTS_model', model_name: '云端语音',
|
||||
supports_function_call: null, config_json: 'must-not-pass',
|
||||
}],
|
||||
voices: [{
|
||||
tts_model_id: 'TTS_model', voice_id: 'voice-1', voice_name: '小夏',
|
||||
languages: ['zh-CN'], is_clone: false, voice_demo: 'internal-url',
|
||||
}],
|
||||
credential: 'secret-token',
|
||||
}));
|
||||
const { handler } = setup(fetchImpl);
|
||||
const result = await invoke(
|
||||
handler,
|
||||
'GET',
|
||||
'/api/works/ai-hardware/catalog?tts_model_id=TTS_model',
|
||||
);
|
||||
|
||||
expect(fetchImpl.mock.calls[0][0]).toBe(
|
||||
'https://square.example/api/ai-hardware/catalog?tts_model_id=TTS_model',
|
||||
);
|
||||
expect((fetchImpl.mock.calls[0][1] as RequestInit).headers).toMatchObject({
|
||||
Authorization: 'Bearer secret-token',
|
||||
});
|
||||
expect(result.payload).toEqual({ success: true, data: {
|
||||
schema_version: 1,
|
||||
models: [{
|
||||
model_type: 'TTS', model_id: 'TTS_model', model_name: '云端语音',
|
||||
supports_function_call: null,
|
||||
}],
|
||||
voices: [{
|
||||
tts_model_id: 'TTS_model', voice_id: 'voice-1', voice_name: '小夏',
|
||||
languages: ['zh-CN'], is_clone: false,
|
||||
}],
|
||||
} });
|
||||
expect(result.header('cache-control')).toBe('private, no-store');
|
||||
expect(result.header('pragma')).toBe('no-cache');
|
||||
expect(JSON.stringify(result.payload)).not.toMatch(/config_json|voice_demo|credential|secret-token/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'/api/works/ai-hardware/catalog?unknown=value',
|
||||
'/api/works/ai-hardware/catalog?tts_model_id=one&tts_model_id=two',
|
||||
'/api/works/ai-hardware/catalog?tts_model_id=%2Funsafe',
|
||||
])('rejects unsupported catalog query %s before fetching', async (path) => {
|
||||
const { handler, fetchImpl } = setup();
|
||||
const result = await invoke(handler, 'GET', path);
|
||||
expect(result.payload).toMatchObject({
|
||||
success: false, status: 400, code: 'AI_HARDWARE_INVALID_REQUEST',
|
||||
});
|
||||
expect(result.header('cache-control')).toBe('private, no-store');
|
||||
expect(result.header('pragma')).toBe('no-cache');
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('projects overview DTOs and drops unexpected sensitive fields', async () => {
|
||||
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({
|
||||
...overview,
|
||||
|
||||
Reference in New Issue
Block a user