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 {
|
function createTempGitRepo(prefix: string): string {
|
||||||
const raw = mkdtempSync(path.join(tmpdir(), prefix));
|
const raw = mkdtempSync(path.join(tmpdir(), prefix));
|
||||||
const dir = realpathSync(raw);
|
const dir = realpathSync(raw);
|
||||||
@@ -252,19 +261,7 @@ describe("WorkspaceReconciliationService", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize as git repo
|
initGitRepoInDir(resolved);
|
||||||
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" });
|
|
||||||
|
|
||||||
const service = new WorkspaceReconciliationService({
|
const service = new WorkspaceReconciliationService({
|
||||||
projectRegistry,
|
projectRegistry,
|
||||||
@@ -286,6 +283,59 @@ describe("WorkspaceReconciliationService", () => {
|
|||||||
expect(projects.get("p1")!.kind).toBe("git");
|
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 () => {
|
test("updates project display name when git remote changes", async () => {
|
||||||
const dir = createTempGitRepo("reconcile-remote-");
|
const dir = createTempGitRepo("reconcile-remote-");
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import type { WorkspaceGitService } from "./workspace-git-service.js";
|
|||||||
|
|
||||||
const DEFAULT_RECONCILE_INTERVAL_MS = 60_000;
|
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 =
|
export type ReconciliationChange =
|
||||||
| { kind: "workspace_archived"; workspaceId: string; directory: string; reason: string }
|
| { kind: "workspace_archived"; workspaceId: string; directory: string; reason: string }
|
||||||
| { kind: "project_archived"; projectId: string; directory: string; reason: string }
|
| { kind: "project_archived"; projectId: string; directory: string; reason: string }
|
||||||
@@ -23,7 +32,7 @@ export type ReconciliationChange =
|
|||||||
kind: "workspace_updated";
|
kind: "workspace_updated";
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
directory: string;
|
directory: string;
|
||||||
fields: Partial<Pick<PersistedWorkspaceRecord, "displayName">>;
|
fields: Partial<Pick<PersistedWorkspaceRecord, "displayName" | "kind">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ReconciliationResult {
|
export interface ReconciliationResult {
|
||||||
@@ -222,20 +231,35 @@ export class WorkspaceReconciliationService {
|
|||||||
const wsDirName = workspace.cwd.split(/[\\/]/).findLast(Boolean) ?? workspace.cwd;
|
const wsDirName = workspace.cwd.split(/[\\/]/).findLast(Boolean) ?? workspace.cwd;
|
||||||
const wsGit = await this.readWorkspaceGitMetadata(workspace.cwd, wsDirName);
|
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) {
|
if (wsGit.projectKind === "git" && workspace.displayName !== wsGit.workspaceDisplayName) {
|
||||||
const timestamp = new Date().toISOString();
|
workspaceUpdates.displayName = wsGit.workspaceDisplayName;
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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