From 7fe08c5c92a2a00c6f13d5e7a7d3c2654fd302d5 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 14:59:19 -0700 Subject: [PATCH] Keep disposable Git fixture maintenance within test ownership --- ...sable-git-fixtures-must-own-maintenance.md | 55 +++++++++++++++++++ .agents/issues/INDEX.md | 1 + tests/lib/runner/unified-cli.test.mjs | 11 +++- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .agents/inbox/2026-10-27-disposable-git-fixtures-must-own-maintenance.md diff --git a/.agents/inbox/2026-10-27-disposable-git-fixtures-must-own-maintenance.md b/.agents/inbox/2026-10-27-disposable-git-fixtures-must-own-maintenance.md new file mode 100644 index 00000000..53a7a51d --- /dev/null +++ b/.agents/inbox/2026-10-27-disposable-git-fixtures-must-own-maintenance.md @@ -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. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index f0c5a0d8..dbbc318a 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -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 | diff --git a/tests/lib/runner/unified-cli.test.mjs b/tests/lib/runner/unified-cli.test.mjs index 8e56a277..a16b7892 100644 --- a/tests/lib/runner/unified-cli.test.mjs +++ b/tests/lib/runner/unified-cli.test.mjs @@ -28,8 +28,10 @@ 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, @@ -37,6 +39,13 @@ function fixture(t) { 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);