Skip to content

fix(tests): make scan-project fixtures hermetic against host global gitignore (#427)#448

Open
tirth8205 wants to merge 3 commits into
Egonex-AI:mainfrom
tirth8205:fix/scan-project-tests-hermetic-git-env
Open

fix(tests): make scan-project fixtures hermetic against host global gitignore (#427)#448
tirth8205 wants to merge 3 commits into
Egonex-AI:mainfrom
tirth8205:fix/scan-project-tests-hermetic-git-env

Conversation

@tirth8205

Copy link
Copy Markdown
Contributor

Problem

scan-project.mjs enumerates files via git ls-files -co --exclude-standard, which honors the host's core.excludesFile (the global gitignore). Many contributors keep .env / .env.local in their global gitignore. When they do, the test fixtures' dotfiles never reach the scanner, byPath(r.output, '.env') returns undefined, and the "dotfile configs" test dies with:

TypeError: Cannot read properties of undefined (reading 'fileCategory')

CI (ubuntu-latest) has no global gitignore, so it never reproduces this — it only bites local contributors. Nulling GIT_CONFIG_GLOBAL / GIT_CONFIG_SYSTEM is not enough, because core.excludesFile defaults to $XDG_CONFIG_HOME/git/ignore (or ~/.config/git/ignore) even with no config file present — the knob itself must be overridden.

Fix

Add a HERMETIC_GIT_ENV constant that nulls the global/system git config and overrides core.excludesFile via the GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0 mechanism, then pass it as env to both spawnSync calls — the git init in setupTree() and the node scan-project.mjs invocation in runScript(). This makes the fixtures hermetic against any host git configuration. The dotfile test's lookup is also guarded with toBeDefined() for a clearer failure message if this ever regresses.

Only the test file tests/skill/understand/test_scan_project.test.mjs changes; the scanner itself is untouched.

Testing

Reproduced the bug by simulating a host global gitignore that ignores .env:

export XDG_CONFIG_HOME="$(mktemp -d)"; mkdir -p "$XDG_CONFIG_HOME/git"
printf '.env\n.env.local\n' > "$XDG_CONFIG_HOME/git/ignore"
  • Before the fix, with that env: the suite fails 1/32 with the TypeError ... reading 'fileCategory' at the .env dotfile test.
  • After the fix, with that env: 32/32 pass.
  • After the fix, without that env (clean): 32/32 pass.

Lint (eslint) and core typecheck (tsc --noEmit) are green; the package test suite (vitest run tests/skill/understand/test_scan_project.test.mjs) passes 32/32 both with and without the simulated global ignore.

Hardening the regression (follow-up commit)

The original dotfile-config fixture was dotfile-only. When a host global gitignore hides every dotfile, git ls-files returns an empty list, so scan-project.mjs falls back to its (gitignore-unaware) recursive walker and re-discovers the dotfiles — masking the #427 leak even without the hermetic env. Adding a non-dotfile sibling (keep.ts) to that fixture keeps git ls-files output non-empty, forcing the git enumeration path so the regression reliably fails without the hermetic env (expected undefined to be defined) and passes with it. Verified red→green:

  • Unpatched env + simulated XDG_CONFIG_HOME global ignore → the dotfile test FAILS.
  • Unpatched env + no simulated ignore → 32/32 pass (explains why CI never caught it).
  • Patched (HERMETIC_GIT_ENV) → 32/32 pass both with and without the simulated ignore.

Full root vitest suite (16 files, 207 tests) and eslint on the changed file are green.

Fixes #427

🤖 Generated with Claude Code

tirth8205 and others added 2 commits June 14, 2026 18:02
…itignore (Egonex-AI#427)

scan-project.mjs enumerates files via `git ls-files -co --exclude-standard`,
which honors the host's core.excludesFile (global gitignore). Contributors who
keep `.env`/`.env.local` in their global gitignore had the fixture dotfiles
hidden from the scanner, so byPath('.env') returned undefined and the dotfile
configs test died with "Cannot read properties of undefined (reading
'fileCategory')". CI (ubuntu-latest) has no global gitignore so it never saw it.

Add a HERMETIC_GIT_ENV constant that nulls GIT_CONFIG_GLOBAL/SYSTEM and, crucially,
overrides core.excludesFile (which defaults to $XDG_CONFIG_HOME/git/ignore even
with no config file). Pass it as `env` to both the `git init` and the
`node scan-project.mjs` spawnSync calls so the fixtures are hermetic against any
host config. Also guard the dotfile test's lookup with toBeDefined() for a
clearer failure message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x-AI#427 reproduces on any host

The HERMETIC_GIT_ENV fix makes scan-project fixtures independent of the host
global gitignore, but the dotfile-config fixture was dotfile-only. When a host
global gitignore hides every dotfile, `git ls-files` returns an empty list,
which makes scan-project fall back to its (gitignore-unaware) recursive walker
and silently re-discover the dotfiles — masking the Egonex-AI#427 leak even without the
hermetic env.

Adding a non-dotfile sibling (`keep.ts`) keeps `git ls-files` output non-empty
so the git enumeration path is actually exercised. With this, the test reliably
FAILS without the hermetic env under a simulated global gitignore and PASSES
with it (verified both with and without a simulated `XDG_CONFIG_HOME` ignore).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@thejesh23 thejesh23 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. /dev/null paths are not portable to Windows contributors

The PR's stated purpose is unbreaking local contributor runs, but /dev/null doesn't exist on Windows — git on Windows will fail to open it as the core.excludesFile (and as the global/system config). On Windows the hermetic env will itself error out git ls-files, silently triggering the walker fallback (which is what the fix tries to avoid). Consider os.devNull (NUL on win32) or an empty tmp file created once per process.

2. GIT_CONFIG_NOSYSTEM not set; system include paths still resolvable

GIT_CONFIG_SYSTEM=/dev/null only redirects the primary system file; it does not stop git from reading $(prefix)/etc/gitconfig includes or honoring core.attributesFile / core.hooksPath from a system install. The canonical fully-hermetic recipe is GIT_CONFIG_NOSYSTEM=1 plus GIT_CONFIG_GLOBAL=/dev/null plus the GIT_CONFIG_COUNT override. Worth adding for completeness, since #427's root cause is exactly this class of leakage.

3. Only one fixture hardened against the silent-walker-fallback masking

The follow-up addition of keep.ts to force the git enumeration path is applied solely to the dotfile-configs test. Other fixtures that are dotfile-only or sparse (e.g., the .env / config-related setups elsewhere in this file) still risk emptying git ls-files and falling through to the gitignore-unaware walker, where the same class of host-config bug would be masked again. Either generalize setupTree to inject a sentinel non-dotfile, or assert r.stderr/source-of-truth is git in tests that care.

The HERMETIC_GIT_ENV nulled global/system git config and core.excludesFile
with a literal '/dev/null', which does not exist on Windows (git uses NUL).
On win32 this would either error or fail to null the sink, defeating the
hermetic guarantee the PR delivers. Use os.devNull, which resolves to
/dev/null on POSIX and NUL on win32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tirth8205

Copy link
Copy Markdown
Contributor Author

1. /dev/null not portable to Windows — Fixed. Imported devNull from node:os and replaced the three /dev/null literals in HERMETIC_GIT_ENV (GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, GIT_CONFIG_VALUE_0) with it, so the null sink resolves to NUL on win32 and /dev/null on POSIX. Added a JSDoc note explaining the cross-platform rationale. The existing 32 tests in this file (and the full 102-test skill suite) stay green.

2. GIT_CONFIG_NOSYSTEM not set — Not changed. Env-injected GIT_CONFIG_* overrides take final precedence over any config file (system, its includes, and global), so the GIT_CONFIG_COUNT/KEY_0/VALUE_0 override pins core.excludesFile to the null sink regardless of system includes — empirically git config --get core.excludesFile returns the override and git ls-files -co --exclude-standard still lists .env even when a system config tries to hide it. The other knobs cited (core.attributesFile, core.hooksPath) don't influence which untracked files git ls-files -co --exclude-standard enumerates (attributes affect diff/merge/eol; hooksPath affects hook lookup), so they can't reintroduce the dotfile-hiding bug. GIT_CONFIG_NOSYSTEM=1 would be harmless defense-in-depth but is a no-op for this specific failure mode.

3. Only one fixture hardened — Already covered. HERMETIC_GIT_ENV is applied at the env level to both setupTree's git init and runScript's scanner subprocess, so no host global gitignore can hide any fixture dotfile in any test, not just the dotfile-config one. The keep.ts sentinel is deliberately scoped to that one test as a red→green regression anchor (so it reliably fails when the env is absent rather than silently passing via the walker fallback). Generalizing setupTree to inject a sentinel into every fixture would break the exact-count assertions (totalFiles of 30/31/150/151/501) and isn't needed given the env-level fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: scan-project tests fail when the contributor's global gitignore excludes .env (fixture scans are not hermetic)

2 participants