diff --git a/.project-docs/30-worklog/tasks/20260906-remove-project-gate-7c31.md b/.project-docs/30-worklog/tasks/20260906-remove-project-gate-7c31.md new file mode 100644 index 0000000..0ed00a9 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260906-remove-project-gate-7c31.md @@ -0,0 +1,123 @@ +# Task: Remove project creation gate and template selection + +## Identity + +- Task ID: 20260906-remove-project-gate-7c31 +- Mode: Feature +- Branch: codex/20260906-remove-project-gate-7c31-remove-project-gate-7c31 +- Worktree: /Users/inmanx/Documents/makelore-task-remove-project-gate-7c31 +- Base commit: f8eee430f4f4167dc2ecc682ece96c652cdde14b +- Owner: codex +- Status: Ready for integration + +## Scope + +- Remove the user-facing project creation identity/type choices from the Code + sidebar and use the existing safe defaults instead. +- Stop treating `config.initialized` as a navigation/UI gate for an otherwise + valid local Code project. +- Keep an empty project usable by presenting a non-blocking route to project + settings when no Agent exists. +- Keep the durable project ID as an internal service concern: remove it from the + normal Project Configuration UI and automatically assign one when a valid + legacy project lacks it. +- Update focused Renderer tests, the shared Electron project-creation flow, and + README product-state documentation. + +## Intent And Constraints + +- A new project receives a Main-generated local UUID and the existing internal + `interactive_ai_app` type without asking the user to choose either during + creation. +- Preserve `.makelore/project.json`, historical project-type normalization, + underlying identity compatibility endpoints, publish contracts, and existing + custom projects without exposing identity choices to young users. +- Keep the bundled Project Scaffold Skill available as an optional Plugin; it + must not be part of, or a prerequisite for, project creation. +- Missing or invalid project metadata remains an entry error. A valid legacy + config missing only `projectId` is repaired by Main with a generated UUID; + the legacy `initialized` Boolean no longer blocks a valid project. +- Renderer backend access continues through the existing Host API/store seams. + +## Outcome + +- Simplified the Code project creation dialog to directory selection only. + Main still generates the local project UUID, while the Renderer writes the + existing internal `interactive_ai_app` default without presenting identity, + type, or template choices. +- Removed the `initialized` navigation redirect and full-workspace overlay for + valid project metadata. Missing or invalid metadata still routes to Project + Configuration. +- Added an empty-Agent state in the coding chat so a newly created project opens + immediately and offers a direct, non-blocking route to create its first Agent. +- Reworded Project Configuration save affordances so the retained compatibility + write to `initialized` is no longer described as an initialization ceremony. +- Removed the Project Configuration identity card, raw UUID, bind form, and + independent-copy action. Main now serializes automatic UUID assignment for a + valid legacy project missing `projectId`, including concurrent reads, while + retaining the underlying compatibility endpoints and immutable schema rules. +- Replaced the last creation-dialog identity terminology with youth-readable + copy: only the folder choice is explained; all other setup is automatic. +- Preserved project schemas, historical project types, publishing contracts, + existing custom projects, and the optional bundled Project Scaffold + Plugin/Skill. +- Updated README product-state documentation and focused unit/Electron coverage. +- Diagnosed a post-verification UI mismatch as a development-runtime issue: the + visible Electron window had been launched from the separate Plugins/settings + task worktree, whose source still contained the former identity choices. The + stale process group was stopped and the verified feature worktree was started + on the standard development ports. + +## Verification + +- `corepack pnpm install --frozen-lockfile` — passed with the repository-pinned + pnpm 10.33.4. +- `corepack pnpm run typecheck` — passed. +- Focused ESLint across all changed source/test files — passed. +- Focused Renderer/Main tests — 60 passed across App routing, Main layout, + Sidebar, CodingChatPanel, Project Configuration, config stores, and serialized + automatic legacy identity assignment. +- Adjacent project-schema/store tests — 34 passed. +- `corepack pnpm run lint:check` — passed with zero errors; five pre-existing + warnings remain in unrelated Home/Makelore files. +- `corepack pnpm test` — passed: 1,862 ordinary tests passed, three skipped, and + the isolated pressure test passed 1/1. +- `corepack pnpm run build:vite` — passed for Renderer, Main, Preload, and the + release utility worker. +- `corepack pnpm exec playwright test tests/e2e/coding-project-identity.spec.ts tests/e2e/project-configuration-skills.spec.ts` + — passed (2/2), covering directory-only creation, direct chat entry, absence + of all identity terminology and controls, automatic legacy UUID repair, and + the empty-Agent settings route. +- Live runtime verification after restart — Vite PID 16968 and Electron PID + 16975 both resolve to this task worktree; the active Renderer command reports + the same `app-path`, and the source served on port 5173 contains the automatic + setup copy while none of `项目身份`, `创建新的项目 ID`, or `绑定已有项目 ID` + is present. +- `git diff --check` — passed. + +## Follow-ups + +- Promote the project-creation workflow change into ADR-008 and the canonical + project architecture/domain documentation during a serialized Integration + task. +- Physically removing the optional bundled Project Scaffold Plugin/Skill, if + desired, should be a separate explicit decision because it affects packaged + resources and release coverage; this task only removes it from the creation + path. + +## Promotion Candidates + +- Target: ADR-008 and canonical project-creation architecture/domain documents. + Proposal: amend the user-facing creation workflow so identity and project type + are safe defaults instead of required choices, and treat `initialized` as a + compatibility field rather than a workspace navigation gate. Durable project + identity remains Main-owned and is generated automatically for new projects + and valid legacy configs that lack it; raw UUIDs, bind choices, and copy + semantics must not return to ordinary youth-facing UI. Existing custom + projects, type normalization, publishing authority, compatibility endpoints, + and the optional Scaffold Skill remain. Future impact: product documentation + and future creation/configuration UI must not restore these choices as + mandatory or visible setup. Human confirmation is represented by the user's + explicit statements that the product targets young users and service-required + identity should default automatically; canonical promotion still belongs to a + serialized Integration task. diff --git a/README.md b/README.md index 75e6478..770f081 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Makelore 是一个面向软件、视觉创作与智能机器人的 AI 桌面工 应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与三个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。 -作品广场、素材广场、独立发布上传和云部署页面不属于 Makelore 2.0 工作台。新建项目可选择“交互式 AI 应用”或“自定义项目”,创建服务只生成 `.makelore/project.json` 与 `knowledge/`;历史 `mini_game` / `mini_program` 配置在读取时归一为交互式 AI 应用,但不会因读取被改写。用户获取、启用并为智能体分配官方 bundled `makelore.project-scaffold` 插件后,可明确调用 `makelore-project-scaffold` Skill,以不覆盖既有路径的方式生成固定六文件 Vite 起步工程;该 Skill 不安装依赖、不联网、不构建、不上传或提审。交互式 AI 应用的项目配置底部提供“一键提交审核”;Main 自动预检、安全打包并提交,构建通过后进入运营审核,审核通过即直接发布。首次创建必须选择 PNG、JPEG 或 WebP 项目封面,并通过 Main-owned multipart 原子接口同时保存资料与封面;已有 draft/published 只提交新版本并沿用平台现有资料与封面。自定义项目只创建工作空间,不配置默认发布方式。项目成果预览 `/deliverables` 继续保留。 +作品广场、素材广场、独立发布上传和云部署页面不属于 Makelore 2.0 工作台。新建 Code 项目只要求选择目录:Main 自动生成内部项目 ID,并以内部 `interactive_ai_app` 类型创建 `.makelore/project.json` 与 `knowledge/`,不再让用户选择或查看项目身份、项目类型和模板;缺少项目 ID 的旧项目在读取时由 Main 自动补全。创建成功后直接进入对话工作区,未创建智能体时只显示可选的设置入口,不再用初始化门禁遮挡工作区。已有 `custom` 项目继续受支持;历史 `mini_game` / `mini_program` 配置在读取时归一为交互式 AI 应用,但不会因读取被改写。用户获取、启用并为智能体分配官方 bundled `makelore.project-scaffold` 插件后,仍可按需明确调用 `makelore-project-scaffold` Skill,以不覆盖既有路径的方式生成固定六文件 Vite 起步工程;该 Skill 不是创建前置条件,也不安装依赖、不联网、不构建、不上传或提审。交互式 AI 应用的项目配置底部提供“一键提交审核”;Main 自动预检、安全打包并提交,构建通过后进入运营审核,审核通过即直接发布。首次创建必须选择 PNG、JPEG 或 WebP 项目封面,并通过 Main-owned multipart 原子接口同时保存资料与封面;已有 draft/published 只提交新版本并沿用平台现有资料与封面。项目成果预览 `/deliverables` 继续保留。 ## 当前产品状态 diff --git a/electron/coding-projects/project-service.ts b/electron/coding-projects/project-service.ts index 45f4ee0..d74c30f 100644 --- a/electron/coding-projects/project-service.ts +++ b/electron/coding-projects/project-service.ts @@ -211,21 +211,7 @@ export class CodingProjectService { ); } } - const config = await this.readCurrentConfig(projectPath); - if (!config) { - throw new CodingProjectServiceError( - 409, - 'CODING_PROJECT_CONFIG_INVALID', - 'Coding project configuration is unavailable', - ); - } - if (!config.projectId) { - throw new CodingProjectServiceError( - 409, - 'CODING_PROJECT_IDENTITY_REQUIRED', - 'A durable coding project identity is required', - ); - } + const config = (await this.getConfig(project.id)).config; return { project, path: projectPath, projectId: config.projectId }; } @@ -335,19 +321,10 @@ export class CodingProjectService { async getConfig(projectId?: string): Promise { const project = projectId ? await this.getProject(projectId) : await this.requireActiveProject(); - const config = await this.readCurrentConfig(project.path); - if (!config) { - throw new CodingProjectServiceError( - 409, - 'CODING_PROJECT_CONFIG_INVALID', - 'Coding project configuration is unavailable', - ); - } - return { - project, - config, - knowledgeFiles: await this.listKnowledgeFiles(project.path), - }; + const config = await this.requireCurrentConfig(project); + return config.projectId + ? await this.projectConfigSnapshot(project, config) + : await this.ensureProjectIdentity(project); } async saveConfig(projectId: string, value: unknown): Promise { @@ -379,8 +356,8 @@ export class CodingProjectService { ): Promise { const project = await this.getProject(localProjectId); return await this.serializeIdentityTransition(project.path, async () => { - const current = await this.getConfig(project.id); - if (current.config.projectId !== undefined) { + const current = await this.requireCurrentConfig(project); + if (current.projectId !== undefined) { throw new CodingProjectServiceError( 409, 'CODING_PROJECT_IDENTITY_IMMUTABLE', @@ -397,7 +374,7 @@ export class CodingProjectService { nextProjectId, ); const next = { - ...current.config, + ...current, projectId: nextProjectId, updatedAt: this.options.now?.() ?? new Date().toISOString(), }; @@ -407,11 +384,7 @@ export class CodingProjectService { storageFailure(error); } await this.options.onResourcesChanged?.(project); - return { - project: current.project, - config: next, - knowledgeFiles: await this.listKnowledgeFiles(current.project.path), - }; + return await this.projectConfigSnapshot(project, next); }); } @@ -428,8 +401,8 @@ export class CodingProjectService { } const project = await this.getProject(localProjectId); return await this.serializeIdentityTransition(project.path, async () => { - const current = await this.getConfig(project.id); - if (current.config.projectId === undefined) { + const current = await this.requireCurrentConfig(project); + if (current.projectId === undefined) { throw new CodingProjectServiceError( 409, 'CODING_PROJECT_IDENTITY_REQUIRED', @@ -442,11 +415,11 @@ export class CodingProjectService { ); await this.options.onProjectIdentityChanging?.( project, - current.config.projectId, + current.projectId, nextProjectId, ); const next = { - ...current.config, + ...current, projectId: nextProjectId, updatedAt: this.options.now?.() ?? new Date().toISOString(), }; @@ -456,11 +429,7 @@ export class CodingProjectService { storageFailure(error); } await this.options.onResourcesChanged?.(project); - return { - project: current.project, - config: next, - knowledgeFiles: await this.listKnowledgeFiles(current.project.path), - }; + return await this.projectConfigSnapshot(project, next); }); } @@ -540,6 +509,61 @@ export class CodingProjectService { }); } + private async ensureProjectIdentity( + project: CodingProject, + ): Promise { + return await this.serializeIdentityTransition(project.path, async () => { + const current = await this.requireCurrentConfig(project); + if (current.projectId) { + return await this.projectConfigSnapshot(project, current); + } + const nextProjectId = resolveProjectIdentity( + { kind: 'create' }, + this.options.createProjectId ?? randomUUID, + ); + await this.options.onProjectIdentityChanging?.( + project, + undefined, + nextProjectId, + ); + const next = { + ...current, + projectId: nextProjectId, + updatedAt: this.options.now?.() ?? new Date().toISOString(), + }; + try { + await (this.options.writeConfig ?? writeCodingProjectConfigV2)(project.path, next); + } catch (error) { + storageFailure(error); + } + await this.options.onResourcesChanged?.(project); + return await this.projectConfigSnapshot(project, next); + }); + } + + private async requireCurrentConfig(project: CodingProject): Promise { + const config = await this.readCurrentConfig(project.path); + if (!config) { + throw new CodingProjectServiceError( + 409, + 'CODING_PROJECT_CONFIG_INVALID', + 'Coding project configuration is unavailable', + ); + } + return config; + } + + private async projectConfigSnapshot( + project: CodingProject, + config: CodingProjectConfigV2, + ): Promise { + return { + project, + config, + knowledgeFiles: await this.listKnowledgeFiles(project.path), + }; + } + private async listKnowledgeFiles(projectPath: string): Promise { try { const entries = await readdir(path.join(projectPath, 'knowledge'), { withFileTypes: true }); diff --git a/src/App.tsx b/src/App.tsx index f1cd4a1..3bd2cb7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -226,7 +226,7 @@ function ProjectChatRoute() { const configResult = await loadProjectConfig(project.id); if (!cancelled) { setResolvedProjectKey(project.id); - setResolvedRoute(configResult.status === 'valid' && Boolean(configResult.config?.initialized) ? 'chat' : 'config'); + setResolvedRoute(configResult.status === 'valid' && Boolean(configResult.config) ? 'chat' : 'config'); } } catch { if (!cancelled) { diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 06c2037..55b3837 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -2,15 +2,11 @@ * Main Layout Component * TitleBar at top, then sidebar + content below. */ -import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { Outlet, useLocation } from 'react-router-dom'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { LockKeyhole, Settings2 } from 'lucide-react'; import { Sidebar } from './Sidebar'; import { TitleBar } from './TitleBar'; -import { Button } from '@/components/ui/button'; import { getAiModuleForPath } from '@/lib/ai-modules'; -import { useCodingWorkspaceStore } from '@/stores/coding-workspace'; -import { useProjectConfigStore } from '@/stores/project-config'; import { useSettingsStore } from '@/stores/settings'; import { cn } from '@/lib/utils'; import type { SidebarPeekSource } from './sidebar-peek'; @@ -18,12 +14,8 @@ import type { SidebarPeekSource } from './sidebar-peek'; const SIDEBAR_PEEK_CLOSE_DELAY_MS = 180; export function MainLayout() { - const activeProject = useCodingWorkspaceStore((state) => state.activeProject); const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed); - const config = useProjectConfigStore((state) => activeProject ? state.configsByProjectId[activeProject.id] : undefined); - const load = useProjectConfigStore((state) => state.load); const location = useLocation(); - const navigate = useNavigate(); const [sidebarPeekOpen, setSidebarPeekOpen] = useState(false); const sidebarPeekCloseTimerRef = useRef | null>(null); const sidebarPeekSourcesRef = useRef>({ @@ -33,13 +25,9 @@ export function MainLayout() { titlebar: false, }); const activeModule = getAiModuleForPath(location.pathname); - const isProgrammingModule = activeModule === 'programming'; const isPaintingModule = activeModule === 'painting'; const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/'); const isChatWorkspace = location.pathname === '/chat'; - const isInitializationSafeRoute = location.pathname === '/project-config' - || location.pathname === '/plugins' - || !isProgrammingModule; const handleSidebarPeekChange = useCallback((open: boolean, source: SidebarPeekSource) => { if (!sidebarCollapsed) return; @@ -84,11 +72,6 @@ export function MainLayout() { if (sidebarPeekCloseTimerRef.current) clearTimeout(sidebarPeekCloseTimerRef.current); }, []); - useEffect(() => { - if (activeProject && isProgrammingModule) void load(activeProject.id).catch(() => undefined); - }, [activeProject, isProgrammingModule, load]); - - const initializationBlocked = Boolean(activeProject && (!config || !config.initialized) && !isInitializationSafeRoute); return (
{/* Title bar: drag region on macOS, icon + controls on Windows */} @@ -117,16 +100,6 @@ export function MainLayout() { )} > - {initializationBlocked ? ( -
-
- -

项目尚未初始化

-

请先在项目配置中完成至少一个手动 Agent 的名称、模型和职责设置。

- -
-
- ) : null}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index f5e9b8b..ab6e39a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -48,11 +48,8 @@ import { useSettingsStore } from '@/stores/settings'; import { useAuthStore, type AuthUser } from '@/stores/auth'; import { useCodingWorkspaceStore } from '@/stores/coding-workspace'; import { useProviderStore } from '@/stores/providers'; -import { isCanonicalCodingProjectId } from '@/lib/coding-projects'; import type { CodingProjectSummary } from '@/types/coding-project'; import { useProjectConfigStore } from '@/stores/project-config'; -import type { ProjectType } from '../../../shared/project-config'; -import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts'; import { toast } from 'sonner'; type OpenDialogResult = { @@ -72,7 +69,6 @@ type ProjectEntryError = { }; type ProjectDirectoryMode = 'use-selected-directory' | 'create-child-directory'; -type ProjectIdentityKind = ProjectIdentityChoice['kind']; function getFolderName(pathValue: string): string { const trimmed = pathValue.trim(); @@ -166,9 +162,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi const [newProjectName, setNewProjectName] = useState(''); const [newProjectSelectedPath, setNewProjectSelectedPath] = useState(''); const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState('use-selected-directory'); - const [newProjectType, setNewProjectType] = useState('interactive_ai_app'); - const [newProjectIdentityKind, setNewProjectIdentityKind] = useState('create'); - const [newProjectIdentityProjectId, setNewProjectIdentityProjectId] = useState(''); const [createProjectError, setCreateProjectError] = useState(null); const [creatingProject, setCreatingProject] = useState(false); const [projectEntryError, setProjectEntryError] = useState(null); @@ -186,7 +179,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi const isPaintingModule = activeModule === 'painting'; const isRobotModule = activeModule === 'robot'; const isPromptMuseum = location.pathname === '/image-prompts' || location.pathname.startsWith('/image-prompts/'); - const projectConfigPath = '/project-config'; const visibleProjects = projects; const selectedProjectFolderName = getFolderName(newProjectSelectedPath); const accountName = userProfile?.displayName?.trim() || getAuthUserDisplayName(authUser) || '未登录用户'; @@ -397,9 +389,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi setNewProjectName(''); setNewProjectSelectedPath(''); setNewProjectDirectoryMode('use-selected-directory'); - setNewProjectType('interactive_ai_app'); - setNewProjectIdentityKind('create'); - setNewProjectIdentityProjectId(''); setCreateProjectError(null); setCreateDialogOpen(true); }; @@ -407,9 +396,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi const closeCreateProject = () => { if (creatingProject) return; setCreateDialogOpen(false); - setNewProjectType('interactive_ai_app'); - setNewProjectIdentityKind('create'); - setNewProjectIdentityProjectId(''); setCreateProjectError(null); }; @@ -436,9 +422,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi return; } await setActiveProject(project.id); - navigate(result.config.initialized - ? '/chat' - : projectConfigPath); + navigate('/chat'); }; const readAndEnterProject = async (project: CodingProjectSummary) => { @@ -462,20 +446,12 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi setCreateProjectError('请选择项目路径'); return; } - const boundProjectId = newProjectIdentityProjectId.trim(); - if (newProjectIdentityKind === 'bind' && !isCanonicalCodingProjectId(boundProjectId)) { - setCreateProjectError('请输入有效的项目 ID(小写 UUID)。'); - return; - } - setCreatingProject(true); setCreateProjectError(null); try { const project = await createProject({ - projectType: newProjectType, - identity: newProjectIdentityKind === 'create' - ? { kind: 'create' } - : { kind: 'bind', projectId: boundProjectId }, + projectType: 'interactive_ai_app', + identity: { kind: 'create' }, ...(newProjectDirectoryMode === 'create-child-directory' ? { parentPath: selectedPath, projectName } : { projectPath: selectedPath }), @@ -756,7 +732,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
新建项目 - 默认直接使用所选文件夹,也可以在所选位置新建下级文件夹。 + 只需选择项目文件夹,其他设置会自动准备好。
+ ) : null} + + + ) : (
)} diff --git a/src/pages/ProjectConfiguration/index.tsx b/src/pages/ProjectConfiguration/index.tsx index 1e86629..20c9200 100644 --- a/src/pages/ProjectConfiguration/index.tsx +++ b/src/pages/ProjectConfiguration/index.tsx @@ -11,20 +11,16 @@ import { ProjectPublishAction } from '@/components/works/ProjectPublishAction'; import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options'; import { getCodingSkills } from '@/lib/coding-product-tools'; import { abortCodingConversation } from '@/lib/coding-conversations'; -import { isCanonicalCodingProjectId } from '@/lib/coding-projects'; import { getSkillDisplayInfo } from '@/lib/skill-display'; import { getAgentAvatarSrc } from '@/lib/agent-avatars'; -import { cn } from '@/lib/utils'; import { codingConversationStore } from '@/stores/coding-conversations'; import { useCodingWorkspaceStore } from '@/stores/coding-workspace'; import { useProjectConfigStore } from '@/stores/project-config'; import { useProviderStore } from '@/stores/providers'; import type { CodingProjectAgent, CodingProjectConfig } from '@/types/coding-project'; -import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts'; const EMPTY_FILES: string[] = []; type DrawerMode = 'models' | 'knowledge' | 'skills' | null; -type ProjectIdentityKind = ProjectIdentityChoice['kind']; type SkillStructureEntry = { path: string; type: 'file' | 'directory' }; type SkillInfo = { id: string; @@ -134,8 +130,6 @@ export function ProjectConfiguration() { const knowledge = useProjectConfigStore((state) => activeProject ? state.knowledgeByProjectId[activeProject.id] ?? EMPTY_FILES : EMPTY_FILES); const load = useProjectConfigStore((state) => state.load); const save = useProjectConfigStore((state) => state.save); - const resolveIdentity = useProjectConfigStore((state) => state.resolveIdentity); - const makeIndependentCopy = useProjectConfigStore((state) => state.makeIndependentCopy); const uploadKnowledge = useProjectConfigStore((state) => state.uploadKnowledge); const removeProjectConfig = useProjectConfigStore((state) => state.remove); const providerAccounts = useProviderStore((state) => state.accounts); @@ -147,11 +141,6 @@ export function ProjectConfiguration() { const [activeAgentId, setActiveAgentId] = useState(null); const [skills, setSkills] = useState([]); const [saving, setSaving] = useState(false); - const [identityKind, setIdentityKind] = useState('create'); - const [identityProjectId, setIdentityProjectId] = useState(''); - const [identitySaving, setIdentitySaving] = useState(false); - const [identityError, setIdentityError] = useState(null); - const [independentCopyDialogOpen, setIndependentCopyDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [partnerDialogOpen, setPartnerDialogOpen] = useState(false); const [archiveAgentId, setArchiveAgentId] = useState(null); @@ -174,11 +163,6 @@ export function ProjectConfiguration() { useEffect(() => { if (activeProject) void load(activeProject.id); }, [activeProject, load]); useEffect(() => { if (stored) setDraft(structuredClone(stored)); }, [stored]); - useEffect(() => { - setIdentityKind('create'); - setIdentityProjectId(''); - setIdentityError(null); - }, [activeProject?.id, draft?.projectId]); useEffect(() => { const requestedAgentId = new URLSearchParams(location.search).get('agent'); if (!requestedAgentId || !draft?.agents.some((agent) => agent.id === requestedAgentId)) return; @@ -263,46 +247,13 @@ export function ProjectConfiguration() { await save(activeProject.id, { ...draft, initialized: true }); await reloadWorkspace(); playSaveConfirmation(); - toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成'); + toast.success('项目配置已保存'); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setSaving(false); } }; - const handleResolveIdentity = async () => { - const boundProjectId = identityProjectId.trim(); - if (identityKind === 'bind' && !isCanonicalCodingProjectId(boundProjectId)) { - setIdentityError('请输入有效的项目 ID(小写 UUID)。'); - return; - } - setIdentitySaving(true); - setIdentityError(null); - try { - await resolveIdentity(activeProject.id, identityKind === 'create' - ? { kind: 'create' } - : { kind: 'bind', projectId: boundProjectId }); - await reloadWorkspace(); - toast.success('项目身份已设置,之后不可通过普通保存修改。'); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - setIdentityError(message); - toast.error(message); - } finally { - setIdentitySaving(false); - } - }; - const handleIndependentCopy = async () => { - setIdentitySaving(true); - try { - await makeIndependentCopy(activeProject.id); - await reloadWorkspace(); - setIndependentCopyDialogOpen(false); - toast.success('已设为独立副本,当前文件夹已获得新的项目 ID。'); - } finally { - setIdentitySaving(false); - } - }; const handleKnowledge = async (file?: File) => { if (!file) return; try { await uploadKnowledge(activeProject.id, file); toast.success(`已上传到 knowledge/${file.name}`); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } }; const handleDeleteProject = async () => { await removeProject(activeProject.id); @@ -341,120 +292,6 @@ export function ProjectConfiguration() {

项目配置

-
- {draft.projectId ? ( -
-
-

项目身份

-

当前项目 ID

- {draft.projectId} -

- 同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。 -

-
- -
- ) : ( -
{ - event.preventDefault(); - void handleResolveIdentity(); - }} - > -
-

为旧项目设置身份

-

- 这是一个没有项目 ID 的旧项目。此操作只会为当前文件夹设置一次身份;项目文件和云端数据都不会被复制。 -

-
-
- 选择项目 ID -
- - -
-
- {identityKind === 'bind' ? ( -
- - { - setIdentityProjectId(event.target.value); - setIdentityError(null); - }} - placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - autoComplete="off" - spellCheck={false} - className="w-full rounded-md border border-foreground/15 bg-white px-3 py-2 font-mono text-sm font-medium outline-none focus:ring-2 focus:ring-brand/20" - disabled={identitySaving} - /> -

- 同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。 -

-
- ) : null} - {identityError ?

{identityError}

: null} -
- -
-
- )} -
} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} />

项目智能体

智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。

{draft.agents.filter((agent) => !agent.archivedAt).map((agent) => { setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ?
当前项目还没有智能体,点击右上角的 + 创建。
: null}
{draft.agents.some((agent) => agent.archivedAt) ?

已归档

{draft.agents.filter((agent) => agent.archivedAt).map((agent) => )}
: null}
@@ -469,7 +306,7 @@ export function ProjectConfiguration() { buttonSize="sm" /> ) : null} - +
@@ -513,16 +350,6 @@ export function ProjectConfiguration() { onConfirm={handleDeleteProject} onError={(error) => toast.error(error instanceof Error ? error.message : String(error))} /> - setIndependentCopyDialogOpen(false)} - onConfirm={handleIndependentCopy} - onError={(error) => toast.error(error instanceof Error ? error.message : String(error))} - /> ; } diff --git a/tests/e2e/coding-project-identity.spec.ts b/tests/e2e/coding-project-identity.spec.ts index 182be06..6e5e054 100644 --- a/tests/e2e/coding-project-identity.spec.ts +++ b/tests/e2e/coding-project-identity.spec.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; -const BOUND_PROJECT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'; +const CANONICAL_PROJECT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; async function readProjectConfig(projectPath: string): Promise> { return JSON.parse(await readFile(path.join(projectPath, '.makelore', 'project.json'), 'utf8')) as Record; @@ -25,90 +25,50 @@ async function selectProgrammingProjectFolder( } test.describe('Coding project identity UX', () => { - test('supports default create, bind validation, cancellation, legacy resolution, and independent copies', async ({ launchElectronApp }) => { - const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-identity-e2e-')); + test('keeps the service identity automatic and invisible to young users', async ({ launchElectronApp }) => { + const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-identity-e2e-')); const app = await launchElectronApp({ skipSetup: true }); try { const page = await selectProgrammingProjectFolder(app, projectPath); await page.getByTestId('sidebar-create-project').click(); const createDialog = page.getByRole('dialog', { name: '新建项目' }); - await expect(createDialog.getByRole('radio', { name: '创建新的项目 ID' })).toBeChecked(); - await createDialog.getByRole('radio', { name: '绑定已有项目 ID' }).click(); - await createDialog.getByRole('textbox', { name: '已有项目 ID' }).fill('not-a-project-id'); + await expect(createDialog.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); + await expect(createDialog.getByRole('radio', { name: '创建新的项目 ID' })).toHaveCount(0); + await expect(createDialog.getByRole('radio', { name: '绑定已有项目 ID' })).toHaveCount(0); await createDialog.getByRole('button', { name: '选择路径' }).click(); + await expect(createDialog.getByLabel('项目路径')).toHaveValue(projectPath); await createDialog.getByRole('button', { name: '确认创建' }).click(); - await expect(createDialog).toContainText('请输入有效的项目 ID(小写 UUID)。'); - await createDialog.getByRole('button', { name: '取消' }).first().click(); - await expect(page.getByRole('dialog', { name: '新建项目' })).toHaveCount(0); - await page.getByTestId('sidebar-create-project').click(); - const defaultCreateDialog = page.getByRole('dialog', { name: '新建项目' }); - await expect(defaultCreateDialog.getByRole('radio', { name: '创建新的项目 ID' })).toBeChecked(); - await defaultCreateDialog.getByRole('button', { name: '选择路径' }).click(); - await expect(defaultCreateDialog.getByLabel('项目路径')).toHaveValue(projectPath); - await defaultCreateDialog.getByRole('button', { name: '确认创建' }).click(); - - await expect(page.getByTestId('project-configuration-page')).toBeVisible(); + await expect(page.getByTestId('coding-chat-empty-agent')).toBeVisible(); const createdConfig = await readProjectConfig(projectPath); - const generatedProjectId = createdConfig.projectId; - expect(generatedProjectId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u); - await expect(page.getByTestId('project-identity-value')).toHaveText(String(generatedProjectId)); - await expect(page.getByText('同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。')).toBeVisible(); + const generatedProjectId = String(createdConfig.projectId); + expect(generatedProjectId).toMatch(CANONICAL_PROJECT_ID); - await writeFile(path.join(projectPath, 'identity-sentinel.txt'), 'keep this file in place'); - const independentButton = page.getByTestId('independent-copy-button'); - await independentButton.click(); - const independentDialog = page.getByRole('dialog', { name: '设为独立副本?' }); - await expect(independentDialog).toContainText('不会移动或复制文件'); - await expect(independentDialog).toContainText('不会复制云端开发数据'); - await independentDialog.getByRole('button', { name: '取消' }).click(); - await expect(independentDialog).toHaveCount(0); - expect((await readProjectConfig(projectPath)).projectId).toBe(generatedProjectId); - - await independentButton.click(); - await page.getByRole('dialog', { name: '设为独立副本?' }).getByRole('button', { name: '确认设为独立副本' }).click(); - await expect(page.getByTestId('project-identity-value')).not.toHaveText(String(generatedProjectId)); - const independentConfig = await readProjectConfig(projectPath); - expect(independentConfig.projectId).not.toBe(generatedProjectId); - expect(await readFile(path.join(projectPath, 'identity-sentinel.txt'), 'utf8')).toBe('keep this file in place'); + await page.getByRole('button', { name: '创建项目智能体' }).click(); + await expect(page.getByTestId('project-configuration-page')).toBeVisible(); + await expect(page.getByTestId('project-identity-card')).toHaveCount(0); + await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); + await expect(page.getByTestId('independent-copy-button')).toHaveCount(0); + await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); const legacyConfig = await readProjectConfig(projectPath); delete legacyConfig.projectId; - await writeFile(path.join(projectPath, '.makelore', 'project.json'), JSON.stringify(legacyConfig, null, 2)); + await writeFile( + path.join(projectPath, '.makelore', 'project.json'), + JSON.stringify(legacyConfig, null, 2), + ); await page.reload(); - await expect(page.getByTestId('legacy-project-identity-card')).toBeVisible(); - - const legacyCard = page.getByTestId('legacy-project-identity-card'); - await legacyCard.getByRole('radio', { name: '绑定已有项目 ID' }).click(); - await legacyCard.getByRole('textbox', { name: '已有项目 ID' }).fill(BOUND_PROJECT_ID); - await legacyCard.getByTestId('resolve-project-identity-button').click(); - await expect(page.getByTestId('project-identity-value')).toHaveText(BOUND_PROJECT_ID); - await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); - } finally { - await closeElectronApp(app); - await rm(projectPath, { recursive: true, force: true }); - } - }); - - test('creates a project bound to an explicit canonical project ID', async ({ launchElectronApp }) => { - const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-bound-identity-e2e-')); - const app = await launchElectronApp({ skipSetup: true }); - - try { - const page = await selectProgrammingProjectFolder(app, projectPath); - await page.getByTestId('sidebar-create-project').click(); - const createDialog = page.getByRole('dialog', { name: '新建项目' }); - await createDialog.getByRole('radio', { name: '绑定已有项目 ID' }).click(); - await expect(createDialog.getByText('同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。')).toBeVisible(); - await createDialog.getByRole('textbox', { name: '已有项目 ID' }).fill(BOUND_PROJECT_ID); - await createDialog.getByRole('button', { name: '选择路径' }).click(); - await createDialog.getByRole('button', { name: '确认创建' }).click(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); - await expect(page.getByTestId('project-identity-value')).toHaveText(BOUND_PROJECT_ID); - expect((await readProjectConfig(projectPath)).projectId).toBe(BOUND_PROJECT_ID); + const repairedConfig = await readProjectConfig(projectPath); + const repairedProjectId = String(repairedConfig.projectId); + expect(repairedProjectId).toMatch(CANONICAL_PROJECT_ID); + expect(repairedProjectId).not.toBe(generatedProjectId); + await expect(page.getByTestId('project-identity-card')).toHaveCount(0); + await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); + await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); } finally { await closeElectronApp(app); await rm(projectPath, { recursive: true, force: true }); diff --git a/tests/e2e/project-configuration-skills.spec.ts b/tests/e2e/project-configuration-skills.spec.ts index 5101eba..4cca7ad 100644 --- a/tests/e2e/project-configuration-skills.spec.ts +++ b/tests/e2e/project-configuration-skills.spec.ts @@ -20,8 +20,10 @@ test.describe('Project configuration skills', () => { await page.getByTestId('ai-module-option-programming').click(); await expect(page.getByTestId('main-layout')).toBeVisible(); await page.getByTestId('sidebar-create-project').click(); - await expect(page.getByRole('radio', { name: '交互式 AI 应用' })).toBeChecked(); - await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked(); + await expect(page.getByRole('radio', { name: '创建新的项目 ID' })).toHaveCount(0); + await expect(page.getByRole('radio', { name: '绑定已有项目 ID' })).toHaveCount(0); + await expect(page.getByRole('radio', { name: '交互式 AI 应用' })).toHaveCount(0); + await expect(page.getByRole('radio', { name: '自定义项目' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '小游戏' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '小程序' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked(); @@ -29,7 +31,16 @@ test.describe('Project configuration skills', () => { await expect(page.getByLabel('项目路径')).toHaveValue(parentPath); await page.getByRole('button', { name: '确认创建' }).click(); + await expect(page.getByTestId('coding-chat-empty-agent')).toBeVisible(); + await expect(page.getByTestId('project-initialization-gate')).toHaveCount(0); + await page.getByRole('button', { name: '创建项目智能体' }).click(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); + await expect(page.getByTestId('project-identity-card')).toHaveCount(0); + await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); + await expect(page.getByTestId('independent-copy-button')).toHaveCount(0); + await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); + await expect(page.getByRole('button', { name: '保存项目配置' })).toBeVisible(); + await expect(page.getByRole('button', { name: '确认并完成初始化' })).toHaveCount(0); await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0); await expect(page.getByText(/新项目还没有智能体/)).toHaveCount(0); await expect(page.getByTestId('project-configuration-actions')).toBeVisible(); @@ -96,6 +107,9 @@ test.describe('Project configuration skills', () => { }, null, 2)); await page.reload(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); + await expect(page.getByTestId('project-identity-card')).toHaveCount(0); + await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); + await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); await page.getByTestId('resource-card-skills').click(); await expect(page.getByTestId('superpowers-card')).toHaveCount(0); diff --git a/tests/unit/app-module-provider-gate.test.tsx b/tests/unit/app-module-provider-gate.test.tsx index edc7385..9bd7fc6 100644 --- a/tests/unit/app-module-provider-gate.test.tsx +++ b/tests/unit/app-module-provider-gate.test.tsx @@ -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, diff --git a/tests/unit/coding-chat-panel.test.tsx b/tests/unit/coding-chat-panel.test.tsx index 28b85cf..9a04bf3 100644 --- a/tests/unit/coding-chat-panel.test.tsx +++ b/tests/unit/coding-chat-panel.test.tsx @@ -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(); + + 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 }); diff --git a/tests/unit/coding-core-routes.test.ts b/tests/unit/coding-core-routes.test.ts index adafc90..585d073 100644 --- a/tests/unit/coding-core-routes.test.ts +++ b/tests/unit/coding-core-routes.test.ts @@ -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, diff --git a/tests/unit/coding-project-identity.test.ts b/tests/unit/coding-project-identity.test.ts index 03d7711..0705449 100644 --- a/tests/unit/coding-project-identity.test.ts +++ b/tests/unit/coding-project-identity.test.ts @@ -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 () => { diff --git a/tests/unit/main-layout-module-gate.test.tsx b/tests/unit/main-layout-module-gate.test.tsx index 37bb229..eda5624 100644 --- a/tests/unit/main-layout-module-gate.test.tsx +++ b/tests/unit/main-layout-module-gate.test.tsx @@ -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'); });