fix: reopen preserved coding projects when selecting their folder

This commit is contained in:
2026-09-21 15:39:57 +08:00
parent 576edb2720
commit 6aa30a57b4
5 changed files with 186 additions and 1 deletions

View File

@@ -6,7 +6,9 @@ import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { HostApiContext } from '../../electron/api/context';
import { dispatchHostApiRequest } from '../../electron/api/host-api-dispatcher';
import { createCodingConversationStore } from '../../electron/coding-projects/conversation-store';
import {
createCodingProjectAgent,
createCodingProjectConfigV2,
isCanonicalCodingProjectId,
normalizeCodingProjectConfigV2,
@@ -45,6 +47,92 @@ function makeStore(localId = 'local-project-id') {
}
describe('coding project durable identity', () => {
it('reopens a removed project when its folder is selected for creation again', async () => {
const projectPath = await makeRoot();
const store = makeStore();
const projects = new CodingProjectService(store, {
createProjectId: vi.fn().mockReturnValueOnce(PROJECT_ID).mockReturnValue(NEXT_PROJECT_ID),
});
const input = { projectPath, projectType: 'interactive_ai_app' as const, identity: { kind: 'create' as const } };
const created = await projects.createProject({ ...input, projectType: 'custom' });
const agent = await createCodingProjectAgent(projectPath, {
id: 'builder', avatarId: 'avatar-01', roleName: 'Builder', name: 'My builder',
model: null, modelResolution: 'required', prompt: 'Keep my instructions', skillIds: [],
responsibility: { mission: 'Build my app', owns: [], boundaries: [], collaborators: [], principles: [] },
});
const conversations = createCodingConversationStore(projectPath);
const conversation = await conversations.create({
agentId: agent.id, title: 'Existing work', model: null, modelResolution: 'required',
});
const conversationPath = path.join(projectPath, '.makelore', 'conversations.json');
const originalConversations = await readFile(conversationPath, 'utf8');
const configPath = path.join(projectPath, '.makelore', 'project.json');
const originalConfig = await readFile(configPath, 'utf8');
await writeFile(path.join(projectPath, 'knowledge', 'notes.md'), 'Keep my project notes');
await projects.removeProject(created.project.id);
expect(await projects.listProjects()).toEqual([]);
expect(await projects.getActiveProject()).toBeNull();
const reopened = await projects.createProject(input);
expect(reopened.config.projectId).toBe(PROJECT_ID);
expect(reopened.config.projectType).toBe('custom');
expect(reopened.config.agents).toEqual([agent]);
expect(reopened.knowledgeFiles).toEqual(['notes.md']);
expect(await projects.getActiveProject()).toEqual(reopened.project);
expect(await projects.listProjects()).toEqual([reopened.project]);
expect(await readFile(configPath, 'utf8')).toBe(originalConfig);
expect(await readFile(conversationPath, 'utf8')).toBe(originalConversations);
expect((await createCodingConversationStore(reopened.project.path).read()).conversations).toEqual([conversation]);
expect(await readFile(path.join(projectPath, 'knowledge', 'notes.md'), 'utf8')).toBe('Keep my project notes');
const repeated = await projects.createProject(input);
expect(repeated.project.id).toBe(reopened.project.id);
expect(await projects.listProjects()).toEqual([repeated.project]);
});
it('keeps child-directory collisions and explicit identity binding as creation errors', async () => {
const parentPath = await makeRoot();
const projectPath = path.join(parentPath, 'existing');
const projects = new CodingProjectService(makeStore(), { createProjectId: () => PROJECT_ID });
const created = await projects.createProject({ projectPath, identity: { kind: 'create' } });
await projects.removeProject(created.project.id);
await expect(projects.createProject({
parentPath, projectName: 'existing', identity: { kind: 'create' },
})).rejects.toMatchObject({ status: 409, code: 'CODING_PROJECT_ALREADY_EXISTS' });
await expect(projects.createProject({
projectPath, identity: { kind: 'bind', projectId: NEXT_PROJECT_ID },
})).rejects.toMatchObject({ status: 409, code: 'CODING_PROJECT_ALREADY_EXISTS' });
expect(await projects.listProjects()).toEqual([]);
expect(await readCodingProjectConfigV2(projectPath)).toMatchObject({ config: { projectId: PROJECT_ID } });
});
it('reports invalid existing metadata without overwriting or registering it', async () => {
const projectPath = await makeRoot();
await mkdir(path.join(projectPath, '.makelore'));
const configPath = path.join(projectPath, '.makelore', 'project.json');
await writeFile(configPath, '{invalid json');
const projects = new CodingProjectService(makeStore());
await expect(projects.createProject({ projectPath, identity: { kind: 'create' } }))
.rejects.toMatchObject({ status: 409, code: 'CODING_PROJECT_CONFIG_INVALID' });
expect(await readFile(configPath, 'utf8')).toBe('{invalid json');
expect(await projects.listProjects()).toEqual([]);
});
it('reuses the existing identity-backfill path for a selected legacy project', async () => {
const projectPath = await makeRoot();
const store = makeStore();
const local = await createLocalCodingProject({ projectPath, now: CREATED }, store);
const projects = new CodingProjectService(store, { createProjectId: () => PROJECT_ID });
await projects.removeProject(local.project.id);
const reopened = await projects.createProject({ projectPath, identity: { kind: 'create' } });
expect(reopened.config).toMatchObject({ projectId: PROJECT_ID, createdAt: CREATED });
expect(await readCodingProjectConfigV2(projectPath)).toMatchObject({ config: { projectId: PROJECT_ID } });
});
it.each(['interactive_ai_app', 'custom'] as const)(
'creates only project-owned metadata for %s projects',
async (projectType) => {