Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Disposable Git fixtures must prevent detached maintenance before cleanup

Date: 2026-10-27.

## Evidence

Comprehensive run [40589229879](https://github.com/GraphDone/GraphDone-Core/actions/runs/40589229879)
on OAuth head `809f17f1301a1354148d2b25114437301dc4f06f` failed its
Node 22.18 harness. Two invalid CLI selection cases completed their assertions,
then `t.after` reported `ENOTEMPTY` removing `.git` and `.git/objects`.
The runner used Git 2.55.0. These invalid options exit before source provenance
collection, whose Git calls are synchronous anyway.

An independent owned Git 2.55 probe recorded 12/12 fixture commits starting
`git maintenance run --auto --quiet --detach`. Ten traces recorded descendant
activity after the commit parent exit. Repository-local `maintenance.auto=false`
prevented all 12 launches. Strict local removal passed both variants; the exact
CI cleanup error was not reproduced. Temporary probe evidence is at
`/tmp/graphdone-review-git255-mofpt0da/trace-summary.json` on the investigating
host, not a portable or published artifact.

The committed regression captures `GIT_TRACE2_EVENT` from the real fixture
commit into its ignored `bin/` directory and rejects maintenance child launches.
On local Git 2.43 / Node 22.18, running
`node --test --test-name-pattern='disposable Git fixture' tests/lib/runner/unified-cli.test.mjs`
failed against the old setup because a maintenance child was recorded.

## Cause and corrective rule

A synchronous Git parent does not guarantee all its descendants have exited.
Newer Git versions can detach automatic maintenance before the fixture owner
removes its repository. This is a confirmed lifetime hazard and the strongest
explanation of the CI error, not proof of which exact file the CI writer made.
See the [Git maintenance contract](https://git-scm.com/docs/git-maintenance).

Immediately after initializing a disposable fixture, configure repository-local
`maintenance.auto=false` and `gc.auto=0` before adding or committing files.
Keep strict cleanup, failure propagation, and provenance checks. Do not add
cleanup retries to conceal a live writer or alter global/developer repository
configuration. Observe the actual child process behavior in the regression.

## Limits

This applies only to repositories owned by the test fixture. It neither
suppresses external processes nor certifies arbitrary inherited Git hooks.
No application runtime, OAuth behavior, or production Git configuration changes.
Final supported-Node CI remains required before integration.

## Corrected validation

The complete CLI suite passed 28/28 on local Git 2.43 and on the owned Git
2.55 build. A pinned clean install followed by `npm run test:unified:lib`
passed 175/175, with no skips, cancellations or retries. Both invalid-option
assertions and strict cleanup remain intact. The correction is isolated to
fixture initialization plus its real-process regression and this guidance.
1 change: 1 addition & 0 deletions .agents/issues/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

| Symptom | Canonical lesson and regression |
| --- | --- |
| Disposable Git fixture teardown reports ENOTEMPTY under .git after a synchronous commit | [Prevent detached fixture maintenance](../inbox/2026-10-27-disposable-git-fixtures-must-own-maintenance.md); `tests/lib/runner/unified-cli.test.mjs` |
| A router major update passes install but path handling or route UI assertions differ | [Validate installed navigation and current UI contracts](../inbox/2026-10-27-router-upgrades-need-current-consumer-contracts.md); `routerNavigation.test.tsx`, `router-navigation.spec.ts`, dependency guard |
| Status-note text lands in the title and Add remains disabled after quick edit opens | [Delayed autofocus must preserve chosen focus](../inbox/2026-10-27-delayed-autofocus-must-preserve-chosen-focus.md); `QuickEditSurface.test.tsx` controls the pending frame |
| Minimap zoom or overview inspection is overwritten while a newly loaded graph settles | [Explicit camera input must stop automatic framing](../inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md); timer-controlled `semantic-lod.spec.ts` negative control |
Expand Down
11 changes: 10 additions & 1 deletion tests/lib/runner/unified-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ function fixture(t) {
writeFileSync(join(dir, 'sentinel.txt'), 'keep source');
writeFileSync(join(dir, 'bin/npx'), `#!${process.execPath}\nconst fs=require('node:fs'); const report={success:true,numTotalTests:1,testResults:[{status:'passed',assertionResults:[{title:'fixture',status:'passed'}]}]}; process.stdout.write(JSON.stringify(report));\n`, { mode: 0o700 });
execFileSync('git', ['init', '-q'], { cwd: dir });
execFileSync('git', ['config', '--local', 'maintenance.auto', 'false'], { cwd: dir });
execFileSync('git', ['config', '--local', 'gc.auto', '0'], { cwd: dir });
execFileSync('git', ['add', '.'], { cwd: dir });
execFileSync('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', 'fixture'], { cwd: dir });
execFileSync('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', 'fixture'], { cwd: dir, env: { ...process.env, GIT_TRACE2_EVENT: join(dir, 'bin/git-trace.jsonl') } });
t.after(() => rmSync(dir, { recursive: true, force: true }));
const run = args => spawnSync(process.execPath, ['tests/run-unified.mjs', ...args], {
cwd: dir, env: { ...process.env, PATH: `${join(dir, 'bin')}:${process.env.PATH}` }, encoding: 'utf8', timeout: 10000,
});
return { dir, run };
}

test('disposable Git fixture commits do not spawn background maintenance', t => {
const { dir } = fixture(t);
const events = readFileSync(join(dir, 'bin/git-trace.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
assert.ok(events.some(event => event.event === 'exit' && event.code === 0));
assert.deepEqual(events.filter(event => event.event === 'child_start' && event.argv?.includes('maintenance')), []);
});

for (const sequence of ['new-user', 'semantic-lod', 'router-navigation', 'mobile', 'perf-budgets']) {
test(`CLI forwards failure-only tracing without retries for ${sequence}`, t => {
const { dir, run } = fixture(t);
Expand Down
Loading