feat: add dynamic Robot configuration choices

This commit is contained in:
2026-08-16 00:55:06 +08:00
parent 7bef261fb8
commit fe55deed04
7 changed files with 524 additions and 10 deletions

View File

@@ -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,