resolveLiveConfigRoots resolves what THIS process sees, and getGlobalConfigDir is env-first -- so with an ambient CLAUDE_CONFIG_DIR the guard watched that path. A spawned child does not see it: TEST_ENV_BASE blanks the config-location vars precisely so the child cannot follow them, and a blanked var is falsy, so the child resolves its HOME-derived root instead. A child that blanks the var and does NOT also sandbox HOME therefore writes into the developer's real ~/.claude, which the guard was not watching. That is this PR's own escape route, taken one process deeper -- and the guard is the artifact that is supposed to make it loud. Both resolutions are now unioned: the ambient one, and the fallback one obtained by handing the REAL descriptor resolver an EMPTY env. Deriving it that way is deliberate -- a hand-listed copy of the scrub set inside the guard is a second list to drift, which is the defect this PR spent three rounds closing one layer up. grok resolves through a hardcoded branch rather than a descriptor, so its fallback is stated explicitly for the same reason it is named in the ambient loop. Addresses review finding: Blocker 3.
549 lines
22 KiB
JavaScript
549 lines
22 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 fc = require('fast-check');
|
|
|
|
const {
|
|
GSD_OWNED_ENTRIES,
|
|
MAX_DEPTH,
|
|
resolveLiveConfigRoots,
|
|
resolveExtraWatchTargets,
|
|
snapshotLiveConfig,
|
|
diffLiveConfig,
|
|
formatViolations,
|
|
newestMtime,
|
|
} = require('../scripts/live-config-guard.cjs');
|
|
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
function tmpRoot() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'live-config-guard-'));
|
|
}
|
|
|
|
/** Create `n` flat files under a fresh dir; returns [dir, entryCount-including-dir]. */
|
|
function treeWithEntries(n) {
|
|
const dir = tmpRoot();
|
|
for (let i = 0; i < n; i++) fs.writeFileSync(path.join(dir, `f${i}`), 'x');
|
|
return [dir, n + 1]; // +1: the directory itself is lstat'd and costs budget
|
|
}
|
|
|
|
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('watches the HOME-derived fallback root, not only the ambient one', () => {
|
|
// #2665 round 5: the guard resolves env-first, so it sees the AMBIENT root.
|
|
// A child that blanks CLAUDE_CONFIG_DIR (which is exactly what TEST_ENV_BASE
|
|
// does) falls back to <HOME>/.claude instead. Watching only the ambient path
|
|
// leaves that fallback unwatched -- the escape route this PR closes, one
|
|
// process deeper.
|
|
const ambient = tmpRoot();
|
|
const fakeHome = tmpRoot();
|
|
const saved = process.env.CLAUDE_CONFIG_DIR;
|
|
try {
|
|
process.env.CLAUDE_CONFIG_DIR = ambient;
|
|
const roots = resolveLiveConfigRoots({ os: { homedir: () => fakeHome } });
|
|
assert.ok(
|
|
roots.includes(path.resolve(ambient)),
|
|
`ambient root missing from ${JSON.stringify(roots)}`,
|
|
);
|
|
assert.ok(
|
|
roots.includes(path.resolve(path.join(fakeHome, '.claude'))),
|
|
`HOME-derived fallback root missing from ${JSON.stringify(roots)}`,
|
|
);
|
|
} finally {
|
|
if (saved === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
else process.env.CLAUDE_CONFIG_DIR = saved;
|
|
cleanup(ambient);
|
|
cleanup(fakeHome);
|
|
}
|
|
});
|
|
|
|
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('detects a DELETED top-level GSD entry', () => {
|
|
const root = tmpRoot();
|
|
try {
|
|
// A fixed owned entry is recorded at BOTH ends whether or not it exists,
|
|
// so a deletion reads {exists:true} -> {exists:false}. Before the union
|
|
// walk that pair matched no branch at all and the run passed silently.
|
|
fs.mkdirSync(path.join(root, 'gsd-core'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'gsd-core', 'x'), 'x');
|
|
const before = snapshotLiveConfig([root]);
|
|
fs.rmSync(path.join(root, 'gsd-core'), { recursive: true, force: true });
|
|
|
|
const violations = diffLiveConfig(before, snapshotLiveConfig([root]));
|
|
const deleted = violations.filter((v) => v.kind === 'deleted');
|
|
assert.strictEqual(deleted.length, 1, JSON.stringify(violations));
|
|
assert.strictEqual(path.basename(deleted[0].path), 'gsd-core');
|
|
} finally {
|
|
cleanup(root);
|
|
}
|
|
});
|
|
|
|
test('detects a DELETED gsd-prefixed child of a shared dir', () => {
|
|
const root = tmpRoot();
|
|
try {
|
|
// The shape a `pre.exists && !post.exists` branch cannot reach on its own:
|
|
// prefixed children are DISCOVERED by readdir, so a deleted one is absent
|
|
// from the `after` snapshot entirely and never enters an after-keyed loop.
|
|
fs.mkdirSync(path.join(root, 'skills', 'gsd-dev-preferences'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'skills', 'gsd-dev-preferences', 'SKILL.md'), '# x');
|
|
const before = snapshotLiveConfig([root]);
|
|
fs.rmSync(path.join(root, 'skills', 'gsd-dev-preferences'), { recursive: true, force: true });
|
|
|
|
const violations = diffLiveConfig(before, snapshotLiveConfig([root]));
|
|
const deleted = violations.filter((v) => v.kind === 'deleted');
|
|
assert.strictEqual(deleted.length, 1, JSON.stringify(violations));
|
|
assert.strictEqual(path.basename(deleted[0].path), 'gsd-dev-preferences');
|
|
} 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/);
|
|
});
|
|
});
|
|
|
|
// ── Round 3: the write surfaces that are not runtime config ROOTS ───────────
|
|
describe('#2665: guard watches non-root write surfaces', () => {
|
|
test('resolveExtraWatchTargets covers $GSD_HOME/.gsd and kimi config.toml', () => {
|
|
const home = tmpRoot();
|
|
const share = tmpRoot();
|
|
try {
|
|
const targets = resolveExtraWatchTargets({
|
|
env: { GSD_HOME: home, KIMI_SHARE_DIR: share },
|
|
os: { homedir: () => home },
|
|
});
|
|
assert.ok(
|
|
targets.includes(path.resolve(path.join(home, '.gsd'))),
|
|
`expected $GSD_HOME/.gsd in ${JSON.stringify(targets)}`,
|
|
);
|
|
assert.ok(
|
|
targets.some((t) => t === path.resolve(path.join(share, 'config.toml'))),
|
|
`expected kimi config.toml in ${JSON.stringify(targets)}`,
|
|
);
|
|
} finally {
|
|
cleanup(home);
|
|
cleanup(share);
|
|
}
|
|
});
|
|
|
|
test('extra targets are DERIVED from the descriptor array, not a named resolver', () => {
|
|
const {
|
|
NON_REGISTRY_CONFIG_HOME_DESCRIPTORS,
|
|
resolveConfigHomeFromDescriptor,
|
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
const home = tmpRoot();
|
|
try {
|
|
const env = { GSD_HOME: home };
|
|
const targets = resolveExtraWatchTargets({ env, os: { homedir: () => home } });
|
|
|
|
// Every descriptor in the array must contribute a target. Calling one
|
|
// named resolver instead would cover one of today's two entries and silently
|
|
// miss tomorrow's — the same partial-enumeration defect that put
|
|
// KIMI_SHARE_DIR outside the scrub set, one layer over.
|
|
//
|
|
// SCOPE BOUNDARY (per round-2 Nit 7, and it bites here): this asserts one
|
|
// target PER DESCRIPTOR and nothing about whether one target per descriptor
|
|
// is ENOUGH. It is not — <root>/hooks/ is also GSD-written and unwatched
|
|
// (named residual in resolveExtraWatchTargets). A test whose expectation is
|
|
// derived from the same array it checks cannot see that class.
|
|
for (const d of NON_REGISTRY_CONFIG_HOME_DESCRIPTORS) {
|
|
const dir = resolveConfigHomeFromDescriptor(d, { env, home });
|
|
assert.ok(
|
|
targets.includes(path.resolve(path.join(dir, 'config.toml'))),
|
|
`descriptor ${JSON.stringify(d.env)} contributed no watch target`,
|
|
);
|
|
}
|
|
// The count is what actually catches a regression to a hardcoded call:
|
|
// it fails the moment the array grows and the guard does not follow.
|
|
assert.strictEqual(
|
|
targets.length,
|
|
1 + NON_REGISTRY_CONFIG_HOME_DESCRIPTORS.length,
|
|
'expected the GSD store root plus exactly one target per descriptor',
|
|
);
|
|
} finally {
|
|
cleanup(home);
|
|
}
|
|
});
|
|
|
|
test('#2755: kimi-code config.toml is watched — named, not enumeration-relative', () => {
|
|
// The test above derives its expectation FROM the descriptor array, so it
|
|
// passes for whatever that array happens to contain and cannot see a
|
|
// descriptor that was never added — the enumeration-relative scope boundary
|
|
// this suite already calls out one layer down. #2755 landed kimi-code's
|
|
// `~/.kimi-code` (KIMI_CODE_HOME) on `next` as an inline literal inside
|
|
// resolveKimiHooksTomlDir's body; until it was hoisted into
|
|
// NON_REGISTRY_CONFIG_HOME_DESCRIPTORS the guard watched Kimi CLI's
|
|
// config.toml and not Kimi Code's. Naming the path is what makes dropping
|
|
// the descriptor fail loudly instead of quietly shrinking the expectation.
|
|
const home = tmpRoot();
|
|
const codeHome = tmpRoot();
|
|
try {
|
|
const env = { GSD_HOME: home, KIMI_CODE_HOME: codeHome };
|
|
const targets = resolveExtraWatchTargets({ env, os: { homedir: () => home } });
|
|
assert.ok(
|
|
targets.includes(path.resolve(path.join(codeHome, 'config.toml'))),
|
|
`expected kimi-code config.toml in ${JSON.stringify(targets)}`,
|
|
);
|
|
} finally {
|
|
cleanup(home);
|
|
cleanup(codeHome);
|
|
}
|
|
});
|
|
|
|
test('GSD_HOME falls back to homedir when unset', () => {
|
|
const home = tmpRoot();
|
|
try {
|
|
const targets = resolveExtraWatchTargets({ env: {}, os: { homedir: () => home } });
|
|
assert.ok(targets.includes(path.resolve(path.join(home, '.gsd'))));
|
|
} finally {
|
|
cleanup(home);
|
|
}
|
|
});
|
|
|
|
test('detects a consent/defaults write into $GSD_HOME/.gsd', () => {
|
|
const home = tmpRoot();
|
|
try {
|
|
const target = path.join(home, '.gsd');
|
|
const before = snapshotLiveConfig([], [target]);
|
|
// The Blocker-1 shape one family over: an ambient GSD_HOME sends real
|
|
// consent records and defaults.json into the developer's own store.
|
|
fs.mkdirSync(target, { recursive: true });
|
|
fs.writeFileSync(path.join(target, 'consent.json'), '{}');
|
|
|
|
const violations = diffLiveConfig(before, snapshotLiveConfig([], [target]));
|
|
assert.strictEqual(violations.length, 1);
|
|
assert.strictEqual(violations[0].kind, 'created');
|
|
} finally {
|
|
cleanup(home);
|
|
}
|
|
});
|
|
|
|
test('detects a [[hooks]] write into kimi config.toml', () => {
|
|
const share = tmpRoot();
|
|
try {
|
|
const target = path.join(share, 'config.toml');
|
|
const before = snapshotLiveConfig([], [target]);
|
|
fs.writeFileSync(target, '[[hooks]]\n');
|
|
|
|
const violations = diffLiveConfig(before, snapshotLiveConfig([], [target]));
|
|
assert.strictEqual(violations.length, 1);
|
|
assert.strictEqual(violations[0].kind, 'created');
|
|
} finally {
|
|
cleanup(share);
|
|
}
|
|
});
|
|
|
|
test('NEGATIVE CONTROL: without the extras both leaks are silent', () => {
|
|
const home = tmpRoot();
|
|
try {
|
|
// This is the pre-round-3 guard shape — roots only. It is what let a leak
|
|
// on either variable pass through the PR's own safety net unreported.
|
|
const before = snapshotLiveConfig([]);
|
|
fs.mkdirSync(path.join(home, '.gsd'), { recursive: true });
|
|
fs.writeFileSync(path.join(home, '.gsd', 'consent.json'), '{}');
|
|
|
|
assert.deepStrictEqual(diffLiveConfig(before, snapshotLiveConfig([])), []);
|
|
} finally {
|
|
cleanup(home);
|
|
}
|
|
});
|
|
|
|
test('a whole-dir extra target does not watch unrelated siblings', () => {
|
|
const home = tmpRoot();
|
|
try {
|
|
const target = path.join(home, '.gsd');
|
|
fs.mkdirSync(target, { recursive: true });
|
|
const before = snapshotLiveConfig([], [target]);
|
|
// A sibling of .gsd is outside the watched target entirely.
|
|
fs.writeFileSync(path.join(home, 'unrelated.json'), '{}');
|
|
|
|
assert.deepStrictEqual(diffLiveConfig(before, snapshotLiveConfig([], [target])), []);
|
|
} finally {
|
|
cleanup(home);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Round 3: the truncation budget — the module's own safety-critical case ───
|
|
describe('#2665: scan-budget truncation', () => {
|
|
test('boundary: limit-1 truncates, limit and limit+1 do not', () => {
|
|
const [dir, entries] = treeWithEntries(24);
|
|
try {
|
|
// RULESET.TESTS.boundary-coverage: N in {limit-1, limit, limit+1}. The
|
|
// budget is injected, so the boundary is exercised at a real threshold
|
|
// without materialising MAX_ENTRIES files.
|
|
assert.strictEqual(
|
|
newestMtime(dir, { remaining: entries - 1 }).truncated,
|
|
true,
|
|
'one entry short of the tree size MUST truncate',
|
|
);
|
|
assert.strictEqual(
|
|
newestMtime(dir, { remaining: entries }).truncated,
|
|
false,
|
|
'a budget exactly equal to the tree size must NOT truncate',
|
|
);
|
|
assert.strictEqual(
|
|
newestMtime(dir, { remaining: entries + 1 }).truncated,
|
|
false,
|
|
'a budget above the tree size must NOT truncate',
|
|
);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('exceeding MAX_DEPTH truncates', () => {
|
|
const dir = tmpRoot();
|
|
try {
|
|
let deep = dir;
|
|
for (let i = 0; i <= MAX_DEPTH + 1; i++) deep = path.join(deep, `d${i}`);
|
|
fs.mkdirSync(deep, { recursive: true });
|
|
|
|
const res = newestMtime(dir, { remaining: 1e6 });
|
|
assert.strictEqual(res.truncated, true, 'a tree deeper than MAX_DEPTH must truncate');
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('a truncated scan reports UNVERIFIED, never clean', () => {
|
|
const [dir, entries] = treeWithEntries(10);
|
|
try {
|
|
// The safety-critical branch named in this module's own docstring: a scan
|
|
// that hit a bound must not read as an attestation of cleanliness.
|
|
const snap = { [dir]: { exists: true, newest: 1, truncated: true } };
|
|
const violations = diffLiveConfig(snap, {
|
|
[dir]: { exists: true, newest: 1, truncated: true },
|
|
});
|
|
assert.strictEqual(violations.length, 1);
|
|
assert.strictEqual(violations[0].kind, 'unverified');
|
|
assert.match(formatViolations(violations), /UNVERIFIED \(scan bound hit/);
|
|
assert.ok(entries > 0);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('a modified path outranks unverified (a real leak is never downgraded)', () => {
|
|
const p = '/live/.claude/gsd-core';
|
|
const violations = diffLiveConfig(
|
|
{ [p]: { exists: true, newest: 1, truncated: true } },
|
|
{ [p]: { exists: true, newest: 2, truncated: true } },
|
|
);
|
|
assert.strictEqual(violations[0].kind, 'modified');
|
|
});
|
|
|
|
test('property: truncation is monotone in the budget (boundary containment)', () => {
|
|
const [dir, entries] = treeWithEntries(12);
|
|
try {
|
|
fc.assert(
|
|
fc.property(fc.integer({ min: 1, max: entries * 3 }), (budget) => {
|
|
const { truncated } = newestMtime(dir, { remaining: budget });
|
|
// The invariant: a budget at or above the tree size never truncates,
|
|
// and one below it always does. A regression flipping `truncated` to
|
|
// false on an exhausted budget — the exact silent-clean failure the
|
|
// module warns about — breaks this for every budget < entries.
|
|
return budget >= entries ? truncated === false : truncated === true;
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('property: newest mtime never exceeds the true maximum', () => {
|
|
const [dir, entries] = treeWithEntries(8);
|
|
try {
|
|
const trueMax = Math.max(
|
|
...fs.readdirSync(dir).map((f) => fs.lstatSync(path.join(dir, f)).mtimeMs),
|
|
fs.lstatSync(dir).mtimeMs,
|
|
);
|
|
fc.assert(
|
|
fc.property(fc.integer({ min: 1, max: entries * 2 }), (budget) => {
|
|
const { newest } = newestMtime(dir, { remaining: budget });
|
|
return newest <= trueMax;
|
|
}),
|
|
{ numRuns: 50 },
|
|
);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('#2665 round 4: CI wires the guard to strict mode', () => {
|
|
// The reversion this guards: dropping GSD_STRICT_LIVE_CONFIG_GUARD from
|
|
// test.yml silently demotes the guard back to report-only, and a future
|
|
// leak of exactly the class #2665 closes prints a warning and CI stays
|
|
// green. Windows lanes are deliberately report-only until the documented
|
|
// pre-existing USERPROFILE leak class is swept (SEVERITY note in
|
|
// scripts/live-config-guard.cjs) — so the assertion is per-OS, not global.
|
|
test('all three test jobs set GSD_STRICT_LIVE_CONFIG_GUARD (Windows carved out)', () => {
|
|
const yaml = require('js-yaml');
|
|
const wf = yaml.load(
|
|
fs.readFileSync(
|
|
path.join(__dirname, '..', '.github', 'workflows', 'test.yml'),
|
|
'utf8',
|
|
),
|
|
);
|
|
|
|
for (const job of ['test', 'test-full']) {
|
|
const v = String(wf.jobs?.[job]?.env?.GSD_STRICT_LIVE_CONFIG_GUARD ?? '');
|
|
assert.match(
|
|
v,
|
|
// The WHOLE expression, anchored — a prefix match accepted both
|
|
// `&& '1' || '1'` (Windows silently strict) and a malformed tail
|
|
// (found by this round's pre-push adversarial review).
|
|
/^\$\{\{\s*matrix\.os\s*!=\s*'windows-latest'\s*&&\s*'1'\s*\|\|\s*''\s*\}\}$/,
|
|
`jobs.${job}.env.GSD_STRICT_LIVE_CONFIG_GUARD must be strict on ` +
|
|
`non-Windows lanes and empty on windows-latest; got: ${JSON.stringify(v)}`,
|
|
);
|
|
}
|
|
|
|
assert.strictEqual(
|
|
String(wf.jobs?.['test-inert']?.env?.GSD_STRICT_LIVE_CONFIG_GUARD ?? ''),
|
|
'1',
|
|
'jobs.test-inert (ubuntu-only) must set GSD_STRICT_LIVE_CONFIG_GUARD: 1',
|
|
);
|
|
});
|
|
});
|