fix(projects): refresh worktree source project kind

This commit is contained in:
Mohamed Boudra
2026-07-16 15:11:29 +00:00
parent 70ed5f25e7
commit 5beee13331
2 changed files with 94 additions and 15 deletions

View File

@@ -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, "get" | "getOrCreateActiveByRoot">;
projectRegistry: Pick<ProjectRegistry, "get" | "getOrCreateActiveByRoot" | "upsert">;
projects: Map<string, PersistedProjectRecord>;
workspaces: Map<string, PersistedWorkspaceRecord>;
}
@@ -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 {

View File

@@ -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, "get" | "getOrCreateActiveByRoot">;
projectRegistry: Pick<ProjectRegistry, "get" | "getOrCreateActiveByRoot" | "upsert">;
workspaceRegistry: Pick<WorkspaceRegistry, "get" | "list" | "upsert">;
workspaceGitService: WorkspaceGitService;
}
@@ -258,14 +259,17 @@ async function resolveSourceProjectIdForWorktree(options: {
inputCwd: string;
projectId?: string;
repoRoot: string;
deps: Pick<CreatePaseoWorktreeDeps, "projectRegistry" | "workspaceRegistry">;
deps: Pick<
CreatePaseoWorktreeDeps,
"projectRegistry" | "workspaceRegistry" | "workspaceGitService"
>;
}): Promise<string> {
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<CreatePaseoWorktreeDeps, "projectRegistry" | "workspaceGitService">,
): Promise<PersistedProjectRecord> {
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: {