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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
autorepair-tree-integrity.test.ts6.0 KB · 130 lines
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);
});