Files
backnotprop__plannotator/packages/shared/vcs-core.test.ts
Raúl bc5470b90d fix(review): bound server memory for large tracked-file diffs (#1167)
* fix(review): bound server memory for large tracked-file diffs

PR #1118 renders large untracked files as binary additions, but staging
one moves it into the tracked `git diff` path, which had no size guard and
buffered the full multi-megabyte patch (~240 MB RSS on a 51 MB text
artifact). Any large tracked text file modified in the working tree hits
the same unguarded path.

Add a per-invocation `git -c core.bigFileThreshold=<MAX_REVIEW_FILE_CONTENT_BYTES>`
prefix to every content-producing git diff, so git renders oversized blobs
as "Binary files ... differ" instead of a text patch. Their bytes never
enter git's diff machinery or the server's buffered stdout, mirroring the
untracked-file guard. The flag is a no-op at or below the threshold, so
smaller files are byte-for-byte unaffected, and the blob hash git emits in
the binary diff still changes with content, so staleness detection holds.

The guard is applied in the shared cores, so the Bun and Pi runtimes
inherit it identically: `review-core.ts` covers the ordinary git provider
(working-tree, staged, commit, and the freshness fingerprint) and
`gitbutler-core.ts` covers the GitButler object diff. The jj provider runs
`jj diff`, which has no `core.bigFileThreshold` equivalent, so it is out of
scope here and stays unbounded as before.

* fix(review): preflight oversized tracked diffs

* fix(review): batch tracked diff preflight

* fix(review): restore browser-safe diff core

* fix(review): preserve gitlinks and textconv

* fix(review): require filesystem runtime seam

Fail compilation when a runtime omits file metadata or symlink support instead of silently disabling bounded reads and expansion.
2026-08-03 13:25:35 -07:00

454 lines
16 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type {
DiffResult,
DiffType,
GitContext,
ReviewGitRuntime,
} from "./review-core";
import {
type VcsProvider,
createGitProvider,
createVcsApi,
resolveInitialDiffType,
} from "./vcs-core";
function context(overrides: Partial<GitContext>): GitContext {
return {
currentBranch: "feature",
defaultBranch: "main",
diffOptions: [
{ id: "uncommitted", label: "Uncommitted changes" },
{ id: "merge-base", label: "Committed changes" },
],
worktrees: [],
availableBranches: { local: [], remote: [] },
vcsType: "git",
...overrides,
};
}
function provider(
id: string,
detected: boolean | (() => boolean),
ownedTypes: string[],
contextOverrides: Partial<GitContext> = {},
root?: string,
): VcsProvider {
const isDetected = () => typeof detected === "function" ? detected() : detected;
return {
id,
async detect() {
return isDetected();
},
async getRoot() {
return isDetected() ? root ?? "/repo" : null;
},
ownsDiffType(diffType: string) {
return ownedTypes.includes(diffType);
},
async getContext() {
return context({ vcsType: id as GitContext["vcsType"], ...contextOverrides });
},
async runDiff(diffType: DiffType, defaultBranch: string): Promise<DiffResult> {
return { patch: `${id}:${diffType}:${defaultBranch}`, label: `${id}:${defaultBranch}` };
},
async getFileContents() {
return { oldContent: id, newContent: id };
},
};
}
const gitRuntime: ReviewGitRuntime = {
async getFileInfo() {
return null;
},
async readLink() {
return null;
},
async runGit() {
return { stdout: "", stderr: "", exitCode: 0 };
},
async readTextFile() {
return null;
},
};
describe("createVcsApi", () => {
test("detects the first matching provider so jj wins colocated workspaces", async () => {
const jj = provider("jj", true, ["jj-current"], {}, "/repo");
const git = provider("git", true, ["uncommitted"], {}, "/repo");
const api = createVcsApi([jj, git]);
await expect(api.detectVcs("/repo")).resolves.toBe(jj);
await expect(api.getVcsContext("/repo")).resolves.toMatchObject({ vcsType: "jj" });
});
test("selects GitButler ahead of Git without changing JJ precedence", async () => {
const jj = provider("jj", false, ["jj-current"], {}, "/repo");
const gitButler = provider("gitbutler", true, ["gitbutler:workspace"], {}, "/repo");
const git = provider("git", true, ["uncommitted"], {}, "/repo");
const api = createVcsApi([jj, gitButler, git]);
await expect(api.detectVcs("/repo")).resolves.toBe(gitButler);
await expect(api.getVcsContext("/repo")).resolves.toMatchObject({ vcsType: "gitbutler" });
const colocatedJj = provider("jj", true, ["jj-current"], {}, "/repo");
const jjApi = createVcsApi([colocatedJj, gitButler, git]);
await expect(jjApi.detectVcs("/repo")).resolves.toBe(colocatedJj);
expect(api.vcsOwnsDiffType("gitbutler", "gitbutler:workspace")).toBe(true);
expect(api.vcsOwnsDiffType("gitbutler", "uncommitted")).toBe(false);
});
test("re-detects Git to GitButler to Git transitions in a long-lived runtime", async () => {
let gitButlerActive = false;
const gitButler = provider(
"gitbutler",
() => gitButlerActive,
["gitbutler:workspace"],
{ diffOptions: [{ id: "gitbutler:workspace", label: "Workspace" }] },
"/repo",
);
const git = provider("git", true, ["uncommitted"], {
diffOptions: [{ id: "uncommitted", label: "Uncommitted" }],
}, "/repo");
const api = createVcsApi([gitButler, git]);
const prepare = () => api.prepareLocalReviewDiff({
cwd: "/repo",
configuredDiffType: "since-base",
});
await expect(prepare()).resolves.toMatchObject({ gitContext: { vcsType: "git" } });
gitButlerActive = true;
await expect(prepare()).resolves.toMatchObject({
gitContext: { vcsType: "gitbutler" },
diffType: "gitbutler:workspace",
});
gitButlerActive = false;
await expect(prepare()).resolves.toMatchObject({ gitContext: { vcsType: "git" } });
});
test("detects the nearest VCS root so nested Git repos beat outer JJ workspaces", async () => {
const jj = provider("jj", true, ["jj-current"], { cwd: "/repo" }, "/repo");
const git = provider("git", true, ["uncommitted"], { cwd: "/repo/packages/tool" }, "/repo/packages/tool");
const api = createVcsApi([jj, git]);
await expect(api.detectVcs("/repo/packages/tool")).resolves.toBe(git);
await expect(api.getVcsContext("/repo/packages/tool")).resolves.toMatchObject({ vcsType: "git" });
});
test("detects the nearest VCS root so nested JJ workspaces beat outer Git repos", async () => {
const jj = provider("jj", true, ["jj-current"], { cwd: "/repo/packages/tool" }, "/repo/packages/tool");
const git = provider("git", true, ["uncommitted"], { cwd: "/repo" }, "/repo");
const api = createVcsApi([jj, git]);
await expect(api.detectVcs("/repo/packages/tool")).resolves.toBe(jj);
await expect(api.getVcsContext("/repo/packages/tool")).resolves.toMatchObject({ vcsType: "jj" });
});
test("continues probing providers when root detection throws", async () => {
const brokenGit = {
...provider("git", true, ["uncommitted"]),
async getRoot() {
throw new Error("git failed");
},
};
const p4 = provider("p4", true, ["p4-default"], {}, "/repo");
const api = createVcsApi([brokenGit, p4]);
await expect(api.detectVcs("/repo")).resolves.toBe(p4);
await expect(api.getVcsContext("/repo")).resolves.toMatchObject({ vcsType: "p4" });
});
test("detectManagedVcs returns null instead of falling back when no provider detects a workspace", async () => {
const git = provider("git", false, ["uncommitted"]);
const api = createVcsApi([git]);
await expect(api.detectManagedVcs("/not-a-repo")).resolves.toBeNull();
await expect(api.detectVcs("/not-a-repo")).resolves.toBe(git);
});
test("detectManagedVcs respects forced VCS selection without throwing", async () => {
const jj = provider("jj", true, ["jj-current"], {}, "/repo");
const git = provider("git", false, ["uncommitted"]);
const api = createVcsApi([jj, git]);
await expect(api.detectManagedVcs("/repo", "git")).resolves.toBeNull();
await expect(api.detectManagedVcs("/repo", "jj")).resolves.toBe(jj);
});
test("detectManagedVcs returns null when forced provider detection throws", async () => {
const git = {
...provider("git", true, ["uncommitted"]),
async detect() {
throw new Error("git failed");
},
};
const api = createVcsApi([git]);
await expect(api.detectManagedVcs("/repo", "git")).resolves.toBeNull();
});
test("routes operations by diff type before falling back to detection", async () => {
const jj = provider("jj", false, ["jj-current"]);
const git = provider("git", true, ["uncommitted"]);
const api = createVcsApi([jj, git]);
await expect(api.runVcsDiff("jj-current", "trunk()", "/repo")).resolves.toMatchObject({
patch: "jj:jj-current:trunk()",
});
await expect(api.runVcsDiff("uncommitted", "main", "/repo")).resolves.toMatchObject({
patch: "git:uncommitted:main",
});
});
test("limits Git staging to working-tree diff modes", async () => {
const git = createVcsApi([createGitProvider(gitRuntime)]);
await expect(git.canStageFiles("uncommitted", "/repo")).resolves.toBe(true);
await expect(git.canStageFiles("unstaged", "/repo")).resolves.toBe(true);
await expect(git.canStageFiles("worktree:/repo:uncommitted", "/repo")).resolves.toBe(true);
await expect(git.canStageFiles("staged", "/repo")).resolves.toBe(false);
await expect(git.canStageFiles("branch", "/repo")).resolves.toBe(false);
await expect(git.canStageFiles("merge-base", "/repo")).resolves.toBe(false);
// Historical commit diffs have nothing stageable — plain and worktree forms.
await expect(git.canStageFiles("commit:abc1234", "/repo")).resolves.toBe(false);
await expect(git.canStageFiles("worktree:/repo:commit:abc1234", "/repo")).resolves.toBe(false);
});
test("the git provider owns commit:<sha> diff types", () => {
const git = createGitProvider(gitRuntime);
expect(git.ownsDiffType("commit:abc1234")).toBe(true);
expect(git.ownsDiffType("worktree:/repo:commit:abc1234")).toBe(true);
});
test("requires providers to explicitly opt into staging", async () => {
const stageFile = async () => {};
const unstageFile = async () => {};
const api = createVcsApi([
{
...provider("custom", true, ["custom-diff"]),
stageFile,
unstageFile,
},
]);
await expect(api.canStageFiles("custom-diff", "/repo")).resolves.toBe(false);
await expect(api.stageFile("custom-diff", "tracked.txt", "/repo")).rejects.toThrow(
"Staging not available for custom",
);
await expect(api.unstageFile("custom-diff", "tracked.txt", "/repo")).rejects.toThrow(
"Unstaging not available for custom",
);
});
test("prepares JJ local reviews by ignoring Git-shaped requested options", async () => {
const jj = provider("jj", true, ["jj-current", "jj-line"], {
defaultBranch: "trunk()",
diffOptions: [
{ id: "jj-current", label: "Current change" },
{ id: "jj-line", label: "Line of work" },
],
vcsType: "jj",
});
const git = provider("git", true, ["merge-base", "uncommitted"]);
const api = createVcsApi([jj, git]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
requestedDiffType: "merge-base",
requestedBase: "main",
configuredDiffType: "unstaged",
})).resolves.toMatchObject({
diffType: "jj-current",
base: "trunk()",
rawPatch: "jj:jj-current:trunk()",
});
});
test("prepares local reviews by preserving valid requested diff types for the detected VCS", async () => {
const jj = provider("jj", true, ["jj-current", "jj-line"], {
defaultBranch: "trunk()",
diffOptions: [
{ id: "jj-current", label: "Current change" },
{ id: "jj-line", label: "Line of work" },
],
vcsType: "jj",
});
const api = createVcsApi([jj]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
requestedDiffType: "jj-line",
requestedBase: "feature@origin",
configuredDiffType: "unstaged",
})).resolves.toMatchObject({
diffType: "jj-line",
base: "feature@origin",
rawPatch: "jj:jj-line:feature@origin",
});
});
test("uses provider context captured atomically with the prepared patch", async () => {
const initialContext = context({
vcsType: "gitbutler",
defaultBranch: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
diffOptions: [{ id: "gitbutler:workspace", label: "Workspace" }],
});
const patchContext = context({
vcsType: "gitbutler",
defaultBranch: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
diffOptions: [{ id: "gitbutler:workspace", label: "Workspace" }],
});
const gitButler: VcsProvider = {
...provider("gitbutler", true, ["gitbutler:workspace"]),
async getContext() {
return initialContext;
},
async runDiff() {
return {
patch: "atomic patch",
label: "Workspace",
gitContext: patchContext,
fingerprint: "atomic fingerprint",
};
},
};
const api = createVcsApi([gitButler]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
configuredDiffType: "since-base",
})).resolves.toMatchObject({
gitContext: patchContext,
base: patchContext.defaultBranch,
rawPatch: "atomic patch",
fingerprint: "atomic fingerprint",
});
});
test("prepares Git local reviews by honoring valid requested base and ignoring JJ diff modes", async () => {
const git = provider("git", true, ["uncommitted", "merge-base"]);
const api = createVcsApi([git]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
requestedDiffType: "jj-line",
requestedBase: "develop",
configuredDiffType: "merge-base",
})).resolves.toMatchObject({
diffType: "merge-base",
base: "develop",
rawPatch: "git:merge-base:develop",
});
});
test("can force Git for local review startup in colocated JJ workspaces", async () => {
const jj = provider("jj", true, ["jj-current"], {
defaultBranch: "trunk()",
diffOptions: [{ id: "jj-current", label: "Current change" }],
vcsType: "jj",
});
const git = provider("git", true, ["uncommitted", "merge-base"]);
const api = createVcsApi([jj, git]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
vcsType: "git",
configuredDiffType: "merge-base",
})).resolves.toMatchObject({
gitContext: { vcsType: "git" },
diffType: "merge-base",
base: "main",
rawPatch: "git:merge-base:main",
});
});
test("can force GitButler and ignores Git-shaped saved defaults and bases", async () => {
const gitButler = provider("gitbutler", true, ["gitbutler:workspace"], {
defaultBranch: "abc1234",
diffOptions: [{ id: "gitbutler:workspace", label: "Workspace" }],
vcsType: "gitbutler",
});
const git = provider("git", true, ["uncommitted", "merge-base"]);
const api = createVcsApi([gitButler, git]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
vcsType: "gitbutler",
requestedDiffType: "merge-base",
requestedBase: "main",
configuredDiffType: "unstaged",
})).resolves.toMatchObject({
gitContext: { vcsType: "gitbutler" },
diffType: "gitbutler:workspace",
base: "abc1234",
rawPatch: "gitbutler:gitbutler:workspace:abc1234",
});
});
test("reports a clear error when forced Git is unavailable", async () => {
const jj = provider("jj", true, ["jj-current"], {
defaultBranch: "trunk()",
diffOptions: [{ id: "jj-current", label: "Current change" }],
vcsType: "jj",
});
const git = provider("git", false, ["uncommitted", "merge-base"]);
const api = createVcsApi([jj, git]);
await expect(api.prepareLocalReviewDiff({
cwd: "/repo",
vcsType: "git",
configuredDiffType: "merge-base",
})).rejects.toThrow("Git workspace not found.");
});
test("refreshes context and remote defaults with the forced VCS", async () => {
const jj = provider("jj", true, ["jj-current"], {
defaultBranch: "trunk()",
diffOptions: [{ id: "jj-current", label: "Current change" }],
vcsType: "jj",
});
const git = {
...provider("git", true, ["uncommitted", "merge-base"]),
detectRemoteDefaultCompareTarget: async () => "origin/main",
};
const api = createVcsApi([jj, git]);
await expect(api.getVcsContext("/repo", "git")).resolves.toMatchObject({
vcsType: "git",
defaultBranch: "main",
});
await expect(api.detectRemoteDefaultCompareTarget("/repo", "git")).resolves.toBe("origin/main");
});
});
describe("resolveInitialDiffType", () => {
test("preserves configured Git diff modes when available", () => {
expect(resolveInitialDiffType(context({}), "merge-base")).toBe("merge-base");
});
test("uses p4-default for P4 contexts", () => {
expect(resolveInitialDiffType(context({ vcsType: "p4" }), "merge-base")).toBe("p4-default");
});
test("ignores saved Git defaults for jj contexts", () => {
const jjContext = context({
defaultBranch: "trunk()",
diffOptions: [
{ id: "jj-current", label: "Current change" },
{ id: "jj-line", label: "Line of work" },
{ id: "jj-all", label: "All files" },
],
vcsType: "jj",
});
expect(resolveInitialDiffType(jjContext, "all")).toBe("jj-current");
expect(resolveInitialDiffType(jjContext, "merge-base")).toBe("jj-current");
expect(resolveInitialDiffType(jjContext, "unstaged")).toBe("jj-current");
});
test("falls back to the first available option for unknown non-jj modes", () => {
expect(resolveInitialDiffType(context({}), "jj-current")).toBe("uncommitted");
});
});