fix: render consultation replies and typed tool activity

This commit is contained in:
2026-09-24 14:36:20 +08:00
parent a628860144
commit 97837cef90
19 changed files with 432 additions and 35 deletions

View File

@@ -113,6 +113,44 @@ it('submits only the published persona, source evidence and current question in
expect(body.local_context.tools).toEqual(['list_project_files', 'read_project_file', 'read_conversation']);
});
it.each([null, { content: 'not a supported output field' }, ['text block']])('reports unsupported final output instead of silently losing %j', async output => {
const f = await fixture();
const text = vi.fn();
const transport: TeacherCloudTransport = {
json: vi.fn(async url => url === '/questions' ? { request_id: requestId, run_id: 'run' } : { status: 'completed', output }),
events: vi.fn(),
};
await expect(prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport)
.run([{ role: 'user', content: '问题' }], new AbortController().signal, text))
.rejects.toMatchObject({ code: 'teacher_protocol_invalid' });
expect(text).not.toHaveBeenCalled();
});
it('merges typed tool activity across replay without adding tool data or child events to the answer', async () => {
const f = await fixture();
let reads = 0;
const activity = vi.fn(), text = vi.fn();
const transport: TeacherCloudTransport = {
json: vi.fn(async url => url === '/questions' ? { request_id: requestId, run_id: 'run' }
: ++reads === 1 ? { status: 'running', thread_id: 'main' } : { status: 'completed', output: '最终正文' }),
events: vi.fn(async (_url, _signal, accept) => {
const start = { stream_event: { type: 'tool_call', tool_call_id: 'call', name: 'search', args: { private: 'private-args' } } };
const finish = { status: 'stream_event', event: { method: 'tools', data: { event: 'tool-finished', tool_call_id: 'call', output: { status: 'success', content: 'private-result' } } } };
accept('messages', { thread_id: 'child', payload: { items: [start] } }, '1-0');
accept('messages', { thread_id: 'main', payload: { items: [start] } }, '2-0');
accept('custom', { thread_id: 'main', payload: { chunk: finish } }, '3-0');
accept('messages', { thread_id: 'main', payload: { items: [start] } }, '4-0');
}),
};
await prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity)
.run([{ role: 'user', content: '问题' }], new AbortController().signal, text);
expect(activity.mock.calls).toEqual([
[{ id: 'run:call', name: 'search', status: 'running' }],
[{ id: 'run:call', name: 'search', status: 'completed' }],
]);
expect(text.mock.calls).toEqual([['最终正文']]);
});
it('submits the active discussion protocol, current tool content and selected focus', async () => {
const f = await fixture();
f.topic.definition.limits.max_input_tokens = 16000;
@@ -448,6 +486,7 @@ it.each(['suggestions', 'discussion-v1'] as const)(
const finalOutput = JSON.stringify(result);
let firstReads = 0, resumedReads = 0;
const text = vi.fn();
const activity = vi.fn();
const transport: TeacherCloudTransport = {
json: vi.fn(async (url) => {
if (url === '/questions') return { request_id: requestId, run_id: 'before-read' };
@@ -481,9 +520,13 @@ it.each(['suggestions', 'discussion-v1'] as const)(
}),
};
await prepareCloudTeacher(
f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport
f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity
).run([{ role: 'user', content: '一起讨论' }], new AbortController().signal, text);
expect(text.mock.calls).toEqual([[finalOutput]]);
expect(activity.mock.calls).toEqual([
[{ id: 'before-read:file', name: 'read_project_file', status: 'running' }],
[{ id: 'before-read:file', name: 'read_project_file', status: 'completed' }],
]);
const response = text.mock.calls.map(([delta]) => delta).join('');
expect(format === 'suggestions'
? parseTeacherSuggestions(response)