fix(teacher): 改善只读工具分页与读取预算

This commit is contained in:
2026-09-27 10:15:17 +08:00
parent 17c7e5c495
commit 56cefe84e7
8 changed files with 297 additions and 56 deletions

View File

@@ -127,6 +127,37 @@ it('waits for the interrupted parent cleanup and resubmits the same three read r
expect(paths.some(item => item.endsWith('/cancel'))).toBe(false);
});
it('returns the complete batch within the byte budget and lets the cloud finish with available evidence', async () => {
const f = await fixture();
await writeFile(path.join(f.access.projectPath, 'src/game.ts'), '源码🙂'.repeat(20000));
const results: { status: string; content: string }[] = [];
const transport: TeacherCloudTransport = {
events: vi.fn(),
json: vi.fn(async (url, body) => {
if (url === '/questions') return { request_id: 'question', run_id: 'one' };
if (url.endsWith('/tool-results')) {
results.push(...(body as { results: typeof results }).results);
return { run_id: 'done' };
}
if (url === '/runs/done') return { status: 'completed', output: '已检查的范围和待补充资料。' };
return { status: 'interrupted', interrupt: {
source: 'client_read_tools', context_id: requestId,
calls: Array.from({ length: 10 }, (_, i) => ({ tool_call_id: 'file-' + i, name: 'read_project_file',
arguments: { path: 'src/game.ts', start_column: 1 + i * 3000 } })),
} };
}),
};
const text = vi.fn();
await prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport)
.run([{ role: 'user', content: '检查' }], new AbortController().signal, text);
expect(results).toHaveLength(10);
const bytes = results.filter(item => item.status === 'success').reduce((sum, item) => sum + Buffer.byteLength(item.content), 0);
expect(bytes).toBeLessThanOrEqual(65536);
expect(bytes).toBeGreaterThan(65024);
expect(results.at(-1)).toMatchObject({ status: 'error', content: expect.stringContaining('budget exhausted') });
expect(text).toHaveBeenCalledWith('已检查的范围和待补充资料。');
});
it.each([
{ ...cleanupBusy, active_run_id: 'other-run' },
{ ...cleanupBusy, active_run_status: 'running' },
@@ -190,7 +221,7 @@ async function submitCompiledContext(
await model.run(compiled.messages, new AbortController().signal, vi.fn());
const body = vi.mocked(transport.json).mock.calls.find(([url]) => url === '/questions')?.[1] as {
query: string;
local_context: { id: string; scope: { project_id: string; source_session_id: string }; tools: string[] };
local_context: { id: string; read_protocol: number; scope: { project_id: string; source_session_id: string }; tools: string[] };
};
expect(JSON.parse(body.query)).toEqual({ messages: compiled.messages });
expect(estimateTeacherTokens(compiled.messages)).toBeLessThanOrEqual(model.inputLimit);
@@ -211,6 +242,7 @@ it('submits only the published persona, source evidence and current question in
expect(compiled.messages[0].content).toContain('你可以通过只读工具');
expect(compiled.messages[1].content).toContain(f.access.source.messages[0].text);
expect(compiled.messages[2].content).toContain('当前问题:\n下一步怎么想?');
expect(body.local_context.read_protocol).toBe(2);
expect(body.local_context.tools).toEqual(['list_project_files', 'read_project_file', 'read_conversation']);
});
@@ -512,7 +544,7 @@ it.each(['方法老师', '朋友', '代码顾问'])('returns all three local rea
expect.objectContaining({
tool_call_id: 'chat',
status: 'success',
content: expect.stringContaining('2: 调整重力'),
content: expect.stringContaining('调整重力'),
}),
],
},
@@ -522,7 +554,7 @@ it.each(['方法老师', '朋友', '代码顾问'])('returns all three local rea
expect(f.progress).toHaveBeenCalledWith('正在读取项目与会话…');
});
it.each([false, true])('finishes six batches and rejects a seventh as a read limit (extra batch: %s)', async extraBatch => {
it.each([false, true])('finishes twelve batches and rejects a thirteenth as a read limit (extra batch: %s)', async extraBatch => {
const f = await fixture();
const returned: unknown[] = [];
const transport: TeacherCloudTransport = {
@@ -534,7 +566,7 @@ it.each([false, true])('finishes six batches and rejects a seventh as a read lim
returned.push(body);
return { run_id: `run-${returned.length}` };
}
if (!extraBatch && returned.length === 6) return { status: 'completed', output: '已根据六批资料完成检查。' };
if (!extraBatch && returned.length === 12) return { status: 'completed', output: '已根据十二批资料完成检查。' };
return { status: 'interrupted', interrupt: {
source: 'client_read_tools', context_id: requestId,
calls: [{ tool_call_id: `read-${returned.length}`, name: 'read_project_file', arguments: { path: 'src/game.ts' } }],
@@ -549,10 +581,10 @@ it.each([false, true])('finishes six batches and rejects a seventh as a read lim
expect(transport.json).toHaveBeenCalledWith('/questions/' + requestId + '/cancel', {}, expect.anything());
} else {
await result;
expect(text).toHaveBeenCalledWith('已根据六批资料完成检查。');
expect(text).toHaveBeenCalledWith('已根据十二批资料完成检查。');
expect(transport.json).not.toHaveBeenCalledWith('/questions/' + requestId + '/cancel', expect.anything(), expect.anything());
}
expect(returned).toHaveLength(6);
expect(returned).toHaveLength(12);
});
it.each([