feat(coding): add project identity UX
This commit is contained in:
@@ -50,4 +50,53 @@ describe('coding project Host facade', () => {
|
||||
['/api/coding/conversations/conversation%2F1', { method: 'DELETE' }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends explicit identity choices and a literal independent-copy confirmation', async () => {
|
||||
hostApiFetch
|
||||
.mockResolvedValueOnce({ snapshot: { project: { id: 'local-1' }, config: { projectId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' }, knowledgeFiles: [] } })
|
||||
.mockResolvedValueOnce({ snapshot: { project: { id: 'local-1' }, config: { projectId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' }, knowledgeFiles: [] } })
|
||||
.mockResolvedValueOnce({ snapshot: { project: { id: 'local-1' }, config: { projectId: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc' }, knowledgeFiles: [] } });
|
||||
const {
|
||||
createCodingProject,
|
||||
isCanonicalCodingProjectId,
|
||||
makeCodingProjectIndependentCopy,
|
||||
resolveCodingProjectIdentity,
|
||||
} = await import('@/lib/coding-projects');
|
||||
|
||||
await createCodingProject({
|
||||
projectPath: 'C:/projects/new',
|
||||
projectType: 'custom',
|
||||
identity: { kind: 'create' },
|
||||
});
|
||||
await resolveCodingProjectIdentity('local-1', {
|
||||
kind: 'bind',
|
||||
projectId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
|
||||
});
|
||||
await makeCodingProjectIndependentCopy('local-1');
|
||||
|
||||
expect(isCanonicalCodingProjectId('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb')).toBe(true);
|
||||
expect(isCanonicalCodingProjectId('BBBBBBBB-BBBB-4BBB-8BBB-BBBBBBBBBBBB')).toBe(false);
|
||||
expect(isCanonicalCodingProjectId('not-a-project-id')).toBe(false);
|
||||
expect(hostApiFetch.mock.calls).toEqual([
|
||||
['/api/coding/projects/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
projectPath: 'C:/projects/new',
|
||||
projectType: 'custom',
|
||||
identity: { kind: 'create' },
|
||||
}),
|
||||
}],
|
||||
['/api/coding/projects/identity', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
localProjectId: 'local-1',
|
||||
identity: { kind: 'bind', projectId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' },
|
||||
}),
|
||||
}],
|
||||
['/api/coding/projects/identity/independent-copy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ localProjectId: 'local-1', confirmed: true }),
|
||||
}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,6 +106,31 @@ describe('coding workspace store', () => {
|
||||
expect(createConversation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes the explicit identity choice through project creation', async () => {
|
||||
const createProject = vi.fn(async () => ({
|
||||
project,
|
||||
config: config([agent('agent-a')]),
|
||||
knowledgeFiles: [],
|
||||
}));
|
||||
const store = createCodingWorkspaceStore({
|
||||
listProjects: vi.fn(async () => ({ projects: [project], activeProjectId: project.id })),
|
||||
getConfig: vi.fn(async () => ({ project, config: config([agent('agent-a')]) })),
|
||||
listConversations: vi.fn(async () => []),
|
||||
createProject,
|
||||
});
|
||||
|
||||
await expect(store.getState().createProject({
|
||||
projectPath: 'C:/projects/new',
|
||||
projectType: 'custom',
|
||||
identity: { kind: 'bind', projectId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' },
|
||||
})).resolves.toEqual(project);
|
||||
expect(createProject).toHaveBeenCalledWith({
|
||||
projectPath: 'C:/projects/new',
|
||||
projectType: 'custom',
|
||||
identity: { kind: 'bind', projectId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' },
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a missing first Conversation once for concurrent callers', async () => {
|
||||
let resolveCreate!: (value: CodingConversationMetadata) => void;
|
||||
const createFlight = new Promise<CodingConversationMetadata>((resolve) => {
|
||||
|
||||
74
tests/unit/project-config-store.test.ts
Normal file
74
tests/unit/project-config-store.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import type { CodingProjectConfigSnapshot } from '@/types/coding-project';
|
||||
|
||||
const resolveCodingProjectIdentity = vi.hoisted(() => vi.fn());
|
||||
const makeCodingProjectIndependentCopy = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/lib/coding-projects', () => ({
|
||||
makeCodingProjectIndependentCopy,
|
||||
resolveCodingProjectIdentity,
|
||||
}));
|
||||
|
||||
const projectId = 'local-project-1';
|
||||
|
||||
function snapshot(identity: string): CodingProjectConfigSnapshot {
|
||||
return {
|
||||
project: {
|
||||
id: projectId,
|
||||
name: 'Local project',
|
||||
createdAt: '2026-08-23T00:00:00.000Z',
|
||||
updatedAt: '2026-08-23T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-08-23T00:00:00.000Z',
|
||||
},
|
||||
config: {
|
||||
schemaVersion: 2,
|
||||
projectType: 'custom',
|
||||
projectId: identity,
|
||||
initialized: true,
|
||||
agents: [],
|
||||
knowledgeDirectory: 'knowledge',
|
||||
legacyConversationNotice: 'none',
|
||||
createdAt: '2026-08-23T00:00:00.000Z',
|
||||
updatedAt: '2026-08-23T00:00:00.000Z',
|
||||
},
|
||||
knowledgeFiles: ['README.md'],
|
||||
};
|
||||
}
|
||||
|
||||
describe('project config identity projection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useProjectConfigStore.setState({
|
||||
configsByProjectId: {},
|
||||
knowledgeByProjectId: {},
|
||||
loadingProjectId: null,
|
||||
errorsByProjectId: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('projects identity resolution snapshots into the renderer cache', async () => {
|
||||
const resolved = snapshot('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa');
|
||||
resolveCodingProjectIdentity.mockResolvedValue(resolved);
|
||||
|
||||
await expect(useProjectConfigStore.getState().resolveIdentity(projectId, { kind: 'create' }))
|
||||
.resolves.toEqual(resolved.config);
|
||||
|
||||
expect(resolveCodingProjectIdentity).toHaveBeenCalledWith(projectId, { kind: 'create' });
|
||||
expect(useProjectConfigStore.getState()).toMatchObject({
|
||||
configsByProjectId: { [projectId]: resolved.config },
|
||||
knowledgeByProjectId: { [projectId]: resolved.knowledgeFiles },
|
||||
});
|
||||
});
|
||||
|
||||
it('projects independent-copy snapshots without writing any renderer-owned data', async () => {
|
||||
const copied = snapshot('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb');
|
||||
makeCodingProjectIndependentCopy.mockResolvedValue(copied);
|
||||
|
||||
await expect(useProjectConfigStore.getState().makeIndependentCopy(projectId))
|
||||
.resolves.toEqual(copied.config);
|
||||
|
||||
expect(makeCodingProjectIndependentCopy).toHaveBeenCalledWith(projectId);
|
||||
expect(useProjectConfigStore.getState().configsByProjectId[projectId]).toEqual(copied.config);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user