feat: integrate pinned teacher discussion workspace

This commit is contained in:
鲨鱼辣椒
2026-09-23 19:13:51 +08:00
parent 7951cca700
commit 404cfcde46
21 changed files with 1666 additions and 11 deletions

View File

@@ -1156,3 +1156,44 @@ it.each(['teacher', 'friend'] as const)('routes project-level %s config and topi
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
});
describe('structured teacher service integration', () => {
it('buffers model JSON, offers once, locks updates, persists state, and never applies a cancelled update', async () => {
const f = await fixture();
const scope = { ...f.scope, sourceId: 'project', role: 'teacher' as const };
const initial = await f.service.create(scope);
const tool = { kind: 'ideas', title: '宠物游戏', items: [{ id: 'dog', text: '养只小狗', state: 'kept' }] };
f.replyWith(JSON.stringify({ reply: '我们可以先把想法放在一起。', quickReplies: [], tool }));
const send = (text: string, discussion?: { toolId: string; revision: number }) => f.service.send(scope, initial.id, {
requestId: crypto.randomUUID(), text, presentation: 'discussion-v1', sourceConversationId: f.scope.sourceId, discussion,
});
const pending = await send('想养只小狗');
expect(pending.requests[0].response).toBe('');
expect(pending.discussion).toBeUndefined();
f.finish();
await vi.waitFor(async () => expect((await f.service.read(scope, initial.id)).requests[0].status).toBe('completed'));
const offered = await f.service.read(scope, initial.id);
expect(offered.discussion?.status).toBe('offered');
expect(offered.requests[0].response).toBe('我们可以先把想法放在一起。');
const active = await f.service.updateDiscussion(scope, initial.id, { toolId: offered.discussion!.id, revision: 1, action: 'enter' });
const context = { toolId: active.discussion!.id, revision: 2 };
f.replyWith(JSON.stringify({ reply: '把花园也放进来了。', tool: { ...tool, title: '小狗和花园' } }));
const running = await send('还想种花', context);
expect(running.discussion?.content.title).toBe('宠物游戏');
await expect(f.service.updateDiscussion(scope, initial.id, { ...context, action: 'finish' })).rejects.toMatchObject({ code: 'teacher_topic_busy' });
await f.service.cancel(scope, initial.id, running.requests.at(-1)!.id);
await vi.waitFor(async () => expect((await f.service.read(scope, initial.id)).requests.at(-1)?.status).toBe('cancelled'));
expect((await f.service.read(scope, initial.id)).discussion?.content.title).toBe('宠物游戏');
await send('再把花园加进来', context);
f.finish();
await vi.waitFor(async () => expect((await f.service.read(scope, initial.id)).requests.at(-1)?.status).toBe('completed'));
const completed = await f.service.read(scope, initial.id);
expect(completed.discussion?.content.title).toBe('小狗和花园');
expect(completed.discussion?.revision).toBe(3);
expect(completed.requests[0].discussionSnapshot?.title).toBe('宠物游戏');
await expect(send('旧版本', context)).rejects.toMatchObject({ code: 'teacher_discussion_changed' });
const restarted = await f.restart();
expect((await restarted.read(scope, initial.id)).discussion).toEqual(completed.discussion);
});
});