import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fsPromises from "fs/promises";
import * as os from "os";
import * as path from "path";
import * as jsonc from "jsonc-parser";
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import { Config } from "@/node/config";
import {
MAX_CODE_WORKSPACE_FILE_BYTES,
MAX_CODE_WORKSPACE_FOLDERS,
computeManagedWorktreePaths,
managedRootsByProject,
syncProjectCodeWorkspace,
updateCodeWorkspaceFile,
} from "./codeWorkspaceSync";
let tempDir: string;
let managedRootDir: string;
let workspaceFilePath: string;
beforeEach(async () => {
tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "code-workspace-sync-"));
managedRootDir = path.join(tempDir, "src", "my-project");
workspaceFilePath = path.join(tempDir, "my-project.code-workspace");
});
afterEach(async () => {
await fsPromises.rm(tempDir, { recursive: true, force: true });
});
async function readWorkspaceFile(): Promise {
return fsPromises.readFile(workspaceFilePath, "utf-8");
}
function parseFolders(text: string): Array {
const parsed = jsonc.parse(text) as { folders: Array };
return parsed.folders;
}
describe("updateCodeWorkspaceFile", () => {
test("creates a missing file with seed folders", async () => {
const worktree = path.join(managedRootDir, "feature-a");
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [path.join(tempDir, "project"), worktree],
});
const folders = parseFolders(await readWorkspaceFile());
expect(folders).toEqual([{ path: path.join(tempDir, "project") }, { path: worktree }]);
});
test("adds missing worktree entries to an existing file", async () => {
const existing = path.join(managedRootDir, "feature-a");
const added = path.join(managedRootDir, "feature-b");
await fsPromises.writeFile(
workspaceFilePath,
JSON.stringify({ folders: [{ path: existing }] }, null, "\t")
);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [existing, added],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([{ path: existing }, { path: added }]);
});
test("removes managed entries that are no longer desired", async () => {
const kept = path.join(managedRootDir, "feature-a");
const removedA = path.join(managedRootDir, "feature-b");
const removedB = path.join(managedRootDir, "feature-c");
await fsPromises.writeFile(
workspaceFilePath,
JSON.stringify({ folders: [{ path: removedA }, { path: kept }, { path: removedB }] })
);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [kept],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([{ path: kept }]);
});
test("never removes user-owned entries outside the managed root", async () => {
const projectRoot = path.join(tempDir, "project");
const userFolder = "/home/user/some-other-folder";
const relativeUserFolder = "./relative-folder";
await fsPromises.writeFile(
workspaceFilePath,
JSON.stringify({
folders: [{ path: projectRoot }, { path: userFolder }, { path: relativeUserFolder }],
})
);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([
{ path: projectRoot },
{ path: userFolder },
{ path: relativeUserFolder },
]);
});
test("preserves comments, settings, and folder names through edits", async () => {
const kept = path.join(managedRootDir, "feature-a");
const removed = path.join(managedRootDir, "feature-b");
const added = path.join(managedRootDir, "feature-c");
const content = [
"{",
"\t// user comment survives",
'\t"folders": [',
`\t\t{ "path": ${JSON.stringify(kept)}, "name": "Kept" },`,
`\t\t{ "path": ${JSON.stringify(removed)} }`,
"\t],",
'\t"settings": { "editor.tabSize": 2 }',
"}",
].join("\n");
await fsPromises.writeFile(workspaceFilePath, content);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [kept, added],
seedFolders: [],
});
const text = await readWorkspaceFile();
expect(text).toContain("// user comment survives");
const parsed = jsonc.parse(text) as {
folders: Array;
settings: Record;
};
expect(parsed.settings).toEqual({ "editor.tabSize": 2 });
expect(parsed.folders).toEqual([{ path: kept, name: "Kept" }, { path: added }]);
});
test("adds a folders array to a file that lacks one", async () => {
const worktree = path.join(managedRootDir, "feature-a");
await fsPromises.writeFile(workspaceFilePath, '{\n\t"settings": {}\n}\n');
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([{ path: worktree }]);
});
test("rejects non-regular targets like device files behind a symlink", async () => {
// A checkout-supplied symlink can point at e.g. /dev/zero, where stat
// reports size 0 but reads never reach EOF.
const linkPath = path.join(tempDir, "device.code-workspace");
await fsPromises.symlink("/dev/null", linkPath);
const result = await updateCodeWorkspaceFile({
codeWorkspacePath: linkPath,
managedRootDirs: [managedRootDir],
desiredPaths: [path.join(managedRootDir, "feature-a")],
seedFolders: [],
});
expect(result.ok).toBe(false);
expect((await fsPromises.lstat(linkPath)).isSymbolicLink()).toBe(true);
});
test("skips files exceeding the size cap without rewriting them", async () => {
// jsonc.parse is synchronous, so oversized (potentially repo-controlled)
// files must be rejected before parsing.
const oversized = `{"folders": [], "pad": "${"x".repeat(MAX_CODE_WORKSPACE_FILE_BYTES + 1)}"}`;
await fsPromises.writeFile(workspaceFilePath, oversized);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [path.join(managedRootDir, "feature-a")],
seedFolders: [],
});
expect(await readWorkspaceFile()).toBe(oversized);
});
test("leaves a malformed file untouched without throwing", async () => {
const malformed = '{ "folders": [ { "path": broken ';
await fsPromises.writeFile(workspaceFilePath, malformed);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [path.join(managedRootDir, "feature-a")],
seedFolders: [],
});
expect(await readWorkspaceFile()).toBe(malformed);
});
test("is idempotent: a second sync does not rewrite the file", async () => {
const worktree = path.join(managedRootDir, "feature-a");
const update = {
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [worktree],
};
await updateCodeWorkspaceFile(update);
const afterFirst = await fsPromises.stat(workspaceFilePath);
await updateCodeWorkspaceFile(update);
const afterSecond = await fsPromises.stat(workspaceFilePath);
expect(afterSecond.mtimeMs).toBe(afterFirst.mtimeMs);
expect(afterSecond.ino).toBe(afterFirst.ino);
});
test("two projects sharing one file manage disjoint entries", async () => {
const otherManagedRoot = path.join(tempDir, "src", "other-project");
const mine = path.join(managedRootDir, "feature-a");
const theirs = path.join(otherManagedRoot, "feature-x");
await fsPromises.writeFile(
workspaceFilePath,
JSON.stringify({ folders: [{ path: mine }, { path: theirs }] })
);
// Sync for "my-project" with an empty desired set: must not touch the
// other project's entry.
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([{ path: theirs }]);
});
test("refuses reconciles whose final folder count exceeds the sync cap", async () => {
const worktree = path.join(managedRootDir, "feature-a");
const filePath = path.join(tempDir, "huge.code-workspace");
// Exactly at the cap: the single addition would push the FINAL count over.
const content = JSON.stringify({
folders: Array.from({ length: MAX_CODE_WORKSPACE_FOLDERS }, () => ({})),
});
await fsPromises.writeFile(filePath, content);
const result = await updateCodeWorkspaceFile({
codeWorkspacePath: filePath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [worktree],
});
expect(result.ok).toBe(false);
expect(await fsPromises.readFile(filePath, "utf-8")).toBe(content);
});
test("refuses to create a file with more seed folders than the sync cap", async () => {
const filePath = path.join(tempDir, "seeded.code-workspace");
const seedFolders = Array.from({ length: MAX_CODE_WORKSPACE_FOLDERS + 1 }, (_, i) =>
path.join(managedRootDir, `feature-${i}`)
);
const result = await updateCodeWorkspaceFile({
codeWorkspacePath: filePath,
managedRootDirs: [managedRootDir],
desiredPaths: seedFolders,
seedFolders,
});
expect(result.ok).toBe(false);
const exists = await fsPromises.stat(filePath).then(
() => true,
() => false
);
expect(exists).toBe(false);
});
test("rejects files with duplicate top-level folders properties", async () => {
const worktree = path.join(managedRootDir, "feature-a");
const filePath = path.join(tempDir, "dupe.code-workspace");
// jsonc.parse reads the last property but jsonc.modify edits the first, so
// a reconcile would silently no-op while reporting success.
const content = `{"folders": [], "folders": [{"path": "/user/data"}]}`;
await fsPromises.writeFile(filePath, content);
const result = await updateCodeWorkspaceFile({
codeWorkspacePath: filePath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [worktree],
});
expect(result.ok).toBe(false);
expect(await fsPromises.readFile(filePath, "utf-8")).toBe(content);
});
test("refuses to write through a symlink whose target is not a .code-workspace file", async () => {
const worktree = path.join(managedRootDir, "feature-a");
const externalJson = path.join(tempDir, "external.json");
const externalContent = JSON.stringify({ folders: [{ path: "/user/data" }] });
await fsPromises.writeFile(externalJson, externalContent);
const linkPath = path.join(tempDir, "proj.code-workspace");
await fsPromises.symlink(externalJson, linkPath);
const result = await updateCodeWorkspaceFile({
codeWorkspacePath: linkPath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [worktree],
});
expect(result.ok).toBe(false);
expect(await fsPromises.readFile(externalJson, "utf-8")).toBe(externalContent);
// The dangling variant must not create the non-extension target either.
const danglingLink = path.join(tempDir, "dangling-bad.code-workspace");
await fsPromises.symlink(path.join(tempDir, "planted.json"), danglingLink);
const danglingResult = await updateCodeWorkspaceFile({
codeWorkspacePath: danglingLink,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [worktree],
});
expect(danglingResult.ok).toBe(false);
const plantedExists = await fsPromises.stat(path.join(tempDir, "planted.json")).then(
() => true,
() => false
);
expect(plantedExists).toBe(false);
});
test("creates a dangling symlink's target instead of replacing the link", async () => {
const worktree = path.join(managedRootDir, "feature-a");
const target = path.join(tempDir, "real-target.code-workspace");
const linkPath = path.join(tempDir, "dangling.code-workspace");
await fsPromises.symlink(target, linkPath);
await updateCodeWorkspaceFile({
codeWorkspacePath: linkPath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [worktree],
});
expect((await fsPromises.lstat(linkPath)).isSymbolicLink()).toBe(true);
expect(parseFolders(await fsPromises.readFile(target, "utf-8"))).toEqual([{ path: worktree }]);
});
test("writes through a symlinked workspace file without replacing the link", async () => {
const worktree = path.join(managedRootDir, "feature-a");
const realFile = path.join(tempDir, "shared-config", "real.code-workspace");
await fsPromises.mkdir(path.dirname(realFile), { recursive: true });
await fsPromises.writeFile(realFile, JSON.stringify({ folders: [] }));
const linkPath = path.join(tempDir, "linked.code-workspace");
await fsPromises.symlink(realFile, linkPath);
await updateCodeWorkspaceFile({
codeWorkspacePath: linkPath,
managedRootDirs: [managedRootDir],
desiredPaths: [worktree],
seedFolders: [],
});
expect((await fsPromises.lstat(linkPath)).isSymbolicLink()).toBe(true);
expect(parseFolders(await fsPromises.readFile(realFile, "utf-8"))).toEqual([
{ path: worktree },
]);
});
test("serializes concurrent updates to the same file", async () => {
await fsPromises.writeFile(workspaceFilePath, JSON.stringify({ folders: [] }));
const otherRoot = path.join(tempDir, "src", "other-project");
const mine = path.join(managedRootDir, "feature-a");
const theirs = path.join(otherRoot, "feature-x");
// Two projects sharing one file sync concurrently; without per-file
// serialization one write clobbers the other's addition.
await Promise.all([
updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [mine],
seedFolders: [],
}),
updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [otherRoot],
desiredPaths: [theirs],
seedFolders: [],
}),
]);
const folders = parseFolders(await readWorkspaceFile());
expect(folders.map((entry) => entry.path).sort()).toEqual([mine, theirs].sort());
});
test("resolves relative folder entries against the file's directory", async () => {
// Entry is relative but points inside the managed root; it must count as
// present (no duplicate added) and be removable when undesired.
const relative = "./src/my-project/feature-a";
await fsPromises.writeFile(
workspaceFilePath,
JSON.stringify({ folders: [{ path: relative }] })
);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [path.join(managedRootDir, "feature-a")],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([{ path: relative }]);
await updateCodeWorkspaceFile({
codeWorkspacePath: workspaceFilePath,
managedRootDirs: [managedRootDir],
desiredPaths: [],
seedFolders: [],
});
expect(parseFolders(await readWorkspaceFile())).toEqual([]);
});
});
describe("syncProjectCodeWorkspace", () => {
test("creates the file from project config, resolving relative setting paths", async () => {
const config = new Config(tempDir);
const projectPath = path.join(tempDir, "repo");
await fsPromises.mkdir(projectPath, { recursive: true });
const worktreePath = path.join(config.srcDir, "repo", "feat-1");
// Checkout must exist on disk or metadata is marked transcript-only.
await fsPromises.mkdir(worktreePath, { recursive: true });
await config.editConfig((cfg) => {
cfg.projects.set(projectPath, {
workspaces: [
{
path: worktreePath,
id: "aaaaaaaaaa",
name: "feat-1",
runtimeConfig: { type: "worktree", srcBaseDir: config.srcDir },
},
],
codeWorkspaceSyncPath: "repo.code-workspace",
});
return cfg;
});
await syncProjectCodeWorkspace(config, projectPath);
const text = await fsPromises.readFile(path.join(projectPath, "repo.code-workspace"), "utf-8");
expect(parseFolders(text)).toEqual([{ path: projectPath }, { path: worktreePath }]);
});
test("does nothing when the setting is unset", async () => {
const config = new Config(tempDir);
const projectPath = path.join(tempDir, "repo");
await fsPromises.mkdir(projectPath, { recursive: true });
await config.editConfig((cfg) => {
cfg.projects.set(projectPath, { workspaces: [] });
return cfg;
});
await syncProjectCodeWorkspace(config, projectPath);
expect((await fsPromises.readdir(projectPath)).length).toBe(0);
});
test("groups symlink aliases of the same file into one reconcile", async () => {
// One project configures the symlink path, another the real path; lexical
// comparison would treat them as different files and let their overlapping
// managed root erase each other's entries.
const config = new Config(tempDir);
const projectA = path.join(tempDir, "a", "repo");
const projectB = path.join(tempDir, "b", "repo");
await fsPromises.mkdir(projectA, { recursive: true });
await fsPromises.mkdir(projectB, { recursive: true });
const realFile = path.join(tempDir, "real.code-workspace");
await fsPromises.writeFile(realFile, JSON.stringify({ folders: [] }));
const linkFile = path.join(tempDir, "alias.code-workspace");
await fsPromises.symlink(realFile, linkFile);
const worktreeA = path.join(config.srcDir, "repo", "feat-a");
const worktreeB = path.join(config.srcDir, "repo", "feat-b");
await fsPromises.mkdir(worktreeA, { recursive: true });
await fsPromises.mkdir(worktreeB, { recursive: true });
const workspaceEntry = (worktree: string, id: string) => ({
path: worktree,
id,
name: path.basename(worktree),
runtimeConfig: { type: "worktree" as const, srcBaseDir: config.srcDir },
});
await config.editConfig((cfg) => {
cfg.projects.set(projectA, {
workspaces: [workspaceEntry(worktreeA, "aaaaaaaaaa")],
codeWorkspaceSyncPath: linkFile,
});
cfg.projects.set(projectB, {
workspaces: [workspaceEntry(worktreeB, "bbbbbbbbbb")],
codeWorkspaceSyncPath: realFile,
});
return cfg;
});
await syncProjectCodeWorkspace(config, projectA);
await syncProjectCodeWorkspace(config, projectB);
const folderPaths = parseFolders(await fsPromises.readFile(realFile, "utf-8")).map(
(entry) => entry.path
);
expect(folderPaths).toContain(worktreeA);
expect(folderPaths).toContain(worktreeB);
});
test("unions projects that target the same file so they cannot erase each other", async () => {
// Same-basename projects share one managed root (/repo); a sync
// scoped to only one project would remove the other's entries.
const config = new Config(tempDir);
const projectA = path.join(tempDir, "a", "repo");
const projectB = path.join(tempDir, "b", "repo");
await fsPromises.mkdir(projectA, { recursive: true });
await fsPromises.mkdir(projectB, { recursive: true });
const sharedFile = path.join(tempDir, "shared.code-workspace");
const worktreeA = path.join(config.srcDir, "repo", "feat-a");
const worktreeB = path.join(config.srcDir, "repo", "feat-b");
await fsPromises.mkdir(worktreeA, { recursive: true });
await fsPromises.mkdir(worktreeB, { recursive: true });
const workspaceEntry = (worktree: string, id: string) => ({
path: worktree,
id,
name: path.basename(worktree),
runtimeConfig: { type: "worktree" as const, srcBaseDir: config.srcDir },
});
await config.editConfig((cfg) => {
cfg.projects.set(projectA, {
workspaces: [workspaceEntry(worktreeA, "aaaaaaaaaa")],
codeWorkspaceSyncPath: sharedFile,
});
cfg.projects.set(projectB, {
workspaces: [workspaceEntry(worktreeB, "bbbbbbbbbb")],
codeWorkspaceSyncPath: sharedFile,
});
return cfg;
});
await syncProjectCodeWorkspace(config, projectA);
await syncProjectCodeWorkspace(config, projectB);
const folders = parseFolders(await fsPromises.readFile(sharedFile, "utf-8"));
const folderPaths = folders.map((entry) => entry.path);
expect(folderPaths).toContain(worktreeA);
expect(folderPaths).toContain(worktreeB);
});
test("removes stale entries under extra managed roots after their workspace is gone", async () => {
// Deleting the last workspace under a custom/legacy srcBaseDir removes the
// metadata that reconstructed its root; callers pass the captured root so
// the deleted checkout's entry still gets cleaned up.
const config = new Config(tempDir);
const projectPath = path.join(tempDir, "repo");
await fsPromises.mkdir(projectPath, { recursive: true });
const legacyRoot = path.join(tempDir, "legacy-src", "repo");
const staleEntry = path.join(legacyRoot, "deleted-feature");
const file = path.join(projectPath, "repo.code-workspace");
await fsPromises.writeFile(
file,
JSON.stringify({ folders: [{ path: projectPath }, { path: staleEntry }] })
);
await config.editConfig((cfg) => {
cfg.projects.set(projectPath, {
workspaces: [],
codeWorkspaceSyncPath: "repo.code-workspace",
});
return cfg;
});
await syncProjectCodeWorkspace(config, projectPath, { extraManagedRootDirs: [legacyRoot] });
expect(parseFolders(await fsPromises.readFile(file, "utf-8"))).toEqual([{ path: projectPath }]);
});
test("refuses paths without the .code-workspace extension", async () => {
const config = new Config(tempDir);
const projectPath = path.join(tempDir, "repo");
await fsPromises.mkdir(projectPath, { recursive: true });
await config.editConfig((cfg) => {
cfg.projects.set(projectPath, { workspaces: [], codeWorkspaceSyncPath: "notes.json" });
return cfg;
});
await syncProjectCodeWorkspace(config, projectPath);
expect((await fsPromises.readdir(projectPath)).length).toBe(0);
});
});
describe("computeManagedWorktreePaths", () => {
const projectPath = "/home/user/projects/my-project";
function makeMetadata(overrides: Partial): FrontendWorkspaceMetadata {
const base: FrontendWorkspaceMetadata = {
id: "abc123def0",
name: "feature-a",
projectName: "my-project",
projectPath,
namedWorkspacePath: path.join("/base/src/my-project", "feature-a"),
runtimeConfig: { type: "worktree", srcBaseDir: "/base/src" },
};
return { ...base, ...overrides };
}
const managedRoot = "/base/src/my-project";
const computeParams = {
projectPath,
defaultManagedRootDir: managedRoot,
};
test("includes active worktree workspaces and sorts deduped paths", () => {
const { desiredPaths } = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({ name: "b", namedWorkspacePath: `${managedRoot}/b` }),
makeMetadata({ name: "a", namedWorkspacePath: `${managedRoot}/a` }),
makeMetadata({ name: "a-dup", namedWorkspacePath: `${managedRoot}/a` }),
],
...computeParams,
});
expect(desiredPaths).toEqual([`${managedRoot}/a`, `${managedRoot}/b`]);
});
test("excludes archived, sub-agent, isolation-none, other-project, and out-of-root workspaces", () => {
const { desiredPaths } = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({ archivedAt: "2026-01-02T00:00:00Z" }),
makeMetadata({ parentWorkspaceId: "parent1234" }),
makeMetadata({ taskIsolation: "none" }),
makeMetadata({ projectPath: "/home/user/projects/other" }),
makeMetadata({ namedWorkspacePath: "/elsewhere/feature-a" }),
makeMetadata({ runtimeConfig: { type: "local" } }),
],
...computeParams,
});
expect(desiredPaths).toEqual([]);
});
test("re-includes unarchived workspaces", () => {
const { desiredPaths } = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({
archivedAt: "2026-01-01T00:00:00Z",
unarchivedAt: "2026-01-02T00:00:00Z",
}),
],
...computeParams,
});
expect(desiredPaths).toEqual([`${managedRoot}/feature-a`]);
});
test("includes devcontainer workspaces (host worktrees under the default root)", () => {
const { desiredPaths } = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({
runtimeConfig: { type: "devcontainer", configPath: ".devcontainer/devcontainer.json" },
}),
],
...computeParams,
});
expect(desiredPaths).toEqual([`${managedRoot}/feature-a`]);
});
test("excludes transcript-only workspaces whose checkout was deleted", () => {
const { desiredPaths } = computeManagedWorktreePaths({
allMetadata: [makeMetadata({ transcriptOnly: true })],
...computeParams,
});
expect(desiredPaths).toEqual([]);
});
test("managedRootsByProject derives devcontainer cleanup roots from the checkout", () => {
const subProjectPath = `${projectPath}/packages/api`;
const metadata = makeMetadata({
subProjectPath,
runtimeConfig: { type: "devcontainer", configPath: ".devcontainer/devcontainer.json" },
});
const roots = managedRootsByProject(metadata);
// Cleanup after removal/reassignment must retain the parent checkout root,
// matching what computeManagedWorktreePaths derives while the metadata exists.
expect(roots.get(subProjectPath)).toEqual([managedRoot]);
expect(roots.get(projectPath)).toEqual([managedRoot]);
});
test("keeps a devcontainer workspace assigned to a sub-project under the parent root", () => {
const subProjectPath = `${projectPath}/packages/api`;
// Devcontainer host worktrees live under the PARENT project's directory;
// the sub-project's default root (/base/src/api) does not contain them.
const { desiredPaths, managedRootDirs } = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({
subProjectPath,
runtimeConfig: { type: "devcontainer", configPath: ".devcontainer/devcontainer.json" },
}),
],
projectPath: subProjectPath,
defaultManagedRootDir: "/base/src/api",
});
expect(desiredPaths).toEqual([`${managedRoot}/feature-a`]);
expect(managedRootDirs).toContain(managedRoot);
});
test("includes workspaces assigned to a registered sub-project", () => {
const subProjectPath = `${projectPath}/packages/api`;
// The workspace lives in the parent's bucket and shares the parent repo's
// worktree directory; the sub-project's own file must still list it.
const { desiredPaths, managedRootDirs } = computeManagedWorktreePaths({
allMetadata: [makeMetadata({ subProjectPath })],
projectPath: subProjectPath,
defaultManagedRootDir: "/base/src/api",
});
expect(desiredPaths).toEqual([`${managedRoot}/feature-a`]);
expect(managedRootDirs).toContain(managedRoot);
});
test("keeps worktrees under a custom or legacy srcBaseDir managed", () => {
// Legacy "local"-with-srcBaseDir runtime rooted somewhere other than the
// current global srcDir (e.g. a pre-rename ~/.mux/src) must still sync.
const { desiredPaths, managedRootDirs } = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({
namedWorkspacePath: "/legacy/src/my-project/feature-a",
runtimeConfig: { type: "local", srcBaseDir: "/legacy/src" },
}),
],
...computeParams,
});
expect(desiredPaths).toEqual(["/legacy/src/my-project/feature-a"]);
expect(managedRootDirs).toEqual(["/base/src/my-project", "/legacy/src/my-project"]);
});
test("derives per-project checkout paths for multi-project workspaces", () => {
const multiProjects = [
{ projectPath: "/home/user/projects/primary", projectName: "primary" },
{ projectPath, projectName: "my-project" },
];
// namedWorkspacePath for multi-project workspaces is the _workspaces/
// symlink container, never a real checkout, for primary and secondary alike.
const asSecondary = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({
projectPath: "/home/user/projects/primary",
namedWorkspacePath: "/base/src/_workspaces/feature-a",
projects: multiProjects,
}),
],
...computeParams,
});
expect(asSecondary.desiredPaths).toEqual([`${managedRoot}/feature-a`]);
const asPrimary = computeManagedWorktreePaths({
allMetadata: [
makeMetadata({
namedWorkspacePath: "/base/src/_workspaces/feature-a",
projects: multiProjects,
}),
],
...computeParams,
});
expect(asPrimary.desiredPaths).toEqual([`${managedRoot}/feature-a`]);
});
});