* feat(#3545): widen no-source-grep with one-hop path-fold and hooks dir Resolve a readFileSync() path argument that is a bare Identifier one hop back to its VariableDeclarator initializer before classification, and recognize `hooks` as a source directory alongside bin/lib/gsd-core/src. Measured (epic #3464 phase 7): fold+hooks together newly flag 76 unsuppressed sites across 18 files that were previously invisible to identifier-indirected or hooks/-rooted source reads. Neither widening alone is sufficient — hooks-only surfaces 0 new sites, confirming #3520's prior finding that the identifier-indirection gap must close first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(#3545): migrate 76 sites newly flagged by the fold+hooks widening Per-site classification: rewrite behaviorally (require() the real module, assert on its actual exported behavior) wherever the read was a proxy for code behavior; add a site-scoped `// allow-test-rule: <reason> (#3545)` marker only where the raw source text genuinely is the product under test (codex-config.test.cjs's adapter-header-contract checks, install.js structural-wiring guards with no exported symbol, AST-parse fixture inputs, etc.) — each marker cites an existing repo-sanctioned category from CONTRIBUTING.md's allow-test-rule exception table. Also converts two try/finally test bodies (introduced during this same migration) to the required t.after() cleanup pattern per CONTRIBUTING.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(#3545): re-baseline effective-exemption ceiling to 81 The fold+hooks widening's own newly-detected sites are now suppressed by site-scoped markers, moving them from invisible into the tightly-ratcheted effective-exemption count. Ceiling rises from 10 to 81 (the exact measured high-water mark, grace unchanged at 2) — a deliberate, measured re-baseline per the widening working as intended, not an ordinary ceiling bump. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3545): use canonical allow-test-rule category tokens 4 markers added during migration cited an issue ref correctly but didn't use one of CONTRIBUTING.md's seven recognized category tokens, unlike every other marker in this change. Cosmetic only — same suppression lines, same effective/live counts (81/81, 0 live). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(#3545): correct stale phase-artifact path in test comment Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,12 @@
|
||||
* call that originated the tracked value -- so annotating the read directly
|
||||
* (the intuitive placement) suppresses the violation just as well as
|
||||
* annotating the search call (adversarial-review fix, epic #3464 phase 4).
|
||||
*
|
||||
* A `readFileSync(...)` path argument that is a bare Identifier is resolved
|
||||
* ONE hop back to its `VariableDeclarator` initializer before classification,
|
||||
* so a path built once and passed by reference is recognized the same as an
|
||||
* inline path expression; `hooks` is also a recognized source directory
|
||||
* alongside `bin`/`lib`/`gsd-core`/`src` (#3545 / epic #3464 phase 7).
|
||||
*/
|
||||
|
||||
// How many derivation hops from the original readFileSync() binding to
|
||||
@@ -326,9 +332,27 @@ const rule = {
|
||||
return identifierVariableMap.get(identifierNode) || null;
|
||||
}
|
||||
|
||||
// WIDENING (fold, #3545/epic #3464 phase 7): resolve a bare Identifier
|
||||
// path argument back to its variable initializer, ONE hop, before
|
||||
// text-matching it -- so `const p = path.join(__dirname, '..', 'src',
|
||||
// 'x.cjs'); readFileSync(p)` is classified the same as an inline
|
||||
// `readFileSync(path.join(__dirname, '..', 'src', 'x.cjs'))`. Only
|
||||
// resolves a `VariableDeclarator`'s `init` (not an `AssignmentExpression`
|
||||
// — an assignment-bound identifier is a documented, deliberate miss, see
|
||||
// 40-design.md "Known limits"), and only ONE hop (a chain of two or more
|
||||
// indirections stays invisible -- also documented).
|
||||
function resolveOneHopText(node) {
|
||||
if (node.type !== 'Identifier') return sourceCode.getText(node);
|
||||
const v = resolveVariable(node);
|
||||
if (!v) return sourceCode.getText(node);
|
||||
const def = v.defs.find((d) => d.type === 'Variable' && d.node && d.node.init);
|
||||
if (!def) return sourceCode.getText(node);
|
||||
return sourceCode.getText(def.node.init);
|
||||
}
|
||||
|
||||
// Detect if a node represents a readFileSync call on a source file
|
||||
// (.cjs/.cts/.js/.mjs/.mts/.ts) that lives in a source directory
|
||||
// (bin, lib, gsd-core, src).
|
||||
// (bin, lib, gsd-core, hooks, src).
|
||||
function isSourceReadFileSync(node) {
|
||||
if (!node || node.type !== 'CallExpression') return false;
|
||||
|
||||
@@ -345,7 +369,7 @@ const rule = {
|
||||
if (!args || args.length === 0) return false;
|
||||
|
||||
const firstArg = args[0];
|
||||
const fullSrc = sourceCode.getText(firstArg);
|
||||
const fullSrc = resolveOneHopText(firstArg);
|
||||
|
||||
return looksLikeSourcePath(fullSrc);
|
||||
}
|
||||
@@ -359,8 +383,9 @@ const rule = {
|
||||
const hasSourceExt = /['"`.][^'"`.]*\.(?:cts|mts|mjs|cjs|js|ts)['"`)]/i.test(src);
|
||||
if (!hasSourceExt) return false;
|
||||
|
||||
// Must reference a source directory indicator somewhere in the expression
|
||||
const hasSourceDir = /['"](?:bin|lib|gsd-core|src)['"]/i.test(src);
|
||||
// Must reference a source directory indicator somewhere in the
|
||||
// expression (bin, lib, gsd-core, hooks, src).
|
||||
const hasSourceDir = /['"](?:bin|lib|gsd-core|hooks|src)['"]/i.test(src);
|
||||
return hasSourceDir;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"maxSites": 10,
|
||||
"maxSites": 81,
|
||||
"grace": 2
|
||||
}
|
||||
|
||||
@@ -55,6 +55,11 @@ describe('#1656: community .sh hooks must be present in hooks/dist', () => {
|
||||
describe('#1657 / #191: installer/package metadata retires sdk seam', () => {
|
||||
let src;
|
||||
test('install.js does not contain the legacy promptSdk() prompt (#1657)', () => {
|
||||
// allow-test-rule: structural-regression-guard (#3545) — regression guard
|
||||
// against literally reintroducing dead/removed code strings (promptSdk(),
|
||||
// --sdk/--no-sdk parsing, installSdkIfNeeded({) — none of them are live
|
||||
// exports or behavior to exercise via require(), so the source text
|
||||
// itself is the contract this test protects
|
||||
src = fs.readFileSync(INSTALL_SRC, 'utf-8');
|
||||
assert.ok(
|
||||
!src.includes('promptSdk('),
|
||||
|
||||
@@ -765,6 +765,10 @@ describe('--check drift detection', () => {
|
||||
// helpers the CLI uses, applied to in-memory strings — giving identical coverage
|
||||
// without touching the filesystem.
|
||||
const originalContent = fs.readFileSync(REGISTRY_PATH, 'utf8');
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — checkPipeline() below is a
|
||||
// raw-text diffing pipeline; this .replace() builds an in-memory tampered
|
||||
// TEXT fixture to drive that real pipeline call, not a text-grep proxy for
|
||||
// module behavior
|
||||
const tamperedContent = originalContent.replace(
|
||||
"version: '" + SCHEMA_VERSION + "'",
|
||||
"version: '0-stale'",
|
||||
@@ -790,6 +794,8 @@ describe('--check drift detection', () => {
|
||||
// Also verify the tampered content contains the stale marker (so the above
|
||||
// assertion is meaningful and not vacuously true due to other diff).
|
||||
assert.ok(
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — sanity check on the
|
||||
// same in-memory tampered TEXT fixture, not a proxy for module behavior
|
||||
tamperedContent.includes("version: '0-stale'"),
|
||||
'precondition: tampered content must contain the stale version marker',
|
||||
);
|
||||
|
||||
@@ -269,6 +269,9 @@ describe('bug #224: --pick stdout capture contract', () => {
|
||||
let src;
|
||||
|
||||
before(() => {
|
||||
// allow-test-rule: structural-implementation-guard (see #224) — locks the
|
||||
// main()-body byte-length seam contract structurally (no deterministic
|
||||
// Windows repro harness yet); see block header above.
|
||||
src = fs.readFileSync(GSD_TOOLS_SRC, 'utf-8');
|
||||
});
|
||||
|
||||
|
||||
@@ -3314,32 +3314,36 @@ test('writeNonClaudeDefaults is called before installCodexConfig in the Codex in
|
||||
const src = fs.readFileSync(INSTALL_JS, 'utf8');
|
||||
|
||||
// Find the call to writeNonClaudeDefaults that precedes installCodexConfig.
|
||||
const writeIdx = src.indexOf('writeNonClaudeDefaults(runtime);');
|
||||
const writeIdx = src.indexOf('writeNonClaudeDefaults(runtime);'); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
assert.ok(writeIdx !== -1, 'writeNonClaudeDefaults(runtime) must be called in the install flow');
|
||||
|
||||
// Find the FIRST installCodexConfig call AFTER the writeNonClaudeDefaults call.
|
||||
const codexGenIdx = src.indexOf('installCodexConfig(targetDir', writeIdx);
|
||||
const codexGenIdx = src.indexOf('installCodexConfig(targetDir', writeIdx); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
assert.ok(codexGenIdx !== -1 && codexGenIdx > writeIdx,
|
||||
'installCodexConfig must be called AFTER writeNonClaudeDefaults so defaults.json ' +
|
||||
'(resolve_model_ids + runtime) exists before agent TOML generation reads it (#2834)');
|
||||
|
||||
// The #2834 comment must be present at the call site.
|
||||
const callSite = src.slice(writeIdx - 300, writeIdx + 100);
|
||||
assert.ok(/#2834/.test(callSite), 'the writeNonClaudeDefaults call must carry the #2834 rationale comment');
|
||||
assert.ok(/#2834/.test(callSite), 'the writeNonClaudeDefaults call must carry the #2834 rationale comment'); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
});
|
||||
|
||||
test('writeNonClaudeDefaults function exists and is a no-op for Claude (#2834)', () => {
|
||||
const src = fs.readFileSync(INSTALL_JS, 'utf8');
|
||||
const fnIdx = src.indexOf('function writeNonClaudeDefaults(');
|
||||
const fnIdx = src.indexOf('function writeNonClaudeDefaults('); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
assert.ok(fnIdx !== -1, 'writeNonClaudeDefaults must be defined as a function');
|
||||
// Bound the slice by the next top-level declaration rather than a fixed
|
||||
// character count, so adding a comment or a guard inside the function cannot
|
||||
// push the asserted tokens out of the window and red this test spuriously.
|
||||
const nextFnIdx = src.indexOf('\nfunction ', fnIdx + 1);
|
||||
const nextFnIdx = src.indexOf('\nfunction ', fnIdx + 1); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
const fnBody = src.slice(fnIdx, nextFnIdx === -1 ? undefined : nextFnIdx);
|
||||
assert.ok(/nativeModelAliases/.test(fnBody), 'writeNonClaudeDefaults must early-return for Claude (nativeModelAliases check)');
|
||||
assert.ok(/resolve_model_ids/.test(fnBody), 'writeNonClaudeDefaults must write resolve_model_ids');
|
||||
assert.ok(/defaults\.runtime/.test(fnBody), 'writeNonClaudeDefaults must write runtime');
|
||||
// Source-text guard, not a behavioral call: writeNonClaudeDefaults() early-returns
|
||||
// as a no-op whenever process.env.GSD_TEST_MODE is set (see its own body), and this
|
||||
// suite sets GSD_TEST_MODE='1' file-wide (line 14), so invoking it here could never
|
||||
// observe the resolve_model_ids/runtime writes it is supposed to make (#2834).
|
||||
assert.ok(/nativeModelAliases/.test(fnBody), 'writeNonClaudeDefaults must early-return for Claude (nativeModelAliases check)'); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
assert.ok(/resolve_model_ids/.test(fnBody), 'writeNonClaudeDefaults must write resolve_model_ids'); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
assert.ok(/defaults\.runtime/.test(fnBody), 'writeNonClaudeDefaults must write runtime'); // allow-test-rule: structural-implementation-guard (#2834)
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6254,16 +6258,18 @@ const src = fs.readFileSync(INSTALL_JS, 'utf8');
|
||||
|
||||
describe('bug #279: Codex adapter documents Agent() and deferred tool discovery', () => {
|
||||
test('adapter mapping section includes explicit Agent(...) -> spawn_agent mapping', () => {
|
||||
// allow-test-rule: source-text-is-the-product [adapter header contract in bin/install.js] (see #279)
|
||||
assert.ok(
|
||||
/Task\(subagent_type="X", prompt="Y"\).*spawn_agent\(agent_type="X", message="Y"\)/.test(src) &&
|
||||
/Agent\(subagent_type="X", prompt="Y"\).*spawn_agent\(agent_type="X", message="Y"\)/.test(src),
|
||||
/Task\(subagent_type="X", prompt="Y"\).*spawn_agent\(agent_type="X", message="Y"\)/.test(src) && // allow-test-rule: source-text-is-the-product [adapter header contract in bin/install.js] (see #279)
|
||||
/Agent\(subagent_type="X", prompt="Y"\).*spawn_agent\(agent_type="X", message="Y"\)/.test(src), // allow-test-rule: source-text-is-the-product [adapter header contract in bin/install.js] (see #279)
|
||||
'Codex adapter must explicitly map both Task(...) and Agent(...) to spawn_agent',
|
||||
);
|
||||
});
|
||||
|
||||
test('adapter includes deferred tool_search discovery guidance before inline fallback', () => {
|
||||
// allow-test-rule: source-text-is-the-product [adapter header contract in bin/install.js] (see #279)
|
||||
assert.ok(
|
||||
src.includes('deferred') && src.includes('tool_search') && src.includes('spawn_agent'),
|
||||
src.includes('deferred') && src.includes('tool_search') && src.includes('spawn_agent'), // allow-test-rule: source-text-is-the-product [adapter header contract in bin/install.js] (see #279)
|
||||
'Codex adapter must instruct deferred tool discovery via tool_search before deciding to run inline',
|
||||
);
|
||||
});
|
||||
@@ -10089,9 +10095,10 @@ const src = fs.readFileSync(INSTALL_JS, 'utf8');
|
||||
// Helper: extract Section C from the raw source text.
|
||||
// Anchors on the heading and ends at </codex_skill_adapter>.
|
||||
function getSectionC() {
|
||||
const headingIdx = src.indexOf('## C. Task() → spawn_agent Mapping');
|
||||
// allow-test-rule: source-text-is-the-product (see #851)
|
||||
const headingIdx = src.indexOf('## C. Task() → spawn_agent Mapping'); // allow-test-rule: source-text-is-the-product (see #851)
|
||||
assert.ok(headingIdx >= 0, 'Section C heading must exist in bin/install.js');
|
||||
const closeTag = src.indexOf('</codex_skill_adapter>', headingIdx);
|
||||
const closeTag = src.indexOf('</codex_skill_adapter>', headingIdx); // allow-test-rule: source-text-is-the-product (see #851)
|
||||
assert.ok(closeTag >= 0, 'Section C must be followed by </codex_skill_adapter>');
|
||||
return src.slice(headingIdx, closeTag);
|
||||
}
|
||||
@@ -10281,8 +10288,9 @@ describe('bug #851: Codex adapter documents multi_agent_v1 schema limitation and
|
||||
// Regression guard: deferred tool discovery must remain (bug-279 contract).
|
||||
test('adapter deferred tool discovery instruction is preserved', () => {
|
||||
// The pre-existing bug-279 contract must remain intact
|
||||
// allow-test-rule: source-text-is-the-product (see #851)
|
||||
assert.ok(
|
||||
src.includes('deferred') && src.includes('tool_search') && src.includes('spawn_agent'),
|
||||
src.includes('deferred') && src.includes('tool_search') && src.includes('spawn_agent'), // allow-test-rule: source-text-is-the-product (see #851)
|
||||
'Adapter must still instruct deferred tool discovery via tool_search before deciding to run inline',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,17 +83,12 @@ describe('config-field-docs', () => {
|
||||
});
|
||||
|
||||
test('every CONFIG_DEFAULTS key appears in the doc', () => {
|
||||
// Extract CONFIG_DEFAULTS keys from config-loader.cjs source (moved from core.cjs by ADR-857 phase 2e)
|
||||
const coreSource = fs.readFileSync(CORE_PATH, 'utf-8');
|
||||
const defaultsMatch = coreSource.match(
|
||||
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own config-loader.cjs source, fixed-size author-controlled content
|
||||
/const CONFIG_DEFAULTS\s*=\s*\{([\s\S]*?)\r?\n\};/
|
||||
);
|
||||
assert.ok(defaultsMatch, 'Could not find CONFIG_DEFAULTS in config-loader.cjs');
|
||||
// Read CONFIG_DEFAULTS' actual keys straight from the module (moved from
|
||||
// core.cjs by ADR-857 phase 2e) instead of regex-parsing its source text.
|
||||
const { CONFIG_DEFAULTS } = require(CORE_PATH);
|
||||
assert.ok(CONFIG_DEFAULTS && typeof CONFIG_DEFAULTS === 'object', 'Could not find CONFIG_DEFAULTS export in config-loader.cjs');
|
||||
|
||||
const body = defaultsMatch[1];
|
||||
// Match property keys (word characters before the colon)
|
||||
const keys = [...body.matchAll(/^\s*(\w+)\s*:/gm)].map(m => m[1]);
|
||||
const keys = Object.keys(CONFIG_DEFAULTS);
|
||||
assert.ok(keys.length > 0, 'Could not extract any keys from CONFIG_DEFAULTS');
|
||||
|
||||
// CONFIG_DEFAULTS uses flat keys; the doc may use namespaced equivalents.
|
||||
|
||||
@@ -182,6 +182,9 @@ test('no `isAugment` occurrence gates a call to an Augment converter function (t
|
||||
const converterCallPattern = new RegExp(`(${converterNames.join('|')})\\s*\\(`);
|
||||
|
||||
const file = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// allow-test-rule: structural-regression-guard (#3545) — absence of an
|
||||
// isAugment-gated converter call is a source-text property (see file
|
||||
// header, #2097).
|
||||
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
||||
const offenders = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -200,6 +203,9 @@ test('no `isAugment` occurrence gates a call to an Augment converter function (t
|
||||
|
||||
test('legitimate isAugment destructure/enumeration sites survive (not eliminated, unlike isAntigravity)', () => {
|
||||
const file = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// allow-test-rule: structural-regression-guard (#3545) — presence of the
|
||||
// isAugment identifier itself is a source-text property (see file header,
|
||||
// #2097).
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
assert.ok(/isAugment/.test(src), 'isAugment must still be destructured from runtimeFlags() for non-conversion uses');
|
||||
});
|
||||
@@ -234,6 +240,9 @@ test('augment capability.json declares a real, converter-bearing agents kind for
|
||||
|
||||
test('_DESCRIPTOR_AGENTS_RUNTIMES no longer exists as a live declaration — the descriptor is authoritative for every runtime, not an allow-listed subset', () => {
|
||||
const file = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// allow-test-rule: structural-regression-guard (#3545) — absence of a live
|
||||
// _DESCRIPTOR_AGENTS_RUNTIMES declaration is a source-text property (see
|
||||
// file header, #2097).
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
// A residual mention in a historical // comment (documenting the #2875
|
||||
// deletion itself) is fine; a live `const _DESCRIPTOR_AGENTS_RUNTIMES =`
|
||||
|
||||
@@ -541,6 +541,161 @@ describe('no-source-grep rule — widening (#3502)', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// ─── fold + hooks widening (#3545 / Phase 7 of #3464) ─────────────────────
|
||||
//
|
||||
// One RuleTester case per row of
|
||||
// .gsd/phase/chore-3545-fold-hooks-widening-migration/50-test-matrix.md,
|
||||
// rows 1-8. Covers `fold` (a bare-Identifier readFileSync
|
||||
// path argument resolved ONE hop back to its VariableDeclarator init) and
|
||||
// `hooks` (now a recognized source directory alongside bin/lib/gsd-core/src).
|
||||
|
||||
test('#3545 row 1: baseline inline literal src path — unchanged by fold/hooks widening', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const s = fs.readFileSync(path.join(__dirname, '..', 'src', 'x.cjs'), 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noSourceGrep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 2: one-hop identifier bound to a src-dir path is now flagged (fold)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const p = path.join(__dirname, '..', 'src', 'x.cjs');
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noSourceGrep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 3: one-hop identifier bound to a hooks-dir path is now flagged (fold + hooks)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const p = path.join(__dirname, '..', 'hooks', 'x.cjs');
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
errors: [{ messageId: 'noSourceGrep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 4: identifier bound to a non-path value is not flagged (fold negative space)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const p = process.env.FOO;
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 5: identifier bound to a non-source-extension path is not flagged (fold negative space)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const p = path.join(__dirname, '..', 'src', 'x.md');
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 6: two-hop indirection is not flagged — fold only resolves one hop (boundary)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const a = path.join(__dirname, '..', 'src', 'x.cjs');
|
||||
const p = a;
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 7: assignment-bound path is not flagged — fold only resolves a VariableDeclarator init (boundary)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let p;
|
||||
p = path.join(__dirname, '..', 'src', 'x.cjs');
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('#3545 row 8: "hooks" as a substring of a longer quoted segment is not flagged (hasSourceDir requires an exact quoted segment)', () => {
|
||||
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const p = path.join(__dirname, '..', 'my-hooks-dir', 'x.cjs');
|
||||
const s = fs.readFileSync(p, 'utf-8');
|
||||
s.includes('y');
|
||||
`,
|
||||
filename: 'tests/foo.test.cjs',
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── no-source-grep site-scoped suppression (#3508 / Phase 4 of #3464) ──────
|
||||
|
||||
@@ -477,18 +477,15 @@ describe('Issue #815: --next dist-tag support', () => {
|
||||
* 4. Single-source: check-latest-version's PACKAGE_NAME === the seam's
|
||||
* packageName === the scoped '@opengsd/gsd-core'.
|
||||
*
|
||||
* Source-grep policy: this test reads hook source via readFileSync. The repo's
|
||||
* lint-no-source-grep rule targets bin/lib/gsd-core — hooks/ is out of
|
||||
* scope. The behavior (correct name → no E404) only manifests at runtime
|
||||
* Source-grep policy: this test reads hook source via readFileSync. Since
|
||||
* #3545 lint-no-source-grep also covers hooks/, not just bin/lib/gsd-core.
|
||||
* The behavior (correct name → no E404) only manifests at runtime
|
||||
* against the live registry; structural assertions are the minimum-cost
|
||||
* contract for the worker, the same rationale #378 carried.
|
||||
* contract for the worker, the same rationale #378 carried. See the
|
||||
* site-scoped `allow-test-rule` marker directly above the readFileSync()
|
||||
* call in workerCodeOnly() below.
|
||||
*/
|
||||
|
||||
// allow-test-rule: structural-regression-guard (see #378)
|
||||
// structural assertion on hook delegation; the behavior being
|
||||
// tested (correct package name → no E404) only manifests at runtime against the
|
||||
// live npm registry, which CI does not call.
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
@@ -502,6 +499,11 @@ const SEAM = require('../gsd-core/bin/lib/package-identity.cjs');
|
||||
const { PACKAGE_NAME } = require('../gsd-core/bin/check-latest-version.cjs');
|
||||
|
||||
function workerCodeOnly() {
|
||||
// allow-test-rule: structural-regression-guard (see #378) — the behavior
|
||||
// being tested (correct scoped package name → no E404) only manifests at
|
||||
// runtime against the live npm registry, which CI does not call; the
|
||||
// stripped-source text is the minimum-cost contract for this worker
|
||||
// (#3545 widening brings hooks/ into the rule's scope)
|
||||
const src = fs.readFileSync(WORKER_PATH, 'utf8');
|
||||
return src
|
||||
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks source, not adversarial input
|
||||
@@ -566,10 +568,11 @@ describe('bug #378 / #498: update worker queries the scoped name via the seam',
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:bug-2784-update-cache-clear-path (consolidation epic #1969 B5 #1974)", () => {
|
||||
// allow-test-rule: structural-regression-guard (see #2784)
|
||||
// Reads hook .js or bin/install.js source to assert structural invariants
|
||||
// (search array order, function wiring, path constants) that cannot be
|
||||
// verified by observing runtime outputs alone. Per CONTRIBUTING.md exception matrix.
|
||||
// verified by observing runtime outputs alone. Per CONTRIBUTING.md exception
|
||||
// matrix. See the site-scoped `allow-test-rule` marker directly above the
|
||||
// readFileSync() call below (#3545 widening brings hooks/ into scope).
|
||||
|
||||
/**
|
||||
* Regression test for bug #2784
|
||||
@@ -603,6 +606,9 @@ const CHECK_UPDATE_HOOK = path.join(REPO_ROOT, 'hooks', 'gsd-check-update.js');
|
||||
|
||||
describe('bug-2784: update.md cache-clear covers shared cache path', () => {
|
||||
test('gsd-check-update.js hook constructs cache dir from .cache and gsd path segments', () => {
|
||||
// allow-test-rule: structural-regression-guard (see #2784) — asserts
|
||||
// the path.join() segment structure that cannot be verified by
|
||||
// observing runtime outputs alone (#3545)
|
||||
const hookContent = fs.readFileSync(CHECK_UPDATE_HOOK, 'utf-8');
|
||||
// Parse the path.join() call structurally rather than text-grepping.
|
||||
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded hooks/gsd-check-update.js source, not adversarial input
|
||||
|
||||
@@ -533,6 +533,10 @@ describe('bug #1891: @file: resolution in gsd-tools.cjs', () => {
|
||||
let src;
|
||||
|
||||
before(() => {
|
||||
// allow-test-rule: structural-implementation-guard (see #1891) — gsd-tools.cjs's
|
||||
// stdout @file: interception has no exported symbol to assert on directly; every
|
||||
// src.includes()/indexOf()/match() call in this describe block traces back to this
|
||||
// read (#3545)
|
||||
src = fs.readFileSync(GSD_TOOLS_SRC, 'utf-8');
|
||||
});
|
||||
|
||||
|
||||
@@ -54,9 +54,15 @@ const HOOK = path.join(__dirname, '..', 'hooks', 'gsd-worktree-path-guard.js');
|
||||
|
||||
function loadNormalizer() {
|
||||
const src = fs.readFileSync(HOOK, 'utf8');
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — normalizeKimiPayload
|
||||
// is deliberately inlined per hook script with no require()-able module (see
|
||||
// file-header note above); text-index extraction is the only way to bind
|
||||
// and eval it, mirroring tests/kimi-guard-normalization-parity.test.cjs
|
||||
const start = src.indexOf('const KIMI_TOOL_NAMES');
|
||||
assert.notEqual(start, -1, 'KIMI_TOOL_NAMES block not found in hook source');
|
||||
const endMarker = ' return data;\n}';
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — same inlined-block
|
||||
// extraction as above, locating the end of the block to eval
|
||||
const end = src.indexOf(endMarker, start);
|
||||
assert.notEqual(end, -1, 'normalizeKimiPayload end not found in hook source');
|
||||
const block = src.slice(start, end + endMarker.length);
|
||||
|
||||
@@ -24,6 +24,7 @@ const REPO_ROOT = path.join(__dirname, '..');
|
||||
const INSTALL_BIN = path.join(REPO_ROOT, 'bin', 'install.js');
|
||||
const { cleanupLegacyGsdCc } = require(INSTALL_BIN);
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
|
||||
const LEGACY_PKG_SIGNAL = 'get-shit-done-cc';
|
||||
|
||||
@@ -103,17 +104,24 @@ describe('#3799: cleanupLegacyGsdCc honors an explicit configDirs scope', () =>
|
||||
});
|
||||
|
||||
describe('#3799: --no-legacy-cleanup and --config-dir CLI flags', () => {
|
||||
const HELP_TEXT = fs.readFileSync(INSTALL_BIN, 'utf-8');
|
||||
|
||||
test('the --no-legacy-cleanup flag exists and is documented in --help', () => {
|
||||
const result = runNode([INSTALL_BIN, '--help']);
|
||||
assert.equal(result.outcome, 'exited', `install.js --help did not exit cleanly: ${result.stderr}`);
|
||||
assert.ok(
|
||||
HELP_TEXT.includes('--no-legacy-cleanup'),
|
||||
'the escape hatch must be documented in the installer help text',
|
||||
result.stdout.includes('--no-legacy-cleanup'),
|
||||
'the escape hatch must be documented in the installer --help output',
|
||||
);
|
||||
});
|
||||
|
||||
test('the install() call site threads the config-dir scope and the skip flag', () => {
|
||||
const src = HELP_TEXT; // same file read — the shipped installer source
|
||||
// allow-test-rule: structural-implementation-guard (#3545) — structural assertion
|
||||
// on install()'s internal wiring (that its cleanupLegacyGsdCc call site threads configDirs/
|
||||
// skipNoLegacyCleanup) — install() is a ~2500-line, side-effect-heavy
|
||||
// top-level installer routine, so exercising this specific plumbing
|
||||
// behaviorally would require a full install() run; the source-slice
|
||||
// check is the minimum-cost regression guard for the exact #3799 defect
|
||||
// shape
|
||||
const src = fs.readFileSync(INSTALL_BIN, 'utf-8');
|
||||
// Slice the install() body up to its cleanup call so the conditional
|
||||
// spread's braces cannot defeat a single-regex match.
|
||||
const fnStart = src.indexOf('function install(isGlobal, runtime = DEFAULT_RUNTIME');
|
||||
|
||||
@@ -42,6 +42,9 @@ describe('orphaned hooks stale detection (#1750)', () => {
|
||||
});
|
||||
|
||||
test('gsd-check-update-worker.js imports managed-hooks-registry.cjs (not inline array)', () => {
|
||||
// allow-test-rule: structural-regression-guard (#3545) — presence of a
|
||||
// require() wire-up and absence of a reintroduced inline array are source
|
||||
// structure facts, not runtime behavior; see file header.
|
||||
const content = fs.readFileSync(WORKER_PATH, 'utf8');
|
||||
assert.ok(
|
||||
content.includes('managed-hooks-registry.cjs'),
|
||||
@@ -55,6 +58,9 @@ describe('orphaned hooks stale detection (#1750)', () => {
|
||||
});
|
||||
|
||||
test('gsd-check-update.js spawns the worker by file path (not inline -e code)', () => {
|
||||
// allow-test-rule: structural-regression-guard (#3545) — spawn-target
|
||||
// wiring and absence of inline `-e` code are source structure facts, not
|
||||
// runtime behavior; see file header.
|
||||
const content = fs.readFileSync(CHECK_UPDATE_PATH, 'utf8');
|
||||
assert.ok(
|
||||
content.includes('gsd-check-update-worker.js'),
|
||||
|
||||
@@ -367,6 +367,9 @@ describe('portability-vocab drift guard', () => {
|
||||
const srcPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const raw = fs.readFileSync(srcPath, 'utf8');
|
||||
// bin/install.js starts with a shebang espree cannot parse — rewrite #! -> //.
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — this is shebang-stripping
|
||||
// to make the source parseable by espree; the test then walks the real AST
|
||||
// structurally (below), it is not a text-grep proxy for behavior
|
||||
const src = raw.startsWith('#!') ? '//' + raw.slice(2) : raw;
|
||||
const ast = espree.parse(src, { ecmaVersion: 2022, loc: true, range: true, tolerant: true });
|
||||
|
||||
@@ -432,6 +435,9 @@ describe('portability-vocab drift guard', () => {
|
||||
test('bin/install.js: the curated INSTALL_JS_PATH_HELPERS still exist (no stale vocab entries after a rename)', () => {
|
||||
const srcPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const raw = fs.readFileSync(srcPath, 'utf8');
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — shebang-stripping to
|
||||
// make the source parseable by espree; the test walks the real AST
|
||||
// structurally (below), not a text-grep proxy for behavior
|
||||
const src = raw.startsWith('#!') ? '//' + raw.slice(2) : raw;
|
||||
const ast = espree.parse(src, { ecmaVersion: 2022, loc: true, range: true, tolerant: true });
|
||||
|
||||
|
||||
@@ -215,11 +215,12 @@ describe('gsd-read-guard hook', () => {
|
||||
});
|
||||
|
||||
test('hook is registered in install.js uninstall hook list', () => {
|
||||
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const content = fs.readFileSync(installPath, 'utf8');
|
||||
// Check the actual exported uninstall hook list instead of grepping
|
||||
// install.js source text.
|
||||
const { GSD_UNINSTALL_HOOKS } = require('../bin/install.js');
|
||||
assert.ok(
|
||||
content.includes("'gsd-read-guard.js'"),
|
||||
'gsd-read-guard.js must be in the uninstall gsdHooks list'
|
||||
GSD_UNINSTALL_HOOKS.includes('gsd-read-guard.js'),
|
||||
'gsd-read-guard.js must be in the uninstall GSD_UNINSTALL_HOOKS list'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -156,6 +156,10 @@ test('enhancement #191: published package no longer exposes gsd-sdk artifacts',
|
||||
});
|
||||
|
||||
test('enhancement #191: installer does not maintain gsd-sdk shim compatibility path', () => {
|
||||
// allow-test-rule: structural-regression-guard (#3545) — asserting the
|
||||
// ABSENCE of a retired flag/function reference from install.js is a source
|
||||
// structure fact (dead-code regression), not something observable by
|
||||
// exercising the installer's runtime behavior.
|
||||
const installJs = fs.readFileSync(INSTALL_PATH, 'utf8');
|
||||
|
||||
assert.equal(/\b--sdk\b/.test(installJs), false,
|
||||
|
||||
@@ -1845,10 +1845,10 @@ const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
// Require the module under test directly
|
||||
const roadmapLib = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'roadmap.cjs');
|
||||
const planScanLib = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'plan-scan.cjs');
|
||||
|
||||
// We test countPhasePlansAndSummaries indirectly via getManagerInfo since
|
||||
@@ -1893,24 +1893,29 @@ describe('bug #3128: roadmap.cjs plan-count for {N}-PLAN-{NN}-{slug}.md layout',
|
||||
assert.ok(!isPlanFile('5-RESEARCH.md'), 'RESEARCH.md must not match');
|
||||
});
|
||||
|
||||
test('roadmap.cjs source uses the extended isPlanFile filter', () => {
|
||||
const roadmapSrc = fs.readFileSync(roadmapLib, 'utf8');
|
||||
// Verify the fix is in place: the old simple inline filter is gone from roadmap.cjs
|
||||
test('roadmap.cjs source uses the extended isPlanFile filter', (t) => {
|
||||
// roadmap.cjs's countPhasePlansAndSummaries (module-private) delegates its
|
||||
// plan counting to plan-scan.cjs's scanPhasePlans/isRootPlanFile -- exercise
|
||||
// the REAL exported module directly instead of grepping roadmap.cjs's source
|
||||
// text for the delegation.
|
||||
const planScan = require(planScanLib);
|
||||
|
||||
// isRootPlanFile must recognize the {N}-PLAN-{NN}-{slug}.md layout (#3128)
|
||||
// that the old inline `f.endsWith('-PLAN.md') || f === 'PLAN.md'` filter missed.
|
||||
assert.ok(
|
||||
!roadmapSrc.includes("phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md')"),
|
||||
'Old simple plan filter still present in roadmap.cjs — fix not applied',
|
||||
);
|
||||
// roadmap.cjs now delegates to plan-scan.cjs via require('./plan-scan.cjs')
|
||||
assert.ok(
|
||||
roadmapSrc.includes('plan-scan.cjs'),
|
||||
'roadmap.cjs does not require plan-scan.cjs — delegation not applied',
|
||||
);
|
||||
// plan-scan.cjs is where the extended plan-file detection logic lives (isRootPlanFile)
|
||||
const planScanSrc = fs.readFileSync(planScanLib, 'utf8');
|
||||
assert.ok(
|
||||
planScanSrc.includes('isRootPlanFile') && planScanSrc.includes('/PLAN/i'),
|
||||
'isRootPlanFile with /PLAN/i not found in plan-scan.cjs — canonical helper missing extended filter',
|
||||
planScan.isRootPlanFile('5-PLAN-01-setup-database.md'),
|
||||
'isRootPlanFile must recognize the slug-form plan filename from bug #3128',
|
||||
);
|
||||
|
||||
// Exercise scanPhasePlans against a synthetic phase directory containing
|
||||
// only a slug-form plan file -- this is the SAME production function
|
||||
// roadmap.cjs's countPhasePlansAndSummaries calls, so a correct count here
|
||||
// proves the extended filter is what actually runs, not a copy of it.
|
||||
const tmpDir = createTempDir('roadmap-plan-scan-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
fs.writeFileSync(path.join(tmpDir, '5-PLAN-01-setup-database.md'), '# plan\n');
|
||||
const scanResult = planScan(tmpDir);
|
||||
assert.equal(scanResult.planCount, 1, 'scanPhasePlans must count the slug-form plan file');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,6 +130,11 @@ describe('#3024 review finding 2: LEGACY_NON_REGISTRY_RUNTIME_IDS drift guard',
|
||||
assert.ok(
|
||||
!registryIds.includes(SENTINEL_ID) &&
|
||||
!legacyIds.includes(SENTINEL_ID) &&
|
||||
// allow-test-rule: source-text-is-the-product (#3545) — sourceParsedIds
|
||||
// is enumeration scaffolding derived from readFileSync'd source text
|
||||
// (see parseHardcodedBranchIds doc comment above); every id it turns
|
||||
// up is verified BEHAVIORALLY below via getGlobalConfigDir, never
|
||||
// trusted on its own
|
||||
!sourceParsedIds.includes(SENTINEL_ID),
|
||||
`sentinel id ${SENTINEL_ID} unexpectedly collides with a real candidate id — pick a different sentinel`,
|
||||
);
|
||||
|
||||
@@ -147,22 +147,35 @@ describe('stripJsonComments (#1461)', () => {
|
||||
});
|
||||
|
||||
describe('readSettings null return on malformed files (#1461)', () => {
|
||||
test('install.js contains JSONC stripping in readSettings', () => {
|
||||
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const content = fs.readFileSync(installPath, 'utf8');
|
||||
assert.ok(content.includes('stripJsonComments'),
|
||||
'install.js should use stripJsonComments in readSettings');
|
||||
test('readSettings strips JSONC comments when reading a real file', (t) => {
|
||||
const tmpFile = path.join(os.tmpdir(), `gsd-settings-test-jsonc-${process.pid}.json`);
|
||||
fs.writeFileSync(tmpFile, `{\n // a comment\n "key": "value"\n}`);
|
||||
t.after(() => fs.unlinkSync(tmpFile));
|
||||
const result = readSettings(tmpFile);
|
||||
assert.deepStrictEqual(
|
||||
result,
|
||||
{ key: 'value' },
|
||||
'readSettings should use stripJsonComments so a commented file parses, not warns-as-malformed'
|
||||
);
|
||||
});
|
||||
|
||||
test('readSettings returns null on truly malformed files (not empty object)', () => {
|
||||
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const content = fs.readFileSync(installPath, 'utf8');
|
||||
assert.ok(content.includes('return null'),
|
||||
'readSettings should return null on parse failure, not empty object');
|
||||
test('readSettings returns null on truly malformed files (not empty object)', (t) => {
|
||||
const tmpFile = path.join(os.tmpdir(), `gsd-settings-test-malformed-return-${process.pid}.json`);
|
||||
fs.writeFileSync(tmpFile, '{ this is not valid json');
|
||||
t.after(() => fs.unlinkSync(tmpFile));
|
||||
const result = readSettings(tmpFile);
|
||||
assert.strictEqual(result, null, 'readSettings should return null on parse failure, not empty object');
|
||||
});
|
||||
|
||||
test('callers guard against null readSettings return', () => {
|
||||
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// allow-test-rule: structural-implementation-guard (#1461) (#3545) — structural
|
||||
// assertion on internal wiring inside install()'s (~2500-line)
|
||||
// settings-configuration call sites — the
|
||||
// null-guard only manifests behaviorally deep inside a full install()
|
||||
// run, so the source-text check is the minimum-cost regression guard
|
||||
// that a caller was not added without also checking readSettings'
|
||||
// documented null return
|
||||
const content = fs.readFileSync(installPath, 'utf8');
|
||||
// Should have null guards at the settings configuration call sites
|
||||
assert.ok(
|
||||
@@ -180,13 +193,16 @@ describe('readSettings null return on malformed files (#1461)', () => {
|
||||
// the behavioural ones beneath it.
|
||||
|
||||
describe('readSettings: JSON null coalesced to empty, malformed warns (#1191)', () => {
|
||||
test('source contains the null-coalescing guard (parsed === null ? {})', () => {
|
||||
// Structural anchor: if someone removes the coalescing, this test catches it
|
||||
// before the behavioural test below even runs.
|
||||
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
const content = fs.readFileSync(installPath, 'utf8');
|
||||
assert.ok(
|
||||
content.includes('parsed === null ? {}'),
|
||||
test('valid JSON null coalesces to {} via the real function (early behavioral anchor)', (t) => {
|
||||
// Behavioral anchor: if someone removes the coalescing, this test catches
|
||||
// it before the more detailed behavioural test below even runs.
|
||||
const tmpFile = path.join(os.tmpdir(), `gsd-settings-test-null-anchor-${process.pid}.json`);
|
||||
fs.writeFileSync(tmpFile, 'null');
|
||||
t.after(() => fs.unlinkSync(tmpFile));
|
||||
const result = readSettings(tmpFile);
|
||||
assert.deepStrictEqual(
|
||||
result,
|
||||
{},
|
||||
'install.js readSettings must coalesce valid JSON null to {} (not malformed warning)'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5490,6 +5490,10 @@ describe('install.js guard for gsd-worktree-path-guard.js', () => {
|
||||
before(() => {
|
||||
// ADR-857 phase 5f-1b: hook registration moved to runtime-hooks-surface.cts.
|
||||
// Concatenate both sources so structural assertions find patterns in either file.
|
||||
// allow-test-rule: structural-implementation-guard (#3545) — structural install.js
|
||||
// guard; install.js has side effects on require and no exported symbol for hook-registration wiring;
|
||||
// every src.includes()/indexOf() and block.includes() call below traces
|
||||
// back to this read
|
||||
const installSrc = fs.readFileSync(INSTALL_SRC, 'utf-8');
|
||||
let hooksSurfaceSrc = '';
|
||||
try { hooksSurfaceSrc = fs.readFileSync(HOOKS_SURFACE_SRC, 'utf-8'); } catch { /* ok */ }
|
||||
|
||||
Reference in New Issue
Block a user