diff --git a/packages/server/src/server/workspace-reconciliation-service.test.ts b/packages/server/src/server/workspace-reconciliation-service.test.ts index 7c85cc3cf..f327e2771 100644 --- a/packages/server/src/server/workspace-reconciliation-service.test.ts +++ b/packages/server/src/server/workspace-reconciliation-service.test.ts @@ -114,6 +114,15 @@ function createWorkspaceGitServiceStub( }; } +function initGitRepoInDir(dir: string): void { + execFileSync("git", ["init", "-b", "main"], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["add", "."], { cwd: dir, stdio: "ignore" }); + execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "ignore" }); +} + function createTempGitRepo(prefix: string): string { const raw = mkdtempSync(path.join(tmpdir(), prefix)); const dir = realpathSync(raw); @@ -252,19 +261,7 @@ describe("WorkspaceReconciliationService", () => { }), ); - // Initialize as git repo - execFileSync("git", ["init", "-b", "main"], { cwd: resolved, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@test.com"], { - cwd: resolved, - stdio: "ignore", - }); - execFileSync("git", ["config", "user.name", "Test"], { cwd: resolved, stdio: "ignore" }); - execFileSync("git", ["config", "commit.gpgsign", "false"], { - cwd: resolved, - stdio: "ignore", - }); - execFileSync("git", ["add", "."], { cwd: resolved, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "init"], { cwd: resolved, stdio: "ignore" }); + initGitRepoInDir(resolved); const service = new WorkspaceReconciliationService({ projectRegistry, @@ -286,6 +283,59 @@ describe("WorkspaceReconciliationService", () => { expect(projects.get("p1")!.kind).toBe("git"); }); + test("updates workspace kind when a directory becomes a git repo", async () => { + const dir = mkdtempSync(path.join(tmpdir(), "reconcile-ws-kind-")); + const resolved = realpathSync(dir); + tempDirs.push(resolved); + writeFileSync(path.join(resolved, "README.md"), "# Test\n"); + + const { projects, workspaces, projectRegistry, workspaceRegistry } = createTestRegistries(); + + projects.set( + "p1", + createPersistedProjectRecord({ + projectId: "p1", + rootPath: resolved, + kind: "non_git", + displayName: path.basename(resolved), + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + workspaces.set( + "w1", + createPersistedWorkspaceRecord({ + workspaceId: "w1", + projectId: "p1", + cwd: resolved, + kind: "directory", + displayName: path.basename(resolved), + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + + initGitRepoInDir(resolved); + + const service = new WorkspaceReconciliationService({ + projectRegistry, + workspaceRegistry, + logger: createTestLogger(), + workspaceGitService: createWorkspaceGitServiceStub({ + [resolved]: { + projectKind: "git", + projectDisplayName: path.basename(resolved), + workspaceDisplayName: "main", + }, + }), + }); + + await service.runOnce(); + + expect(projects.get("p1")!.kind).toBe("git"); + expect(workspaces.get("w1")!.kind).toBe("local_checkout"); + }); + test("updates project display name when git remote changes", async () => { const dir = createTempGitRepo("reconcile-remote-"); tempDirs.push(dir); diff --git a/packages/server/src/server/workspace-reconciliation-service.ts b/packages/server/src/server/workspace-reconciliation-service.ts index cbb3676df..eb8f63a33 100644 --- a/packages/server/src/server/workspace-reconciliation-service.ts +++ b/packages/server/src/server/workspace-reconciliation-service.ts @@ -10,6 +10,15 @@ import type { WorkspaceGitService } from "./workspace-git-service.js"; const DEFAULT_RECONCILE_INTERVAL_MS = 60_000; +function deriveWorkspaceKindFromMetadata(metadata: { + projectKind: "git" | "directory"; + isWorktree: boolean; +}): PersistedWorkspaceRecord["kind"] { + if (metadata.projectKind !== "git") return "directory"; + if (metadata.isWorktree) return "worktree"; + return "local_checkout"; +} + export type ReconciliationChange = | { kind: "workspace_archived"; workspaceId: string; directory: string; reason: string } | { kind: "project_archived"; projectId: string; directory: string; reason: string } @@ -23,7 +32,7 @@ export type ReconciliationChange = kind: "workspace_updated"; workspaceId: string; directory: string; - fields: Partial>; + fields: Partial>; }; export interface ReconciliationResult { @@ -222,20 +231,35 @@ export class WorkspaceReconciliationService { const wsDirName = workspace.cwd.split(/[\\/]/).findLast(Boolean) ?? workspace.cwd; const wsGit = await this.readWorkspaceGitMetadata(workspace.cwd, wsDirName); + const expectedKind = deriveWorkspaceKindFromMetadata(wsGit); + + const workspaceUpdates: Partial> = + {}; + if (wsGit.projectKind === "git" && workspace.displayName !== wsGit.workspaceDisplayName) { - const timestamp = new Date().toISOString(); - await this.workspaceRegistry.upsert({ - ...workspace, - displayName: wsGit.workspaceDisplayName, - updatedAt: timestamp, - }); - changes.push({ - kind: "workspace_updated", - workspaceId: workspace.workspaceId, - directory: workspace.cwd, - fields: { displayName: wsGit.workspaceDisplayName }, - }); + workspaceUpdates.displayName = wsGit.workspaceDisplayName; } + + if (workspace.kind !== expectedKind) { + workspaceUpdates.kind = expectedKind; + } + + if (Object.keys(workspaceUpdates).length === 0) { + return; + } + + const timestamp = new Date().toISOString(); + await this.workspaceRegistry.upsert({ + ...workspace, + ...workspaceUpdates, + updatedAt: timestamp, + }); + changes.push({ + kind: "workspace_updated", + workspaceId: workspace.workspaceId, + directory: workspace.cwd, + fields: workspaceUpdates, + }); }), ); }