fix(server): keep Git observation and worktree IO off the event loop (#7943)

* fix(server): keep Git observation and worktree IO off the event loop

Resolve subscription path identities before callbacks and serialize async worktree metadata mutations by canonical ownership. Git command tracing must not probe the working directory before spawning.

* test(server): verify Git trace records without filesystem probes

* fix(server): establish Git identities at observation setup

Read-only checkout refreshes should not resolve watcher namespaces. Observation tests await setup and compare cross-workspace fan-out without depending on completion order.

* fix(server): retain literal symlink targets in Git event identity

Canonical realpaths can erase a symlink target spelling when its parent is also a symlink, such as /var on macOS. Resolve these aliases once during setup so deleted and ignored event paths still match without per-event IO.

* fix(server): preserve Windows worktree path spelling

* fix(server): retain native aliases in worktree ownership

* test(server): write Git IO trace markers asynchronously
This commit is contained in:
Mohamed Boudra
2026-10-27 09:57:35 +07:00
committed by GitHub
parent 67a2459254
commit 9bc998fa38
23 changed files with 906 additions and 311 deletions
+121 -10
View File
@@ -21,7 +21,13 @@ import {
type WorktreeConfig,
} from "./worktree";
import type { PaseoConfig } from "@getpaseo/protocol/paseo-config-schema";
import { getPaseoWorktreeMetadataPath, readPaseoWorktreeMetadata } from "./worktree-metadata.js";
import {
getPaseoWorktreeMetadataPath,
readPaseoWorktreeMetadata,
writePaseoWorktreeMetadata,
writePaseoWorktreeRuntimeMetadata,
writePaseoWorktreeFirstAgentBranchAutoNameMetadata,
} from "./worktree-metadata.js";
import {
getCheckoutDiff,
getCheckoutStatus,
@@ -125,7 +131,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
expect(result.worktreePath).toBe(join(paseoHome, "worktrees", projectHash, "hello-world"));
expect(existsSync(result.worktreePath)).toBe(true);
expect(existsSync(join(result.worktreePath, "file.txt"))).toBe(true);
const metadataPath = getPaseoWorktreeMetadataPath(result.worktreePath);
const metadataPath = await getPaseoWorktreeMetadataPath(result.worktreePath);
expect(existsSync(metadataPath)).toBe(true);
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
expect(metadata).toMatchObject({
@@ -256,7 +262,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
cwd: result.worktreePath,
});
const metadataPath = getPaseoWorktreeMetadataPath(result.worktreePath);
const metadataPath = await getPaseoWorktreeMetadataPath(result.worktreePath);
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
expect(metadata).toMatchObject({
version: 1,
@@ -319,7 +325,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
// baseRefName must be the repo's default branch, not the checked-out branch
// itself — otherwise the diff panel compares the branch against itself and
// always shows no changes.
const metadataPath = getPaseoWorktreeMetadataPath(result.worktreePath);
const metadataPath = await getPaseoWorktreeMetadataPath(result.worktreePath);
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
expect(metadata).toMatchObject({
version: 1,
@@ -370,7 +376,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
});
expect(existsSync(result.worktreePath)).toBe(true);
const metadataPath = getPaseoWorktreeMetadataPath(result.worktreePath);
const metadataPath = await getPaseoWorktreeMetadataPath(result.worktreePath);
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
expect(metadata).toMatchObject({ version: 1, baseRefName: "develop" });
});
@@ -406,7 +412,9 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
expect(result.branchName).toBe("main-1");
expect(existsSync(result.worktreePath)).toBe(true);
expect(readPaseoWorktreeMetadata(result.worktreePath)?.changeRequestLookupTarget).toEqual({
expect(
(await readPaseoWorktreeMetadata(result.worktreePath))?.changeRequestLookupTarget,
).toEqual({
headRef: "main-1",
localBranchName: "main-1",
});
@@ -459,7 +467,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
.trim();
expect(currentBranch).toBe("user/feature");
const metadataPath = getPaseoWorktreeMetadataPath(result.worktreePath);
const metadataPath = await getPaseoWorktreeMetadataPath(result.worktreePath);
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
expect(metadata).toMatchObject({ baseRefName: "main" });
});
@@ -566,7 +574,9 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
"from-origin\n",
);
expect(
JSON.parse(readFileSync(getPaseoWorktreeMetadataPath(originResult.worktreePath), "utf8")),
JSON.parse(
readFileSync(await getPaseoWorktreeMetadataPath(originResult.worktreePath), "utf8"),
),
).toMatchObject({ baseRefName: "main" });
});
@@ -607,7 +617,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
// to the commit the worktree was cut from: "main" resolves local-first, which here is
// a different commit than the fork's upstream.
expect(
JSON.parse(readFileSync(getPaseoWorktreeMetadataPath(result.worktreePath), "utf8")),
JSON.parse(readFileSync(await getPaseoWorktreeMetadataPath(result.worktreePath), "utf8")),
).toMatchObject({ baseRefName: "main", baseRef: "refs/remotes/upstream/main" });
// An untouched child must report no work of its own. Comparing against the wrong base
@@ -694,7 +704,7 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
});
expect(
JSON.parse(readFileSync(getPaseoWorktreeMetadataPath(result.worktreePath), "utf8")),
JSON.parse(readFileSync(await getPaseoWorktreeMetadataPath(result.worktreePath), "utf8")),
).toMatchObject({
baseRefName: "release+hotfix",
baseRef: "refs/remotes/upstream/release+hotfix",
@@ -1617,3 +1627,104 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
});
});
});
describe.skipIf(isPlatform("win32"))("async worktree metadata", () => {
it("serializes metadata mutations per worktree while another worktree progresses", async () => {
const root = mkdtempSync(join(tmpdir(), "worktree-metadata-order-"));
const first = join(root, "first");
const second = join(root, "second");
mkdirSync(join(first, ".git"), { recursive: true });
mkdirSync(join(second, ".git"), { recursive: true });
try {
await writePaseoWorktreeMetadata(first, { baseRefName: "main" });
await writePaseoWorktreeMetadata(second, { baseRefName: "release" });
const order: string[] = [];
const runtime = writePaseoWorktreeRuntimeMetadata(first, { worktreePort: 4101 }).then(() =>
order.push("first-runtime"),
);
const unrelated = writePaseoWorktreeRuntimeMetadata(second, { worktreePort: 4102 }).then(() =>
order.push("second-runtime"),
);
const naming = writePaseoWorktreeFirstAgentBranchAutoNameMetadata(first, {
placeholderBranchName: "topic",
}).then(() => order.push("first-naming"));
// A read issued while writes are pending observes their completed snapshot.
const snapshot = readPaseoWorktreeMetadata(first);
await Promise.all([runtime, unrelated, naming]);
expect(order.indexOf("first-runtime")).toBeLessThan(order.indexOf("first-naming"));
expect(await snapshot).toMatchObject({
baseRefName: "main",
runtime: { worktreePort: 4101 },
firstAgentBranchAutoName: { status: "pending", placeholderBranchName: "topic" },
});
expect(await readPaseoWorktreeMetadata(second)).toMatchObject({
baseRefName: "release",
runtime: { worktreePort: 4102 },
});
const alias = join(root, "first-alias");
symlinkSync(first, alias, "dir");
await Promise.all([
writePaseoWorktreeRuntimeMetadata(first, { worktreePort: 4103 }),
writePaseoWorktreeFirstAgentBranchAutoNameMetadata(alias, {
placeholderBranchName: "renamed",
}),
]);
expect(await readPaseoWorktreeMetadata(first)).toMatchObject({
runtime: { worktreePort: 4103 },
firstAgentBranchAutoName: { placeholderBranchName: "renamed" },
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("a queued metadata mutation cannot recreate an archived Git directory", async () => {
const root = mkdtempSync(join(tmpdir(), "worktree-metadata-archive-"));
mkdirSync(join(root, ".git"));
try {
await writePaseoWorktreeMetadata(root, { baseRefName: "main" });
const pending = writePaseoWorktreeRuntimeMetadata(root, { worktreePort: 4101 });
// The mutation is queued in a microtask, so archive wins this ordering.
rmSync(join(root, ".git"), { recursive: true });
await expect(pending).rejects.toThrow("Not a git repository");
expect(existsSync(join(root, ".git"))).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("recognizes a removed worktree under a symlinked ownership root", async () => {
const root = mkdtempSync(join(tmpdir(), "worktree-ownership-alias-"));
const actual = join(root, "actual");
const alias = join(root, "worktrees");
mkdirSync(actual);
symlinkSync(actual, alias, "dir");
try {
const cwd = join(alias, "project", "removed");
expect(
await isPaseoOwnedWorktreeCwd(cwd, { worktreesRoot: alias, knownGitCommonDir: null }),
).toMatchObject({ allowed: true, worktreePath: cwd });
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("rejects oversized metadata and named pipes without reading them whole", async () => {
const root = mkdtempSync(join(tmpdir(), "worktree-metadata-admission-"));
mkdirSync(join(root, ".git", "paseo"), { recursive: true });
try {
const path = await getPaseoWorktreeMetadataPath(root);
writeFileSync(path, JSON.stringify({ version: 1, baseRefName: "x".repeat(64 * 1024) }));
await expect(readPaseoWorktreeMetadata(root)).rejects.toThrow(
"Invalid Paseo worktree metadata file",
);
rmSync(path);
execFileSync("mkfifo", [path]);
await expect(readPaseoWorktreeMetadata(root)).rejects.toThrow(
"Invalid Paseo worktree metadata file",
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});