From ad21f9eb931c3dc5238a07edb2eb37179e0bca88 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Nov 2026 08:48:13 +0100 Subject: [PATCH] fix(diff): refresh equivalent worktree paths across platforms Explicit refreshes must match filesystem-equivalent roots, including Windows path aliases. Use the existing realpath-aware matcher, budget real Git completion waits consistently, and assert optional status metadata in the filesystem commit-file journey. --- .../server/daemon-e2e/filesystem.e2e.test.ts | 1 + packages/server/src/server/diff/index.ts | 5 +- .../src/server/diff/subscriptions.test.ts | 56 ++++++++++++++++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts b/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts index da52c4dac4..068e0628d0 100644 --- a/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts @@ -690,6 +690,7 @@ describe("commit file diffs", () => { expect(result).toEqual({ file: { path: filePath, + status: "ok", isNew: false, isDeleted: false, additions: 1, diff --git a/packages/server/src/server/diff/index.ts b/packages/server/src/server/diff/index.ts index 06e43a6670..9ddd586681 100644 --- a/packages/server/src/server/diff/index.ts +++ b/packages/server/src/server/diff/index.ts @@ -1,6 +1,6 @@ import { TOTAL_DIFF_MAX_BYTES } from "../../utils/checkout-git.js"; import { DEFAULT_MAX_OUTPUT_BYTES, runGitCommand } from "../../utils/run-git-command.js"; -import { expandTilde } from "../../utils/path.js"; +import { expandTilde, createRealpathAwarePathMatcher } from "../../utils/path.js"; import { packBodies } from "./batches.js"; import { setImmediate } from "node:timers/promises"; import { @@ -216,8 +216,9 @@ export class DiffSubscriptions { }; } invalidateForCwd(cwd: string): void { + const matches = createRealpathAwarePathMatcher(cwd); for (const target of this.targets.values()) - if (target.compare.cwd === cwd) this.invalidate(target); + if (matches(target.compare.cwd)) this.invalidate(target); } private invalidate(target: Target): void { if (target.closed) return; diff --git a/packages/server/src/server/diff/subscriptions.test.ts b/packages/server/src/server/diff/subscriptions.test.ts index 8df95de909..e738386eb5 100644 --- a/packages/server/src/server/diff/subscriptions.test.ts +++ b/packages/server/src/server/diff/subscriptions.test.ts @@ -67,7 +67,9 @@ test("shares computation, highlights only the changed file, recovers refs and re const refreshStarted = performance.now(); await writeFile(join(cwd, "9.ts"), "const value = 999;\n"); await expect - .poll(() => first.filter((frame) => frame.opcode === DiffOpcode.End).length) + .poll(() => first.filter((frame) => frame.opcode === DiffOpcode.End).length, { + timeout: 10000, + }) .toBeGreaterThan(0); const begin = first.find((frame) => frame.opcode === DiffOpcode.Begin); if (!begin || begin.opcode !== DiffOpcode.Begin) throw new Error("Missing begin"); @@ -133,7 +135,7 @@ test("shares computation, highlights only the changed file, recovers refs and re await observer.close(); await rm(cwd, { recursive: true, force: true }); } -}, 30_000); +}, 60_000); test("supersedes an in-flight revision at a batch boundary and ends the replacement", async () => { const cwd = await mkdtemp(join(tmpdir(), "diff-supersede-")); @@ -344,7 +346,9 @@ test("a cached replay completes before a live refresh can depend on it", async ( resume(); await replay; await expect - .poll(() => frames.filter((frame) => frame.opcode === DiffOpcode.End).length) + .poll(() => frames.filter((frame) => frame.opcode === DiffOpcode.End).length, { + timeout: 10000, + }) .toBe(2); let committed = 0; for (const frame of frames) { @@ -534,3 +538,49 @@ test.each([true, false])( } }, ); + +test("explicit refresh accepts an equivalent worktree path alias", async () => { + const { symlink } = await import("node:fs/promises"); + const root = await mkdtemp(join(tmpdir(), "diff-refresh-alias-")); + const cwd = join(root, "repo"); + const alias = join(root, "alias"); + const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" }).trim(); + const streams = new DiffSubscriptions({ + async requestWorkingTreeWatch(directory) { + return { repoRoot: directory, unsubscribe: async () => {} }; + }, + }); + const frames: DiffFrame[] = []; + try { + await mkdir(cwd); + await symlink(cwd, alias, "junction"); + git("init", "-b", "main"); + git("config", "user.name", "Test"); + git("config", "user.email", "test@example.com"); + await writeFile(join(cwd, "a.txt"), "base\n"); + git("add", "."); + git("-c", "commit.gpgsign=false", "commit", "-m", "base"); + const base = git("rev-parse", "HEAD"); + await writeFile(join(cwd, "a.txt"), "first\n"); + git("add", "."); + git("-c", "commit.gpgsign=false", "commit", "-m", "edit"); + const head = git("rev-parse", "HEAD"); + const subscription = await streams.subscribe( + { cwd, base: { kind: "rev", rev: base }, head: { kind: "rev", rev: head } }, + async (frame) => { + frames.push(frame); + }, + ); + await subscription.start(); + frames.length = 0; + streams.invalidateForCwd(alias); + await expect + .poll(() => frames.some((frame) => frame.opcode === DiffOpcode.End), { timeout: 10000 }) + .toBe(true); + expect(frames.at(-1)).toMatchObject({ opcode: DiffOpcode.End, revision: 2, error: null }); + await subscription.release(); + } finally { + await streams.close(); + await rm(root, { recursive: true, force: true }); + } +});