CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | /**
* Regression test for the 2026-07-16 incident: autoRepair()'s tree-rebuild
* used to feed `git ls-tree -r` output (full nested paths like
* "src/lib/foo.ts") directly into `git mktree`, which only ever builds a
* single tree level and fatally rejects any path containing a slash. None
* of the git subprocess calls in that path checked their exit code, so the
* failure was silently swallowed: the resulting "tree" only contained the
* handful of top-level files that happened to parse, and that near-empty
* tree got committed and force-written over the branch ref — silently
* deleting every nested file in the repository on a routine push-time
* repair (e.g. a couple of missing .gitignore entries).
*
* This test seeds a real bare repo (via plain plumbing — no clone/push, to
* avoid unrelated flakiness in this environment) with a nested directory
* structure, runs a real autoRepair() against it, and asserts every nested
* file survives with its original content intact.
*/
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { mkdir, rm, writeFile } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { initBareRepo, getRepoPath } from "../git/repository";
import { autoRepair } from "../lib/autorepair";
const GIT_REPOS_PATH = join(tmpdir(), "gluecron-autorepair-tree-" + Date.now());
const OWNER = "acme";
const REPO = "widgets";
let barePath: string;
async function git(args: string[], cwd?: string): Promise<{ stdout: string; exitCode: number }> {
const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
const stdout = (await new Response(proc.stdout).text()).trim();
const exitCode = await proc.exited;
return { stdout, exitCode };
}
beforeAll(async () => {
await mkdir(GIT_REPOS_PATH, { recursive: true });
process.env.GIT_REPOS_PATH = GIT_REPOS_PATH;
barePath = await initBareRepo(OWNER, REPO);
expect(barePath).toBe(getRepoPath(OWNER, REPO));
// Seed a nested-directory commit directly via plumbing (no clone/push —
// avoids unrelated network/protocol flakiness in this environment).
const workDir = join(GIT_REPOS_PATH, "_seed-work");
await mkdir(join(workDir, "src", "lib"), { recursive: true });
await mkdir(join(workDir, "src", "routes"), { recursive: true });
await mkdir(join(workDir, "docs"), { recursive: true });
// .gitignore missing some of the "essential entries" autoRepair adds —
// triggers REPAIR 1 (gitignore).
await writeFile(join(workDir, ".gitignore"), "node_modules/\n");
// package.json so autoRepair treats this as a JS project.
await writeFile(join(workDir, "package.json"), JSON.stringify({ name: REPO }, null, 2) + "\n");
// Nested file with trailing whitespace — triggers REPAIR 2 (whitespace),
// specifically exercising the "update an existing nested entry" path that
// the original mktree bug corrupted.
await writeFile(join(workDir, "src", "lib", "foo.ts"), "export const foo = 1; \n");
// Nested files that should survive UNTOUCHED — these are the ones the
// original bug silently deleted.
await writeFile(join(workDir, "src", "routes", "bar.ts"), "export const bar = 2;\n");
await writeFile(join(workDir, "docs", "notes.md"), "# Notes\n\nNothing to see here.\n");
await writeFile(join(workDir, "README.md"), "# widgets\n");
const gitArgs = (...args: string[]) => [`--git-dir=${barePath}`, `--work-tree=${workDir}`, ...args];
await git(gitArgs("add", "-A"), workDir);
const commit = await git(
gitArgs("-c", "user.email=test@test.com", "-c", "user.name=Test", "commit", "-m", "initial"),
workDir
);
expect(commit.exitCode).toBe(0);
await rm(workDir, { recursive: true, force: true });
}, 30000);
afterAll(async () => {
delete process.env.GIT_REPOS_PATH;
await rm(GIT_REPOS_PATH, { recursive: true, force: true }).catch(() => {});
});
describe("autoRepair — tree integrity after a repair", () => {
it("keeps every nested file after committing a repair (2026-07-16 regression)", async () => {
const before = await git(["ls-tree", "-r", "--name-only", "main"], barePath);
expect(before.exitCode).toBe(0);
const beforeFiles = before.stdout.split("\n").filter(Boolean).sort();
expect(beforeFiles).toContain("src/lib/foo.ts");
expect(beforeFiles).toContain("src/routes/bar.ts");
expect(beforeFiles).toContain("docs/notes.md");
const result = await autoRepair(OWNER, REPO, "main");
expect(result.repaired).toBe(true);
expect(result.commitSha).not.toBeNull();
expect(result.repairs.length).toBeGreaterThanOrEqual(2);
const after = await git(["ls-tree", "-r", "--name-only", "main"], barePath);
expect(after.exitCode).toBe(0);
const afterFiles = after.stdout.split("\n").filter(Boolean).sort();
// Every file present before the repair must still be present after.
for (const f of beforeFiles) {
expect(afterFiles).toContain(f);
}
// The untouched nested files must be byte-for-byte unchanged.
const bar = await git(["show", "main:src/routes/bar.ts"], barePath);
expect(bar.stdout).toBe("export const bar = 2;");
const notes = await git(["show", "main:docs/notes.md"], barePath);
expect(notes.stdout).toBe("# Notes\n\nNothing to see here.");
// The repaired nested file lost its trailing whitespace.
const foo = await git(["show", "main:src/lib/foo.ts"], barePath);
expect(foo.stdout).toBe("export const foo = 1;");
// The repaired .gitignore gained the missing entries.
const gitignore = await git(["show", "main:.gitignore"], barePath);
expect(gitignore.stdout).toContain(".env");
}, 30000);
it("makes no commit at all if a git subprocess fails (fail-closed)", async () => {
// Point at a ref that doesn't exist — every downstream git call should
// fail cleanly and autoRepair must report repaired:false, never a
// partial/corrupt commit.
const result = await autoRepair(OWNER, REPO, "no-such-branch");
expect(result.repaired).toBe(false);
expect(result.commitSha).toBeNull();
}, 15000);
});
|