fix(chat): reconcile reply recovery with continuous agent conversations
This commit is contained in:
@@ -6,6 +6,7 @@ import path from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { TeacherConversationStore } from '../../electron/coding-teacher/conversation-store';
|
||||
import { TeacherTopicStore } from '../../electron/coding-teacher/store';
|
||||
import { applyTeacherReply } from '../../electron/coding-teacher/reply';
|
||||
import { CodingTeacherService } from '../../electron/coding-teacher/service';
|
||||
import { CodingProjectService } from '../../electron/coding-projects/project-service';
|
||||
import { createCodingProjectStore, createMemoryCodingProjectStorage } from '../../electron/coding-projects/project-store';
|
||||
@@ -36,6 +37,55 @@ function turn(index: number): TeacherRequest {
|
||||
omittedMessages: 0, status: 'completed', response: '回答 ' + index };
|
||||
}
|
||||
describe('continuous conversation persistence', () => {
|
||||
it('repairs retained replies on every page without rewriting their files during restart recovery', async () => {
|
||||
const dir = await root();
|
||||
const store = new TeacherConversationStore(dir, 'account', 'agent-a');
|
||||
const topic = await store.ensure(definition, 1);
|
||||
const prose = '它早就不是"刚搭好架子"那种阶段了。\n完整的后半段。';
|
||||
const damaged = { ...turn(0), response: '它早就不是', discussionError: '旧解析失败',
|
||||
unparsedResponse: '{"reply":"' + prose + '","quickReplies":[]}' };
|
||||
const incomplete = { ...turn(1), response: '它早就不是', discussionError: '旧解析失败',
|
||||
unparsedResponse: '{"reply":"它早就不是"刚搭好架子' };
|
||||
const running = { ...turn(60), status: 'running' as const };
|
||||
topic.requests = [damaged, incomplete, ...Array.from({ length: 57 }, (_, i) => turn(i + 2)), running];
|
||||
await store.save(topic);
|
||||
const file = (id: string) => path.join(dir, 'turns', id + '.json');
|
||||
const rawBefore = await readFile(file(damaged.id), 'utf8');
|
||||
// Simulate a pre-repair index, including an untrusted partial assistant excerpt.
|
||||
const manifestFile = path.join(dir, 'conversation.json');
|
||||
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
||||
for (const item of manifest.turns) delete item.replyProjectionVersion;
|
||||
manifest.turns[1].messages.push({ id: 'teacher:' + incomplete.id + ':assistant', role: 'assistant', text: '它早就不是' });
|
||||
await atomicWriteJson(manifestFile, manifest);
|
||||
const resumed = new TeacherConversationStore(dir, 'account', 'agent-a');
|
||||
const latest = await resumed.page();
|
||||
expect(latest.requests.at(-1)?.status).toBe('interrupted');
|
||||
const archive = await resumed.history();
|
||||
expect(archive.messages.some(message => message.text === '它早就不是')).toBe(false);
|
||||
expect(await archive.read('teacher:' + incomplete.id + ':assistant')).toBeUndefined();
|
||||
expect((await archive.read('teacher:' + damaged.id + ':assistant'))?.text).toBe(prose);
|
||||
const older = await resumed.page(latest.before!);
|
||||
expect(older.requests[0]).toMatchObject({ response: prose, unparsedResponse: damaged.unparsedResponse });
|
||||
expect(older.requests[1].replyIncomplete).toBe(true);
|
||||
expect(await readFile(file(damaged.id), 'utf8')).toBe(rawBefore);
|
||||
expect(JSON.parse(await readFile(file(incomplete.id), 'utf8'))).not.toHaveProperty('replyIncomplete');
|
||||
expect(JSON.parse(await readFile(file(running.id), 'utf8')).status).toBe('interrupted');
|
||||
});
|
||||
|
||||
it('keeps rejected new shortcuts hidden after reload while retaining historical cards', async () => {
|
||||
const dir = await root();
|
||||
const store = new TeacherConversationStore(dir, 'account', 'agent-a');
|
||||
const topic = await store.ensure(definition, 1);
|
||||
const current = turn(1);
|
||||
const old = { ...turn(0), suggestedQuestions: ['一', '二', '三', '四'] };
|
||||
applyTeacherReply(current, JSON.stringify({ reply: '完整回答', quickReplies: old.suggestedQuestions }));
|
||||
topic.requests = [old, current];
|
||||
await store.save(topic);
|
||||
const loaded = await new TeacherConversationStore(dir, 'account', 'agent-a').read();
|
||||
expect(loaded.requests[0].suggestedQuestions).toEqual(old.suggestedQuestions);
|
||||
expect(loaded.requests[1]).toMatchObject({ response: '完整回答', suggestedQuestions: [], replyQuickRepliesRejected: true, replyParseError: expect.any(String) });
|
||||
});
|
||||
|
||||
it('pages thousands of turns, restores the latest page and preserves repeated questions', async () => {
|
||||
const dir = await root();
|
||||
const store = new TeacherConversationStore(dir, 'account', 'agent-a');
|
||||
|
||||
Reference in New Issue
Block a user