Fix History restore and diff truncation edge cases

This commit is contained in:
Mohamed Boudra
2026-06-18 20:44:57 +07:00
parent b3f81c0bf4
commit 00c57617d5
25 changed files with 717 additions and 127 deletions
@@ -561,6 +561,76 @@ test.skipIf(isPlatform("win32"))(
},
);
// The default archive path (scope "workspace", worktreePath only) resolves
// repoRoot=null, so deletePaseoWorktree's `git worktree remove`/`prune` is
// skipped: the directory is rm-ed but the admin registration survives, pinning
// the branch as "already checked out". Restore must self-heal by pruning the
// stale registration before recreating, regardless of how it was archived.
test.skipIf(isPlatform("win32"))(
"recreates a worktree whose dir was rm-ed without git worktree remove (stale registration)",
async () => {
const { repoDir, tempDir } = createGitRepo();
cleanupPaths.push(tempDir);
const paseoHome = path.join(tempDir, ".paseo");
execFileSync("git", ["branch", "restore-me"], { cwd: repoDir, stdio: "pipe" });
const created = await createWorktree({
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
runSetup: false,
paseoHome,
});
expect(existsSync(created.worktreePath)).toBe(true);
// Simulate the default-archive teardown: remove the working directory but
// leave the git worktree registration intact.
rmSync(created.worktreePath, { recursive: true, force: true });
expect(existsSync(created.worktreePath)).toBe(false);
const worktreeList = execFileSync("git", ["worktree", "list", "--porcelain"], {
cwd: repoDir,
stdio: "pipe",
}).toString();
expect(worktreeList).toContain(created.worktreePath);
// Recreating without pruning fails with the stale registration pinning the
// branch — this is the case restore must heal.
await expect(
createWorktree({
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
runSetup: false,
paseoHome,
}),
).rejects.toMatchObject({ name: "BranchAlreadyCheckedOutError" });
// The restore-side prune frees the stale registration; recreate then succeeds.
execFileSync("git", ["worktree", "prune"], { cwd: repoDir, stdio: "pipe" });
const recreated = await createWorktree({
cwd: repoDir,
worktreeSlug: "restore-me",
source: { kind: "checkout-branch", branchName: "restore-me" },
runSetup: false,
paseoHome,
});
expect(recreated.worktreePath).toBe(created.worktreePath);
expect(existsSync(recreated.worktreePath)).toBe(true);
expect(
execFileSync("git", ["branch", "--show-current"], {
cwd: recreated.worktreePath,
stdio: "pipe",
})
.toString()
.trim(),
).toBe("restore-me");
},
);
test.skipIf(isPlatform("win32"))(
"rejects with UnknownBranchError when the kept branch no longer exists",
async () => {
+14
View File
@@ -277,6 +277,7 @@ import {
toWorktreeWireError,
} from "./worktree-errors.js";
import { type WorktreeConfig, createWorktree } from "../utils/worktree.js";
import { runGitCommand } from "../utils/run-git-command.js";
import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
@@ -1689,6 +1690,7 @@ export class Session {
return {
projectKey: project.projectId,
projectName: resolveProjectDisplayName(project),
workspaceName: resolveWorkspaceDisplayName(workspace),
checkout,
};
}
@@ -7118,6 +7120,18 @@ export class Session {
});
}
// Archiving through the default path (scope "workspace", worktreePath only)
// resolves repoRoot=null, so deletePaseoWorktree's `git worktree remove`/
// `prune` is skipped and the admin registration survives — pinning the
// branch as "already checked out". Prune here frees any stale registration
// whose working dir is missing (a no-op for live worktrees) so the recreate
// below succeeds regardless of how the worktree was archived.
try {
await runGitCommand(["worktree", "prune"], { cwd: project.rootPath, timeout: 30_000 });
} catch {
// not critical; git will prune lazily
}
let result: WorktreeConfig;
try {
result = await createWorktree({
@@ -1156,6 +1156,40 @@ const x = 1;
expect(metrics.maxConcurrent).toBeLessThanOrEqual(8);
});
it("marks tracked files omitted by the total diff budget as too_large", async () => {
for (let i = 1; i <= 4; i += 1) {
writeFileSync(join(repoDir, `budget-${i}.txt`), "old\n");
}
execFileSync("git", ["add", "."], { cwd: repoDir });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add budget files"], {
cwd: repoDir,
});
const largeLine = "x".repeat(700_000);
for (let i = 1; i <= 4; i += 1) {
writeFileSync(join(repoDir, `budget-${i}.txt`), `${largeLine}-${i}\n`);
}
const diff = await getCheckoutDiff(repoDir, { mode: "uncommitted", includeStructured: true });
expect(
diff.structured?.map((file) => ({
path: file.path,
status: file.status,
hunks: file.hunks.length,
})),
).toEqual([
{ path: "budget-1.txt", status: "ok", hunks: 1 },
{ path: "budget-2.txt", status: "ok", hunks: 1 },
{ path: "budget-3.txt", status: "too_large", hunks: 0 },
{ path: "budget-4.txt", status: "too_large", hunks: 0 },
]);
expect(diff.diff).toContain("budget-1.txt");
expect(diff.diff).toContain("budget-2.txt");
expect(diff.diff).toContain("# budget-3.txt: diff too large omitted");
expect(diff.diff).toContain("# budget-4.txt: diff too large omitted");
});
it("short-circuits tracked binary files", async () => {
const trackedBinaryPath = join(repoDir, "tracked-blob.bin");
writeFileSync(trackedBinaryPath, Buffer.from([0x00, 0xff, 0x10, 0x80, 0x00]));
@@ -2194,6 +2194,10 @@ async function processTrackedChanges(
}
const diffBytes = Buffer.byteLength(fileDiff.text, "utf8");
if (trackedDiffBytes + diffBytes > TOTAL_DIFF_MAX_BYTES) {
trackedPlaceholderByPath.set(fileDiff.path, {
status: "too_large",
stat: trackedNumstatByPath.get(fileDiff.path) ?? null,
});
continue;
}
trackedDiffBytes += diffBytes;