diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts index 0ac2dcb9f..134b7a2de 100644 --- a/packages/server/src/server/paseo-worktree-service.test.ts +++ b/packages/server/src/server/paseo-worktree-service.test.ts @@ -73,6 +73,50 @@ test("creates a worktree and registers it in the source workspace project withou expect(events).toEqual([`workspace:${result.workspace.workspaceId}`]); }); +test("refreshes a source project that became Git while creating a worktree", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const deps = createDeps(); + const sourceProject = { + ...createPersistedProjectRecordForTest({ + projectId: "prj_existing-source", + rootPath: repoDir, + displayName: "Repository", + }), + kind: "non_git" as const, + customName: "My project", + }; + const sourceWorkspace = createPersistedWorkspaceRecordForTest({ + workspaceId: "ws-main-checkout", + projectId: sourceProject.projectId, + cwd: repoDir, + kind: "local_checkout", + displayName: "main", + }); + deps.projects.set(sourceProject.projectId, sourceProject); + deps.workspaces.set(sourceWorkspace.workspaceId, sourceWorkspace); + + const result = await createPaseoWorktree( + { + cwd: repoDir, + worktreeSlug: "project-became-git", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + deps, + ); + + expect(result.workspace.projectId).toBe(sourceProject.projectId); + expect(deps.projects.get(sourceProject.projectId)).toMatchObject({ + projectId: sourceProject.projectId, + rootPath: repoDir, + kind: "git", + displayName: "Repository", + customName: "My project", + archivedAt: null, + }); +}); + test("repairs a legacy source workspace whose project record is missing", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); @@ -727,7 +771,7 @@ test.skipIf(isPlatform("win32"))( ); interface TestDeps extends CreatePaseoWorktreeDeps { - projectRegistry: Pick; + projectRegistry: Pick; projects: Map; workspaces: Map; } @@ -760,6 +804,9 @@ function createDeps(options?: { projects.set(project.projectId, project); return project; }, + upsert: async (project) => { + projects.set(project.projectId, project); + }, }, workspaceRegistry: { get: async (workspaceId) => workspaces.get(workspaceId) ?? null, @@ -841,15 +888,30 @@ function createWorkspaceGitServiceStub(): WorkspaceGitService { unsubscribe: () => {}, }), peekSnapshot: (cwd) => createWorkspaceGitSnapshot(cwd), - getCheckout: async (cwd) => ({ - cwd, - isGit: false, - currentBranch: null, - remoteUrl: null, - worktreeRoot: null, - isPaseoOwnedWorktree: false, - mainRepoRoot: null, - }), + getCheckout: async (cwd) => { + try { + const snapshot = createWorkspaceGitSnapshot(cwd); + return { + cwd, + isGit: snapshot.git.isGit, + currentBranch: snapshot.git.currentBranch, + remoteUrl: snapshot.git.remoteUrl, + worktreeRoot: snapshot.git.repoRoot, + isPaseoOwnedWorktree: snapshot.git.isPaseoOwnedWorktree, + mainRepoRoot: snapshot.git.mainRepoRoot, + }; + } catch { + return { + cwd, + isGit: false, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false, + mainRepoRoot: null, + }; + } + }, getSnapshot: async (cwd) => createWorkspaceGitSnapshot(cwd), resolveRepoRoot: async (cwd) => { try { diff --git a/packages/server/src/server/paseo-worktree-service.ts b/packages/server/src/server/paseo-worktree-service.ts index fd9488718..fa93cdde4 100644 --- a/packages/server/src/server/paseo-worktree-service.ts +++ b/packages/server/src/server/paseo-worktree-service.ts @@ -1,6 +1,7 @@ import type { WorkspaceGitService } from "./workspace-git-service.js"; import { resolve } from "node:path"; import { + type PersistedProjectRecord, type PersistedWorkspaceRecord, type ProjectRegistry, type WorkspaceRegistry, @@ -51,7 +52,7 @@ export interface AttemptFirstAgentBranchAutoNameResult { } export interface CreatePaseoWorktreeDeps extends CreateWorktreeCoreDeps { - projectRegistry: Pick; + projectRegistry: Pick; workspaceRegistry: Pick; workspaceGitService: WorkspaceGitService; } @@ -258,14 +259,17 @@ async function resolveSourceProjectIdForWorktree(options: { inputCwd: string; projectId?: string; repoRoot: string; - deps: Pick; + deps: Pick< + CreatePaseoWorktreeDeps, + "projectRegistry" | "workspaceRegistry" | "workspaceGitService" + >; }): Promise { if (options.projectId) { const project = await options.deps.projectRegistry.get(options.projectId); if (!project || project.archivedAt) { throw new Error(`Project not found for worktree: ${options.projectId}`); } - return project.projectId; + return (await refreshProjectKind(project, options.deps)).projectId; } const sourceWorkspace = await findWorkspaceForSource({ @@ -276,7 +280,7 @@ async function resolveSourceProjectIdForWorktree(options: { if (sourceWorkspace) { const sourceProject = await options.deps.projectRegistry.get(sourceWorkspace.projectId); - if (sourceProject) return sourceProject.projectId; + if (sourceProject) return (await refreshProjectKind(sourceProject, options.deps)).projectId; // COMPAT(worktreeMissingSourceProject): added in v0.1.107, remove after 2027-01-15. // Orphaned legacy workspace FKs fall through to exact-root allocation. } @@ -287,7 +291,20 @@ async function resolveSourceProjectIdForWorktree(options: { displayName: options.repoRoot.split(/[\\/]/).findLast(Boolean) ?? options.repoRoot, timestamp: new Date().toISOString(), }); - return project.projectId; + return (await refreshProjectKind(project, options.deps)).projectId; +} + +async function refreshProjectKind( + project: PersistedProjectRecord, + deps: Pick, +): Promise { + const checkout = await deps.workspaceGitService.getCheckout(project.rootPath); + const kind: PersistedProjectRecord["kind"] = checkout.isGit ? "git" : "non_git"; + if (project.kind === kind) return project; + + const refreshed = { ...project, kind, updatedAt: new Date().toISOString() }; + await deps.projectRegistry.upsert(refreshed); + return refreshed; } async function findWorkspaceForSource(options: {