feat(teacher): 连接 Yuxi 老师并提供项目与会话只读工具
This commit is contained in:
@@ -56,7 +56,7 @@ afterEach(async () => {
|
||||
await Promise.all(services.splice(0).map((service) => service.dispose()));
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
async function fixture({ durableSource = false, sourceContext = context, liveModel = false } = {}) {
|
||||
async function fixture({ durableSource = false, sourceContext = context, liveModel = false, cloudTeacher = false } = {}) {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'coding-teacher-'));
|
||||
roots.push(root);
|
||||
const projects = new CodingProjectService(
|
||||
@@ -100,6 +100,9 @@ async function fixture({ durableSource = false, sourceContext = context, liveMod
|
||||
if (!accountCurrent) throw new TeacherError(401, 'teacher_account_changed', '账号变化');
|
||||
},
|
||||
availability: async () => ({ enabled, published_version: version, revision: version }),
|
||||
catalog: cloudTeacher ? async () => ({ items: [{ teacher_id: 'cloud-teacher', version: 9, is_default: true,
|
||||
definition: { ...definition, config_id: 'cloud-teacher', runtime: 'yuxi' as const, system_prompt: '', skills: [],
|
||||
yuxi: { agent_slug: 'teacher', agent_version: 2 } } }] }) : undefined,
|
||||
version: async (_account, v) => ({
|
||||
version: v,
|
||||
payload: { ...definition, name: '老师 v' + v },
|
||||
@@ -132,6 +135,42 @@ async function fixture({ durableSource = false, sourceContext = context, liveMod
|
||||
};
|
||||
}
|
||||
describe('cloud coding teacher', () => {
|
||||
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 });
|
||||
await writeFile(path.join(f.created.project.path, 'counter.ts'), 'let count = 42;');
|
||||
vi.spyOn(teacherCloud, 'assertTeacherAccount').mockReturnValue(undefined);
|
||||
vi.spyOn(teacherCloud, 'teacherCloudRequest').mockResolvedValue({
|
||||
scope: 'makelore-teachers', access_token: 'teacher-test', api_base_url: 'https://teacher.invalid',
|
||||
expires_at: Math.floor(Date.now() / 1000) + 120,
|
||||
});
|
||||
const id = '22222222-2222-4222-8222-222222222222';
|
||||
const requests: Array<{ path: string; body: unknown }> = [];
|
||||
vi.spyOn(teacherTransport, 'proxyAwareFetch').mockImplementation(async (url, init) => {
|
||||
const pathname = new URL(String(url)).pathname.replace('/api/makelore/teachers', '');
|
||||
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
|
||||
requests.push({ path: pathname, body });
|
||||
if (pathname === '/questions') return Response.json({ request_id: 'cloud-question', run_id: 'parent' });
|
||||
if (pathname === '/runs/parent') return Response.json({ status: 'interrupted', interrupt: {
|
||||
source: 'client_read_tools', context_id: id, calls: [{ tool_call_id: 'read-counter', name: 'read_project_file', arguments: { path: 'counter.ts' } }],
|
||||
} });
|
||||
if (pathname.endsWith('/tool-results')) return Response.json({ run_id: 'continued' });
|
||||
if (pathname === '/runs/continued') return Response.json({ status: 'completed', output: '计数器从 42 开始。' });
|
||||
throw new Error('unexpected cloud call: ' + pathname);
|
||||
});
|
||||
try {
|
||||
const topic = await f.service.create(f.scope, undefined, undefined, 9);
|
||||
await f.service.send(f.scope, topic.id, { requestId: id, text: '解释项目里的计数器' });
|
||||
await vi.waitFor(async () => expect((await f.service.read(f.scope, topic.id)).requests[0].status).toBe('completed'));
|
||||
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(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;') }],
|
||||
});
|
||||
expect(f.run).not.toHaveBeenCalled();
|
||||
} finally { vi.restoreAllMocks(); }
|
||||
});
|
||||
it('connects the scoped service, source context, file tools and model continuation', async () => {
|
||||
const f = await fixture({ liveModel: true });
|
||||
await writeFile(path.join(f.created.project.path, 'counter.ts'), 'let count = 42;');
|
||||
|
||||
Reference in New Issue
Block a user