feat(coding): add conversation archive and first-message titles

This commit is contained in:
2026-09-20 12:43:51 +08:00
parent d61226532a
commit fd0293fe3d
22 changed files with 698 additions and 69 deletions

View File

@@ -79,6 +79,47 @@ function deferred<T>() {
}
describe('coding workspace store', () => {
it('discards a stale list read and refreshes after a manual rename', async () => {
const old = conversation('conversation-a', 'agent-a');
const stale = deferred<CodingConversationMetadata[]>();
const latest = { ...old, title: '手动标题' };
const listConversations = vi.fn().mockResolvedValueOnce([old])
.mockReturnValueOnce(stale.promise).mockResolvedValue([latest]);
const store = createCodingWorkspaceStore({
listProjects: async () => ({ projects: [project], activeProjectId: project.id }),
getConfig: async () => ({ project, config: config([agent('agent-a')]) }),
listConversations, patchConversation: async () => latest,
});
await store.getState().load();
const refresh = store.getState().refreshConversations(project.id);
await vi.waitFor(() => expect(listConversations).toHaveBeenCalledTimes(2));
await store.getState().patchConversation(old.id, { title: latest.title });
stale.resolve([old]);
await refresh;
expect(store.getState().conversations[0].title).toBe(latest.title);
expect(listConversations).toHaveBeenCalledTimes(3);
expect(store.getState().loadState).toBe('ready');
});
it('does a follow-up read when metadata changes during an in-flight refresh', async () => {
const old = conversation('conversation-a', 'agent-a');
const stale = deferred<CodingConversationMetadata[]>();
const latest = { ...old, archivedAt: '2026-09-20T00:00:00Z' };
const listConversations = vi.fn().mockResolvedValueOnce([old])
.mockReturnValueOnce(stale.promise).mockResolvedValue([latest]);
const store = createCodingWorkspaceStore({
listProjects: async () => ({ projects: [project], activeProjectId: project.id }),
getConfig: async () => ({ project, config: config([agent('agent-a')]) }), listConversations,
});
await store.getState().load();
const refresh = store.getState().refreshConversations(project.id);
await vi.waitFor(() => expect(listConversations).toHaveBeenCalledTimes(2));
const followup = store.getState().refreshConversations(project.id);
stale.resolve([old]);
await Promise.all([refresh, followup]);
expect(store.getState().conversations).toEqual([latest]);
});
it('loads local project metadata and selects the pinned Agent without touching runtime APIs', async () => {
const listProjects = vi.fn(async () => ({ projects: [project], activeProjectId: project.id }));
const getConfig = vi.fn(async () => ({