feat: simplify code project creation
This commit is contained in:
@@ -6,6 +6,9 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useUserSyncStore } from '@/stores/user-sync';
|
||||
import { codingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config';
|
||||
|
||||
vi.mock('@/components/layout/MainLayout', () => ({
|
||||
MainLayout: () => (
|
||||
@@ -41,6 +44,13 @@ vi.mock('@/pages/Settings', () => ({
|
||||
|
||||
describe('App programming provider initialization gate', () => {
|
||||
const initProviders = vi.fn();
|
||||
const project = {
|
||||
id: 'local-project',
|
||||
name: 'Local project',
|
||||
createdAt: '2026-09-06T00:00:00.000Z',
|
||||
updatedAt: '2026-09-06T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-09-06T00:00:00.000Z',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -49,6 +59,18 @@ describe('App programming provider initialization gate', () => {
|
||||
init: vi.fn(),
|
||||
});
|
||||
useProviderStore.setState({ init: initProviders });
|
||||
codingWorkspaceStore.setState({
|
||||
activeProjectId: project.id,
|
||||
activeProject: project,
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
useProjectConfigStore.setState({
|
||||
load: vi.fn().mockResolvedValue({
|
||||
status: 'valid',
|
||||
config: { ...createCodingProjectConfigV2(), initialized: false },
|
||||
knowledgeFiles: [],
|
||||
}),
|
||||
});
|
||||
useAuthStore.setState({
|
||||
initialized: true,
|
||||
accessToken: 'access-token',
|
||||
@@ -94,6 +116,13 @@ describe('App programming provider initialization gate', () => {
|
||||
await waitFor(() => expect(initProviders).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('opens a valid project without requiring the legacy initialized flag', async () => {
|
||||
await renderAt('/chat');
|
||||
|
||||
expect(await screen.findByText('Programming workspace')).toBeInTheDocument();
|
||||
expect(screen.queryByText('项目尚未初始化')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects an orphaned token without a user identity away from a direct workspace route', async () => {
|
||||
useAuthStore.setState({
|
||||
initialized: true,
|
||||
|
||||
@@ -199,6 +199,24 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('keeps a project with no Agent accessible and offers optional Agent setup', async () => {
|
||||
const openProjectSettings = vi.fn();
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({
|
||||
project,
|
||||
config: { ...configForAgents([]), initialized: false },
|
||||
});
|
||||
projectApi.conversations.mockResolvedValue([]);
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
|
||||
render(<CodingChatPanel onOpenProjectSettings={openProjectSettings} />);
|
||||
|
||||
expect(await screen.findByTestId('coding-chat-empty-agent')).toHaveTextContent('项目已准备好');
|
||||
expect(projectApi.create).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建项目智能体' }));
|
||||
expect(openProjectSettings).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('makes the first-Conversation textarea editable while runtime metadata is held', async () => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config });
|
||||
|
||||
@@ -736,11 +736,11 @@ describe('PI-100 coding core Host contract', () => {
|
||||
|
||||
it('maps persistence failures to a stable fixed Host error', async () => {
|
||||
const result = await setup();
|
||||
const current = await result.projects.getConfig('project-a');
|
||||
const failingProjects = new CodingProjectService(result.store, {
|
||||
writeConfig: async () => { throw new Error(`secret disk path=${result.root}`); },
|
||||
});
|
||||
const failingConversations = new CodingConversationService(failingProjects, result.runtime);
|
||||
const current = await failingProjects.getConfig('project-a');
|
||||
const response = await dispatchHostApiRequest(context({
|
||||
...result,
|
||||
projects: failingProjects,
|
||||
|
||||
@@ -174,6 +174,36 @@ describe('coding project durable identity', () => {
|
||||
expect((await projects.getConfig(local.project.id)).config.projectId).toBe(PROJECT_ID);
|
||||
});
|
||||
|
||||
it('automatically assigns one identity when a legacy project is read concurrently', async () => {
|
||||
const projectPath = await makeRoot();
|
||||
const store = makeStore();
|
||||
const local = await createLocalCodingProject({ projectPath, now: CREATED }, store);
|
||||
const createProjectId = vi.fn(() => PROJECT_ID);
|
||||
const events: string[] = [];
|
||||
const projects = new CodingProjectService(store, {
|
||||
createProjectId,
|
||||
now: () => UPDATED,
|
||||
onProjectIdentityChanging: () => { events.push('invalidate'); },
|
||||
writeConfig: async (filePath, value) => {
|
||||
events.push('write');
|
||||
await writeCodingProjectConfigV2(filePath, value);
|
||||
},
|
||||
onResourcesChanged: () => { events.push('resources'); },
|
||||
});
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
projects.getConfig(local.project.id),
|
||||
projects.getConfig(local.project.id),
|
||||
]);
|
||||
|
||||
expect(first.config.projectId).toBe(PROJECT_ID);
|
||||
expect(second.config.projectId).toBe(PROJECT_ID);
|
||||
expect(createProjectId).toHaveBeenCalledTimes(1);
|
||||
expect(events).toEqual(['invalidate', 'write', 'resources']);
|
||||
await expect(readCodingProjectConfigV2(projectPath))
|
||||
.resolves.toMatchObject({ status: 'valid', config: { projectId: PROJECT_ID, updatedAt: UPDATED } });
|
||||
});
|
||||
|
||||
it('keeps ordinary saves immutable and supports a confirmed independent copy', async () => {
|
||||
const projectPath = await makeRoot();
|
||||
const store = makeStore();
|
||||
@@ -280,8 +310,13 @@ describe('coding project durable identity', () => {
|
||||
const legacyPath = await makeRoot('makelore-project-identity-legacy-');
|
||||
const legacyStore = makeStore();
|
||||
await createLocalCodingProject({ projectPath: legacyPath }, legacyStore);
|
||||
await expect(new CodingProjectService(legacyStore).requireActiveRealProjectWithIdentity())
|
||||
.rejects.toMatchObject({ code: 'CODING_PROJECT_IDENTITY_REQUIRED', status: 409 });
|
||||
const legacyProjects = new CodingProjectService(legacyStore, {
|
||||
createProjectId: () => NEXT_PROJECT_ID,
|
||||
});
|
||||
await expect(legacyProjects.requireActiveRealProjectWithIdentity())
|
||||
.resolves.toMatchObject({ projectId: NEXT_PROJECT_ID });
|
||||
await expect(readCodingProjectConfigV2(legacyPath))
|
||||
.resolves.toMatchObject({ status: 'valid', config: { projectId: NEXT_PROJECT_ID } });
|
||||
});
|
||||
|
||||
it('exposes identity resolution and independent-copy through the Host routes', async () => {
|
||||
|
||||
@@ -2,10 +2,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MainLayout } from '@/components/layout/MainLayout';
|
||||
import { codingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config';
|
||||
|
||||
const routeRenderMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -46,20 +43,6 @@ function renderLayout(path: string) {
|
||||
describe('MainLayout module isolation', () => {
|
||||
beforeEach(() => {
|
||||
routeRenderMock.mockReset();
|
||||
const project = {
|
||||
id: 'local-project',
|
||||
path: '/tmp/local-project',
|
||||
name: 'local-project',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
lastOpenedAt: '',
|
||||
};
|
||||
codingWorkspaceStore.setState({ activeProject: project });
|
||||
useProjectConfigStore.setState({
|
||||
configsByProjectId: {
|
||||
[project.id]: { ...createCodingProjectConfigV2(), initialized: false },
|
||||
},
|
||||
});
|
||||
useSettingsStore.setState({ sidebarCollapsed: false });
|
||||
});
|
||||
|
||||
@@ -89,19 +72,17 @@ describe('MainLayout module isolation', () => {
|
||||
});
|
||||
|
||||
it('does not overlay the local project initialization gate on AI hardware', () => {
|
||||
const load = vi.fn().mockResolvedValue(undefined);
|
||||
useProjectConfigStore.setState({ load });
|
||||
renderLayout('/ai-hardware');
|
||||
|
||||
expect(screen.getByTestId('route-content')).toBeVisible();
|
||||
expect(screen.queryByTestId('project-initialization-gate')).not.toBeInTheDocument();
|
||||
expect(load).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the local project initialization gate for AI programming routes', () => {
|
||||
it('does not restore the removed project initialization gate on AI programming routes', () => {
|
||||
renderLayout('/chat');
|
||||
|
||||
expect(screen.getByTestId('project-initialization-gate')).toBeVisible();
|
||||
expect(screen.getByTestId('route-content')).toBeVisible();
|
||||
expect(screen.queryByTestId('project-initialization-gate')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('titlebar-stub')).toHaveAttribute('data-overlay', 'false');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user