Merge teacher reply cleanup with continuous conversations

This commit is contained in:
鲨鱼辣椒
2026-09-28 18:29:04 +08:00
40 changed files with 1759 additions and 1988 deletions

View File

@@ -91,6 +91,68 @@ describe('continuous conversation persistence', () => {
await other.importProject(project);
expect(await other.exists()).toBe(false);
});
it('imports reply history and its execution identity without reviving archived components', async () => {
const dir = await root(), project = { id: randomUUID(), path: path.join(dir, 'project'), name: '旧项目' };
const sourceId = randomUUID();
const archived = { obsoleteComponent: 'never-send-this-to-model', broken: ['not', 'a', 'component'] };
const oldStore = new TeacherTopicStore(path.join(project.path, '.makelore', 'teacher-conversations', 'account', sourceId));
const request: TeacherRequest = { ...turn(1), sourceConversationId: sourceId,
presentation: 'discussion-v1', discussionContext: archived, discussionSnapshot: archived,
discussionError: 'historical parser error', cloudRequestId: 'old-cloud-request',
response: '保留的正文', suggestedQuestions: ['继续聊这个想法'] };
const old: TeacherTopic = { schemaVersion: 1, revision: 1, accountId: 'account', id: randomUUID(),
projectId: project.id, sourceConversationId: sourceId, definition, version: 7,
createdAt: 'now', updatedAt: 'now', requests: [request], discussion: archived };
await oldStore.save(old);
const file = path.join(oldStore.directory, old.id + '.json'), before = await readFile(file, 'utf8');
const target = path.join(dir, 'new');
const store = new TeacherConversationStore(target, 'account', 'agent-a');
await store.importProject(project);
const resumed = new TeacherConversationStore(target, 'account', 'agent-a');
await resumed.importProject(project);
const topic = await resumed.read();
expect(topic.requests).toHaveLength(1);
expect(topic.requests[0]).toMatchObject({
origin: { projectId: project.id, topicId: old.id, requestId: request.id },
projectId: project.id, projectName: project.name, sourceConversationId: sourceId,
teacherVersion: 7, runtimeThreadId: old.id, cloudRequestId: 'old-cloud-request',
response: '保留的正文', suggestedQuestions: ['继续聊这个想法'],
discussionContext: archived, discussionSnapshot: archived, discussionError: 'historical parser error',
});
expect(topic.conversation).not.toHaveProperty('discussions');
expect(topic).not.toHaveProperty('discussion');
const manifest = JSON.parse(await readFile(path.join(target, 'conversation.json'), 'utf8'));
expect(manifest).not.toHaveProperty('importedDiscussions');
expect(JSON.stringify((await resumed.history()).messages)).not.toContain(archived.obsoleteComponent);
const history = await resumed.history();
expect((await history.read(`teacher:${topic.requests[0].id}:assistant`))?.text)
.toBe('保留的正文\n\n可以接着聊的问题:\n- 继续聊这个想法');
expect(await readFile(file, 'utf8')).toBe(before);
});
it('preserves existing continuous-chat component archives opaquely across recovery and writes', async () => {
const dir = await root();
const store = new TeacherConversationStore(dir, 'account', 'agent-a');
const topic = await store.ensure(definition, 1);
expect(topic.conversation).not.toHaveProperty('discussions');
const manifestPath = path.join(dir, 'conversation.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
const archives = { project: { malformedLegacyState: true, active: ['opaque', 'archive'] } };
const imported = { project: { legacyMarker: true } };
manifest.topic.conversation.discussions = archives;
manifest.importedDiscussions = imported;
await atomicWriteJson(manifestPath, manifest);
const pending = { ...turn(1), status: 'running' as const, discussionSnapshot: { malformed: true } };
await atomicWriteJson(path.join(dir, 'turns', pending.id + '.json'), pending);
const resumed = new TeacherConversationStore(dir, 'account', 'agent-a');
const restored = await resumed.read();
expect(restored.requests[0]).toMatchObject({ status: 'interrupted', discussionSnapshot: { malformed: true } });
restored.requests.push(turn(2));
await resumed.save(restored, restored.requests[1].id);
const saved = JSON.parse(await readFile(manifestPath, 'utf8'));
expect(saved.topic.conversation.discussions).toEqual(archives);
expect(saved.importedDiscussions).toEqual(imported);
expect((await new TeacherConversationStore(dir, 'account', 'agent-a').page()).requests).toHaveLength(2);
});
it('recovers an import index failure before retrying in the same process', async () => {
const dir = await root(), project = { id: randomUUID(), path: path.join(dir, 'project'), name: '旧项目' };
const oldStore = new TeacherTopicStore(path.join(project.path, '.makelore', 'teacher-conversations', 'account', 'project'));