fix: accept compressed Robot revision ETags

This commit is contained in:
2026-08-15 20:48:54 +08:00
parent a26a53a7f4
commit ea75b0618a
3 changed files with 154 additions and 9 deletions

View File

@@ -98,10 +98,56 @@ describe('AI hardware Host API route', () => {
expect(result.payload).toEqual({ success: true, data: config, revision: 0 });
});
it('rejects weak upstream ETags without exposing upstream data', async () => {
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: 'W/"0"' } }));
}, { 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);
@@ -109,7 +155,7 @@ describe('AI hardware Host API route', () => {
expect(JSON.stringify(result.payload)).not.toContain('secret');
});
it('rejects config and assignment DTO revisions that disagree with the strong ETag', async () => {
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,
@@ -118,13 +164,13 @@ describe('AI hardware Host API route', () => {
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: '"1"' } })));
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: '"2"' } },
{ 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' });