fix(teacher): 区分词元预算与传输字节避免短问题被拦截

This commit is contained in:
2026-09-24 11:09:29 +08:00
parent eb3dc85a2f
commit de72b1cd94
10 changed files with 187 additions and 35 deletions

View File

@@ -61,7 +61,7 @@ afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
vi.useRealTimers();
});
async function fixture({ durableSource = false, sourceContext = context, liveModel = false, cloudTeacher = false, mockCloud = false, modelInputLimit = definition.limits.max_input_tokens } = {}) {
async function fixture({ durableSource = false, sourceContext = context, liveModel = false, cloudTeacher = false, mockCloud = false, modelInputLimit = definition.limits.max_input_tokens, modelSystemPrompt = definition.system_prompt } = {}) {
const root = await mkdtemp(path.join(tmpdir(), 'coding-teacher-'));
roots.push(root);
const projects = new CodingProjectService(
@@ -114,7 +114,7 @@ async function fixture({ durableSource = false, sourceContext = context, liveMod
yuxi: { agent_slug: 'teacher', agent_version: 2 } } }] }) : undefined,
version: async (_account, v) => ({
version: v,
payload: { ...definition, name: '老师 v' + v, limits: { ...definition.limits, max_input_tokens: modelInputLimit } },
payload: { ...definition, name: '老师 v' + v, system_prompt: modelSystemPrompt, limits: { ...definition.limits, max_input_tokens: modelInputLimit } },
}),
preview: async (_account, revision) => {
if (revision !== 2) throw new TeacherError(409, 'teacher_draft_changed', '草稿变化');
@@ -170,7 +170,9 @@ describe('cloud coding teacher', () => {
expect((await f.service.list(f.scope)).lastSelectedTopicId).toBe(selected.id);
});
it('wires the Yuxi topic through scoped credentials and local tools without a local prompt or model loop', async () => {
const f = await fixture({ cloudTeacher: true });
const f = await fixture({ cloudTeacher: true, sourceContext: {
...context, messages: [{ id: 'long-code', role: 'assistant', text: 'const title = "game";\n'.repeat(500) }],
} });
await writeFile(path.join(f.created.project.path, 'counter.ts'), 'let count = 42;');
vi.spyOn(teacherCloud, 'assertTeacherAccount').mockReturnValue(undefined);
vi.spyOn(teacherCloud, 'teacherCloudRequest').mockResolvedValue({
@@ -198,7 +200,9 @@ describe('cloud coding teacher', () => {
const saved = (await f.service.read(f.scope, topic.id)).requests[0];
expect(saved.response).toBe('计数器从 42 开始。');
expect(saved.cloudRequestId).toBe('cloud-question');
expect(JSON.stringify(requests[0].body)).toContain('创建计数器');
expect(JSON.stringify(requests[0].body)).toContain('const title');
expect(saved.truncatedMessages).toBeGreaterThan(0);
expect(Buffer.byteLength((requests[0].body as { query: string }).query)).toBeLessThanOrEqual(8000);
expect(requests.find(item => item.path.endsWith('/tool-results'))?.body).toMatchObject({
context_id: id, results: [{ tool_call_id: 'read-counter', status: 'success', content: expect.stringContaining('let count = 42;') }],
});
@@ -1102,7 +1106,7 @@ describe('teacher context and wire contract', () => {
expect(compiled.messages[0].content).toContain(TEACHER_BEHAVIOR_PROMPT);
expect(compiled.includedSourceMessageIds).toEqual(['source-user']);
expect(compiled.omittedMessages).toBe(1);
expect(() => compileTeacherContext(definition, context, [], 'x'.repeat(9000), [])).toThrow(
expect(() => compileTeacherContext(definition, context, [], 'x'.repeat(18000), [])).toThrow(
'超过上下文预算'
);
});
@@ -1297,7 +1301,14 @@ it.each(['teacher', 'friend'] as const)('routes project-level %s config and topi
describe('structured teacher service integration', () => {
it.each(['suggestions', 'discussion'] as const)('parses only final %s JSON after the real local runner reads a project file', async (format) => {
const f = await fixture({ liveModel: true, modelInputLimit: 24000 });
const f = await fixture({
liveModel: true, modelInputLimit: 8000,
modelSystemPrompt: '帮助学生理解设计,结合实际进展给出建议。'.repeat(45),
sourceContext: { ...context, messages: [
{ id: 'current-question', role: 'user', text: '创建计数器' },
{ id: 'long-code', role: 'assistant', text: 'const title = "game";\n'.repeat(500) },
] },
});
const scope = { ...f.scope, sourceId: 'project', role: 'teacher' as const };
await writeFile(path.join(f.created.project.path, 'counter.ts'), 'let count = 42;');
vi.spyOn(teacherCloud, 'assertTeacherAccount').mockReturnValue(undefined);