/**
* Base resolution against real git topologies.
*
* The bug these pin down (#2160): CI passed `--base
* ${{ github.event.pull_request.base.sha }}` while checking out the MERGE REF.
* That sha is an ancestor of the merge ref, so `merge-base` returned it verbatim
* and every commit main landed since the PR opened was judged as the PR's own
* new code — failing PRs on files not in their diff. A three-dot diff does not
* help: `base...HEAD` collapses to `base..HEAD` exactly when base is an ancestor.
*
* Each scenario is built as a real repository, and every "fixed" assertion is
* paired with one showing the raw stale base still reproduces the failure — a
* test that only exercised the happy path would pass against the broken code.
*/
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { changedFiles } from "./changed-files.mjs";
import { resolveBase } from "./git-base.mjs";
/** A file `main` lands while the PR is open. Not in the PR's diff. */
const FOREIGN = "apps/web/components/settings/agent-generated-task-title-settings.tsx";
/** The PR's own new file. */
const OWNED = "apps/web/components/gitlab/watch-table.tsx";
/** Clean contents for OWNED: no user-facing literal for the ratchet to find. */
const CLEAN_SOURCE = "export const Watch = () => null;\n";
const tmpRoots: string[] = [];
afterAll(() => {
for (const dir of tmpRoots) fs.rmSync(dir, { recursive: true, force: true });
});
function git(cwd: string, args: string[], { quiet = false }: { quiet?: boolean } = {}) {
return execFileSync(
"git",
[
"-c",
"user.email=ratchet@example.test",
"-c",
"user.name=Ratchet Test",
"-c",
"commit.gpgsign=false",
// A developer machine may set core.hooksPath globally; an inherited
// pre-commit hook would fail these fixture commits for unrelated reasons.
"-c",
"core.hooksPath=/nonexistent",
...args,
],
{ cwd, encoding: "utf8", stdio: ["ignore", "pipe", quiet ? "ignore" : "inherit"] },
);
}
function write(dir: string, rel: string, body: string) {
const file = path.join(dir, rel);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, body);
}
function commit(dir: string, message: string) {
git(dir, ["add", "-A"]);
git(dir, ["commit", "-q", "-m", message]);
return git(dir, ["rev-parse", "HEAD"]).trim();
}
function initRepo() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kandev-ratchet-"));
tmpRoots.push(dir);
git(dir, ["init", "-q", "-b", "main"]);
write(dir, "apps/web/components/base/base.tsx", "export const Base = () => null;\n");
return { dir, root: commit(dir, "base") };
}
/** Stands in for `origin/main` without needing an actual remote. */
function setOriginMain(dir: string, sha: string) {
git(dir, ["update-ref", "refs/remotes/origin/main", sha]);
}
function landOnMain(dir: string) {
write(
dir,
FOREIGN,
[
'export const Card = () =>
Hardcoded
;\n"); commit(dir, "feat: add a literal"); setOriginMain(dir, mainTip); expect(changedFiles(baseOf(dir, ["--base", root]), "A", dir)).toEqual([OWNED]); }); }); describe("resolveBase on other checkout shapes", () => { it("floors a rebased branch, which has no merge commit but the same defect", () => { const { dir, root } = initRepo(); const mainTip = landOnMain(dir); setOriginMain(dir, mainTip); // Rebased onto the new tip: linear history that still CONTAINS main's commit. git(dir, ["checkout", "-q", "-b", "pr"]); write(dir, OWNED, CLEAN_SOURCE); commit(dir, "feat(i18n): localize GitLab"); expect(changedFiles(root, "A", dir)).toContain(FOREIGN); expect(baseOf(dir, ["--base", root])).toBe(mainTip); expect(changedFiles(baseOf(dir, ["--base", root]), "A", dir)).toEqual([OWNED]); }); it("does not advance a push build, where HEAD is the base branch tip", () => { const { dir, root } = initRepo(); const mainTip = landOnMain(dir); setOriginMain(dir, mainTip); // `push` to main passes github.event.before. Advancing to the fork point // would compare HEAD with itself and wave the pushed commit through. expect(baseOf(dir, ["--base", root])).toBe(root); expect(changedFiles(baseOf(dir, ["--base", root]), "A", dir)).toContain(FOREIGN); }); it("keeps an explicit base when there is no origin/main to floor against", () => { const { dir, root } = initRepo(); git(dir, ["checkout", "-q", "-b", "pr"]); write(dir, OWNED, CLEAN_SOURCE); commit(dir, "feat: add"); expect(baseOf(dir, ["--base", root])).toBe(root); }); }); describe("the guard-allowlist check inherits the same base", () => { /** * check-guard-allowlist.mjs reads the baseline with `git show