Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

autorepair-tree-integrity.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

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