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.
162 lines
6.1 KiB
JavaScript
162 lines
6.1 KiB
JavaScript
'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 WARNING/);
|
|
assert.match(out, /\/live\/\.claude\/gsd-core/);
|
|
assert.match(out, /scrubConfigLocationEnv/);
|
|
assert.match(out, /GSD_SKIP_LIVE_CONFIG_GUARD/);
|
|
assert.match(out, /GSD_STRICT_LIVE_CONFIG_GUARD/);
|
|
});
|
|
});
|