fix(teacher): 区分读取额度与上下文失效

This commit is contained in:
2026-09-26 18:03:47 +08:00
parent 287dcd303d
commit 60c20f996e
4 changed files with 106 additions and 7 deletions

View File

@@ -440,6 +440,58 @@ 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 => {
const f = await fixture();
const returned: unknown[] = [];
const transport: TeacherCloudTransport = {
events: vi.fn(),
json: vi.fn(async (url, body) => {
if (url === '/questions') return { request_id: 'question', run_id: 'run-0' };
if (url.endsWith('/cancel')) return { status: 'cancelled' };
if (url.endsWith('/tool-results')) {
returned.push(body);
return { run_id: `run-${returned.length}` };
}
if (!extraBatch && returned.length === 6) 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' } }],
} };
}),
};
const run = prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport);
const text = vi.fn();
const result = run.run([{ role: 'user', content: '检查项目' }], new AbortController().signal, text);
if (extraBatch) {
await expect(result).rejects.toMatchObject({ code: 'teacher_read_limit', message: expect.stringContaining('读取上限') });
expect(transport.json).toHaveBeenCalledWith('/questions/' + requestId + '/cancel', {}, expect.anything());
} else {
await result;
expect(text).toHaveBeenCalledWith('已根据六批资料完成检查。');
expect(transport.json).not.toHaveBeenCalledWith('/questions/' + requestId + '/cancel', expect.anything(), expect.anything());
}
expect(returned).toHaveLength(6);
});
it.each([
{ label: 'missing', calls: undefined },
{ label: 'empty', calls: [] },
{ label: 'oversized', calls: Array.from({ length: 33 }, () => ({})) },
])('rejects $label read batches as protocol errors', async ({ calls }) => {
const f = await fixture();
const transport: TeacherCloudTransport = {
events: vi.fn(),
json: vi.fn(async url => {
if (url === '/questions') return { request_id: 'question', run_id: 'one' };
if (url.endsWith('/cancel')) return { status: 'cancelled' };
return { status: 'interrupted', interrupt: { source: 'client_read_tools', context_id: requestId, calls } };
}),
};
const run = prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport);
await expect(run.run([], new AbortController().signal, vi.fn())).rejects.toMatchObject({ code: 'teacher_protocol_invalid' });
expect(transport.json).not.toHaveBeenCalledWith(expect.stringContaining('/tool-results'), expect.anything(), expect.anything());
});
it('rejects a stale context before reading or returning any project data', async () => {
const f = await fixture();
const transport: TeacherCloudTransport = {