test(#2665): fail the suite when it writes into a live config dir
The recurrence guard, and #2665's own "Optional hardening". This class is silent by construction: TEST_ENV_BASE cannot see an in-process caller, and CI cannot see the class at all because CI never has these env vars set. It damages the developer's machine and reports nothing -- which is how two prior authors each diagnosed it and fixed only the instance in front of them. run-tests.cjs snapshots GSD's install footprint in every live runtime config dir before the suite and re-checks it after, failing the run on a create or a modify. Roots come from the product's own getGlobalConfigDir, so the guard watches wherever the product actually points, including through an ambient var. Scope is ownership-based, not whole-root: the top-level install footprint plus gsd-prefixed children of dirs GSD shares with the host agent. A config root like ~/.claude is shared, and watching it wholesale would false-positive on the host's own history.jsonl or settings.json -- a guard that cries wolf gets disabled, and then catches nothing. The prefix test is load-bearing: the first version watched only the three top-level entries and MISSED a real leak into skills/gsd-*. It earned its place immediately -- it is what found the fifth in-process leak in runtime-artifact-layout.test.cjs, which no amount of reading the review would have surfaced. Known gap documented in the module: a write to a file GSD does not own is out of scope by construction. Lives in scripts/, deliberately NOT scripts/lib/ -- the installer copies that dir into every user's config dir wholesale while uninstall removes only an allowlist, so a test-only module there would ship to users and survive uninstall. Addresses review finding: Minor 8.
This commit is contained in:
160
tests/live-config-guard.test.cjs
Normal file
160
tests/live-config-guard.test.cjs
Normal file
@@ -0,0 +1,160 @@
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
GSD_OWNED_ENTRIES,
|
||||
resolveLiveConfigRoots,
|
||||
snapshotLiveConfig,
|
||||
diffLiveConfig,
|
||||
formatViolations,
|
||||
} = require('../scripts/live-config-guard.cjs');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
function tmpRoot() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'live-config-guard-'));
|
||||
}
|
||||
|
||||
describe('#2665: live-config hermeticity guard', () => {
|
||||
test('resolves real runtime config roots via the product resolver', () => {
|
||||
const roots = resolveLiveConfigRoots();
|
||||
// Guards the guard: an empty set would make every downstream assertion
|
||||
// vacuous, and run-tests.cjs would silently skip the check.
|
||||
assert.ok(roots.length > 5, `expected many runtime config roots, got ${roots.length}`);
|
||||
for (const root of roots) {
|
||||
assert.ok(path.isAbsolute(root), `root must be absolute: ${root}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a clean run produces no violations', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
fs.mkdirSync(path.join(root, 'gsd-core', 'bin'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'gsd-core', 'bin', 'x.cjs'), 'x');
|
||||
|
||||
const before = snapshotLiveConfig([root]);
|
||||
const after = snapshotLiveConfig([root]);
|
||||
assert.deepStrictEqual(diffLiveConfig(before, after), []);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('detects a global install CREATED during the run', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
const before = snapshotLiveConfig([root]);
|
||||
// Exactly the Blocker 1 shape: an in-process install(true, …) landing a
|
||||
// full global install in a live config dir that was previously empty.
|
||||
fs.mkdirSync(path.join(root, 'gsd-core'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'gsd-file-manifest.json'), '{}');
|
||||
|
||||
const violations = diffLiveConfig(before, snapshotLiveConfig([root]));
|
||||
const kinds = Object.fromEntries(violations.map((v) => [path.basename(v.path), v.kind]));
|
||||
assert.strictEqual(kinds['gsd-core'], 'created');
|
||||
assert.strictEqual(kinds['gsd-file-manifest.json'], 'created');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('detects an existing install MODIFIED during the run', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
const target = path.join(root, 'gsd-core', 'bin');
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
const file = path.join(target, 'gsd-tools.cjs');
|
||||
fs.writeFileSync(file, 'original');
|
||||
|
||||
const before = snapshotLiveConfig([root]);
|
||||
// mtime resolution is coarse on some filesystems; set it forward explicitly
|
||||
// rather than racing the clock with a sleep.
|
||||
const future = new Date(Date.now() + 10000);
|
||||
fs.writeFileSync(file, 'clobbered');
|
||||
fs.utimesSync(file, future, future);
|
||||
|
||||
const violations = diffLiveConfig(before, snapshotLiveConfig([root]));
|
||||
assert.strictEqual(violations.length, 1);
|
||||
assert.strictEqual(violations[0].kind, 'modified');
|
||||
assert.strictEqual(path.basename(violations[0].path), 'gsd-core');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('ignores non-GSD writes in a shared config root', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
const before = snapshotLiveConfig([root]);
|
||||
// A concurrent host-agent session writing its own state must NOT trip the
|
||||
// guard — a guard that cries wolf gets disabled, and then catches nothing.
|
||||
fs.writeFileSync(path.join(root, 'history.jsonl'), '{}');
|
||||
fs.mkdirSync(path.join(root, 'todos'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'settings.json'), '{}');
|
||||
|
||||
assert.deepStrictEqual(diffLiveConfig(before, snapshotLiveConfig([root])), []);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('watches exactly the GSD-owned entry set', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
const snap = snapshotLiveConfig([root]);
|
||||
const watched = Object.keys(snap).map((p) => path.basename(p)).sort();
|
||||
assert.deepStrictEqual(watched, [...GSD_OWNED_ENTRIES].sort());
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('detects a gsd-prefixed artifact written into a SHARED dir', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
fs.mkdirSync(path.join(root, 'skills'), { recursive: true });
|
||||
const before = snapshotLiveConfig([root]);
|
||||
// The exact leak the first version of this guard MISSED: a writer that
|
||||
// sandboxed HOME but inherited an ambient CLAUDE_CONFIG_DIR landed
|
||||
// <live>/skills/gsd-dev-preferences/SKILL.md, outside the three
|
||||
// top-level GSD entries.
|
||||
fs.mkdirSync(path.join(root, 'skills', 'gsd-dev-preferences'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'skills', 'gsd-dev-preferences', 'SKILL.md'), '# x');
|
||||
|
||||
const violations = diffLiveConfig(before, snapshotLiveConfig([root]));
|
||||
assert.strictEqual(violations.length, 1);
|
||||
assert.strictEqual(violations[0].kind, 'created');
|
||||
assert.strictEqual(path.basename(violations[0].path), 'gsd-dev-preferences');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('ignores NON-gsd artifacts in a shared dir', () => {
|
||||
const root = tmpRoot();
|
||||
try {
|
||||
fs.mkdirSync(path.join(root, 'skills'), { recursive: true });
|
||||
const before = snapshotLiveConfig([root]);
|
||||
// The host agent's own skills must not trip the guard.
|
||||
fs.mkdirSync(path.join(root, 'skills', 'my-personal-skill'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'skills', 'my-personal-skill', 'SKILL.md'), '# mine');
|
||||
|
||||
assert.deepStrictEqual(diffLiveConfig(before, snapshotLiveConfig([root])), []);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('the report names the path and the remedy', () => {
|
||||
const out = formatViolations([{ path: '/live/.claude/gsd-core', kind: 'created' }]);
|
||||
assert.match(out, /HERMETICITY FAILURE/);
|
||||
assert.match(out, /\/live\/\.claude\/gsd-core/);
|
||||
assert.match(out, /scrubConfigLocationEnv/);
|
||||
assert.match(out, /GSD_SKIP_LIVE_CONFIG_GUARD/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user