fix(server): reconcile workspace kind alongside project kind (#975)
The reconciler updated project.kind when a directory became a git repo but never updated the workspace.kind underneath, leaving the sidebar in a split-brain state: an expandable git project containing a workspace with no leading icon (workspaceKind stayed "directory" instead of flipping to "local_checkout" or "worktree").
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<Pick<PersistedWorkspaceRecord, "displayName">>;
|
||||
fields: Partial<Pick<PersistedWorkspaceRecord, "displayName" | "kind">>;
|
||||
};
|
||||
|
||||
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<Pick<PersistedWorkspaceRecord, "displayName" | "kind">> =
|
||||
{};
|
||||
|
||||
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,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user