fix(coding): scope metadata updates by project

This commit is contained in:
2026-08-24 09:43:45 +08:00
parent da92eb1957
commit fcd03f9f6d
5 changed files with 228 additions and 12 deletions

View File

@@ -69,6 +69,16 @@ function conversation(id: string, agentId: string): CodingConversationMetadata {
};
}
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
describe('coding workspace store', () => {
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 }));
@@ -156,4 +166,78 @@ describe('coding workspace store', () => {
second,
]);
});
it('does not write an old project metadata result into the newly active project', async () => {
const secondProject = { ...project, id: 'project-2', name: 'Second project' };
const firstConversation = conversation('conversation-a', 'agent-a');
const secondConversation = conversation('conversation-b', 'agent-b');
const patchFlight = deferred<CodingConversationMetadata>();
let activeProject = project;
const store = createCodingWorkspaceStore({
listProjects: vi.fn(async () => ({
projects: [project, secondProject],
activeProjectId: activeProject.id,
})),
getConfig: vi.fn(async (projectId: string) => (
projectId === project.id
? { project, config: config([agent('agent-a')]) }
: { project: secondProject, config: config([agent('agent-b')]) }
)),
listConversations: vi.fn(async (projectId: string) => (
projectId === project.id ? [firstConversation] : [secondConversation]
)),
createConversation: vi.fn(),
patchConversation: vi.fn(() => patchFlight.promise),
});
await store.getState().load();
const pendingPatch = store.getState().patchConversation(firstConversation.id, { title: 'Old project title' });
activeProject = secondProject;
await store.getState().load();
patchFlight.resolve({ ...firstConversation, title: 'Old project title' });
await pendingPatch;
expect(store.getState()).toMatchObject({
activeProjectId: secondProject.id,
conversations: [secondConversation],
});
});
it('keeps a metadata rejection on its source project and Conversation', async () => {
const secondProject = { ...project, id: 'project-2', name: 'Second project' };
const firstConversation = conversation('conversation-a', 'agent-a');
const secondConversation = conversation('conversation-b', 'agent-b');
const patchFlight = deferred<CodingConversationMetadata>();
let activeProject = project;
const store = createCodingWorkspaceStore({
listProjects: vi.fn(async () => ({
projects: [project, secondProject],
activeProjectId: activeProject.id,
})),
getConfig: vi.fn(async (projectId: string) => (
projectId === project.id
? { project, config: config([agent('agent-a')]) }
: { project: secondProject, config: config([agent('agent-b')]) }
)),
listConversations: vi.fn(async (projectId: string) => (
projectId === project.id ? [firstConversation] : [secondConversation]
)),
createConversation: vi.fn(),
patchConversation: vi.fn(() => patchFlight.promise),
});
await store.getState().load();
const pendingPatch = store.getState().patchConversation(firstConversation.id, { title: 'Rejected title' });
const rejection = expect(pendingPatch).rejects.toThrow('metadata rejected');
activeProject = secondProject;
await store.getState().load();
patchFlight.reject(new Error('metadata rejected'));
await rejection;
expect(store.getState().error).toBeNull();
expect(store.getState().conversationErrorsByProjectId).toEqual({
[project.id]: { [firstConversation.id]: 'metadata rejected' },
});
expect(store.getState().conversationErrorsByProjectId[secondProject.id]).toBeUndefined();
});
});