Fix History restore and diff truncation edge cases
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user