test(#2665): ship the hermeticity guard at report level, not fatal

Its first CI run found PRE-EXISTING leaks on the Windows lane —
C:\Users\runneradmin\.claude\gsd-core and skills\gsd-dev-preferences — with all
1196 Windows tests otherwise passing. os.homedir() reads USERPROFILE on Windows,
and ~190 test sites across 31 files sandbox HOME alone, so the suite has been
installing GSD into the runner's real home directory invisibly. That is exactly
the class the guard exists to surface, and exactly the class this PR's review
said CI could never catch.

It is also a different defect from the one #2665 closes, and too large to fold in
here. A brand-new gate that immediately reds an unrelated lane gets bypassed or
reverted rather than obeyed, so the guard reports by default and fails only under
GSD_STRICT_LIVE_CONFIG_GUARD=1.

This is the repo's own established ratchet, not a hedge: the local/no-source-grep
ESLint rule shipped at `warn` and was promoted to `error` after its cleanup sweep
(ADR 452). Promote this the same way once the USERPROFILE sweep lands.
This commit is contained in:
0xdhx
2026-07-28 06:19:20 -05:00
parent 5863351281
commit e2eed1c58a
3 changed files with 23 additions and 4 deletions

View File

@@ -41,6 +41,17 @@
* KNOWN GAP — a leak into a file GSD does not own (e.g. mutating the host's own * KNOWN GAP — a leak into a file GSD does not own (e.g. mutating the host's own
* `.claude.json`) is outside this guard by construction. Closing it would require * `.claude.json`) is outside this guard by construction. Closing it would require
* watching shared files, which is the false-positive trap above. * watching shared files, which is the false-positive trap above.
*
* SEVERITY — reports by default, fails only under GSD_STRICT_LIVE_CONFIG_GUARD=1.
* Not timidity: on its first CI run this guard found PRE-EXISTING leaks on the
* Windows lane (`C:\Users\runneradmin\.claude\gsd-core` and
* `skills\gsd-dev-preferences`), because os.homedir() reads USERPROFILE there and
* ~190 test sites sandbox HOME alone. Those are real and worth fixing, but they
* are a different defect class from the one #2665 closes, and a brand-new gate
* that immediately reds an unrelated lane gets bypassed or reverted rather than
* obeyed. This repo already has the pattern: the local/no-source-grep ESLint rule
* shipped at `warn` and was promoted to `error` after its cleanup sweep (ADR 452).
* Promote this the same way once the USERPROFILE sweep lands.
*/ */
const fs = require('fs'); const fs = require('fs');
@@ -209,7 +220,7 @@ function diffLiveConfig(before, after) {
function formatViolations(violations) { function formatViolations(violations) {
const lines = [ const lines = [
'', '',
'run-tests: HERMETICITY FAILURE — the suite wrote into a LIVE config directory.', 'run-tests: HERMETICITY WARNING — the suite wrote into a LIVE config directory.',
'', '',
'A test resolved a runtime config dir from the ambient environment instead of a', 'A test resolved a runtime config dir from the ambient environment instead of a',
'sandbox. The usual cause is an IN-PROCESS install() call: tests/helpers.cjs', 'sandbox. The usual cause is an IN-PROCESS install() call: tests/helpers.cjs',
@@ -225,7 +236,10 @@ function formatViolations(violations) {
lines.push(` ${label}: ${v.path}`); lines.push(` ${label}: ${v.path}`);
} }
lines.push(''); lines.push('');
lines.push('Set GSD_SKIP_LIVE_CONFIG_GUARD=1 to bypass (intentionally loud).'); lines.push(
'Reporting only. Set GSD_STRICT_LIVE_CONFIG_GUARD=1 to make this fail the run, ' +
'or GSD_SKIP_LIVE_CONFIG_GUARD=1 to skip the check entirely.',
);
lines.push(''); lines.push('');
return lines.join('\n'); return lines.join('\n');
} }

View File

@@ -1055,7 +1055,11 @@ function main() {
const violations = diffLiveConfig(liveConfigBefore, snapshotLiveConfig(liveConfigRoots)); const violations = diffLiveConfig(liveConfigBefore, snapshotLiveConfig(liveConfigRoots));
if (violations.length > 0) { if (violations.length > 0) {
console.error(formatViolations(violations)); console.error(formatViolations(violations));
if (firstFailureExit === 0) firstFailureExit = 1; // Reports by default; fails only under opt-in strict mode. See the
// SEVERITY note in scripts/live-config-guard.cjs for why.
if (process.env.GSD_STRICT_LIVE_CONFIG_GUARD === '1' && firstFailureExit === 0) {
firstFailureExit = 1;
}
} }
} }

View File

@@ -152,9 +152,10 @@ describe('#2665: live-config hermeticity guard', () => {
test('the report names the path and the remedy', () => { test('the report names the path and the remedy', () => {
const out = formatViolations([{ path: '/live/.claude/gsd-core', kind: 'created' }]); const out = formatViolations([{ path: '/live/.claude/gsd-core', kind: 'created' }]);
assert.match(out, /HERMETICITY FAILURE/); assert.match(out, /HERMETICITY WARNING/);
assert.match(out, /\/live\/\.claude\/gsd-core/); assert.match(out, /\/live\/\.claude\/gsd-core/);
assert.match(out, /scrubConfigLocationEnv/); assert.match(out, /scrubConfigLocationEnv/);
assert.match(out, /GSD_SKIP_LIVE_CONFIG_GUARD/); assert.match(out, /GSD_SKIP_LIVE_CONFIG_GUARD/);
assert.match(out, /GSD_STRICT_LIVE_CONFIG_GUARD/);
}); });
}); });