Files
msd-core/tests/shadow-report.test.cjs
sim 147856040b fix(#2873): close review findings across fences, sanitizer and docs
Isolated security review found resolveSpecRootReference's fence tracker
toggled on any delimiter, so a backtick fence could be closed by a tilde
one and an include in the gap was rewritten inside a code block. Fixed by
reusing scanFencedBlocks - the canonical engine already behind
stripFencedCode and extractFencedBlock - rather than carrying a fourth
copy of fence detection, which also closes the duplication the standards
review flagged.

sanitizeForRender now strips combining marks and zero-width characters
alongside the ANSI, control and bidi classes it already handled.

Adds the C, E and F matrix rows the spec review found missing, including
installer-level coverage that spawns the real install rather than calling
the report builder. Ships the how-to, the reference and command docs in
five locales, the changeset, the inventory and glossary entries, and
regenerates health.md for the new W028 rule.

Refs #2873
2026-08-14 23:48:39 -04:00

900 lines
44 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* tests/shadow-report.test.cjs — pure IR unit suite for
* `install-shadow-report.cts`'s `buildShadowReport` (#2873, epic #2866 Phase
* 4a — governed by `.gsd/phase/feat-2873-cross-scope-shadowing/40-design.md`).
*
* Implements matrix section A (`buildShadowReport()`, rows A1-A24) and
* properties F1/F4 from `.gsd/phase/feat-2873-cross-scope-shadowing/50-test-matrix.md`.
* The matrix's own "Suites" section names this file `install-shadow-report
* .test.cjs`; it is shipped as `shadow-report.test.cjs` instead so its
* `lint-test-file-count.cjs` prefix is `shadow` (0 files before this PR, at
* the 2-file cap after it) rather than colliding with the already
* grandfathered, already-at-cap `install` prefix bucket.
*
* A21-A24 cover the per-scope truth filter (#2873 Task 1): a `full`-profile
* global install alongside a `core`-profile local install must never report
* the profile-only stems as shadowed local artifacts that do not exist on
* disk. F1 is updated in lockstep — its expected shadowed set is now the
* INTERSECTION of the two scopes' stems, not their union.
*
* F4 ("4b transform is idempotent over arbitrary bodies") targets
* `resolveSpecRootReference` (`runtime-artifact-conversion.cts`, #2873 Phase
* 4b), which has since landed — see the "F4" describe block below.
*
* Fixture strategy mirrors `tests/installed-surface-resolver.test.cjs`
* (`buildShadowReport` forwards its `opts` verbatim to
* `resolveInstalledSurfaces`): an injectable `readManifest` keyed by the
* REAL `configHome` `resolveScope` computes for a given runtime/scope, never
* a hand-typed path literal.
*/
const { test, describe } = 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 fc = require('./helpers/fast-check-setup.cjs');
const {
buildShadowReport,
renderShadowReport,
sanitizeForRender,
SHADOW_REASON,
} = require('../gsd-core/bin/lib/install-shadow-report.cjs');
const { resolveScope } = require('../gsd-core/bin/lib/install-scope.cjs');
const { MANIFEST_NAME } = require('../gsd-core/bin/lib/installer-migrations.cjs');
const { resolveSpecRootReference } = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
// ─── Fixture helpers (mirrors tests/installed-surface-resolver.test.cjs) ───
const ABSENT_MANIFEST = Object.freeze({ manifestVersion: null, runtime: null, scope: null, files: {} });
function manifest({ manifestVersion = null, runtime = null, scope = null, files = {} } = {}) {
return { manifestVersion, runtime, scope, files };
}
function mkReadManifest(byConfigHome) {
return (configDir) => byConfigHome.get(configDir) ?? ABSENT_MANIFEST;
}
function scopeHomes(runtime, home, cwd) {
const base = { runtime, env: {}, home, existsSync: () => false, cwd };
return {
global: resolveScope({ ...base, id: 'global' }).configHome,
local: resolveScope({ ...base, id: 'local' }).configHome,
};
}
function baseOpts(home, cwd, overrides = {}) {
return { home, cwd, env: {}, existsSync: () => false, ...overrides };
}
/** `commands/gsd/*.md` stems shipped by the real repo — used so A1's "71
* entries" tracks the real roster instead of a hardcoded, driftable count. */
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const REAL_STEMS = fs.readdirSync(REAL_COMMANDS_DIR)
.filter((f) => f.endsWith('.md'))
.map((f) => f.slice(0, -3))
.sort();
function skillFilesFor(stems) {
const files = {};
for (const s of stems) files[`skills/gsd-${s}/SKILL.md`] = 'x';
return files;
}
function commandFilesFor(stems) {
const files = {};
for (const s of stems) files[`commands/gsd-${s}.md`] = 'x';
return files;
}
/** Build a claude coexistence fixture: `stems` installed as global skills AND
* local commands (so every one of them is a shadowed trigger). */
function coexistenceOpts(home, cwd, stems, overrides = {}) {
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(stems) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(stems) })],
]);
return baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome), ...overrides });
}
// ─── A1-A6 — shape happy/negative paths ────────────────────────────────────
describe('buildShadowReport — shape (A1-A6)', () => {
test('reports shadowing for a claude coexistence, full real roster (A1)', () => {
const home = '/fixture/a1-home';
const cwd = '/fixture/a1-cwd';
const report = buildShadowReport('claude', coexistenceOpts(home, cwd, REAL_STEMS));
assert.strictEqual(report.shadowed, true);
assert.strictEqual(report.reason, SHADOW_REASON.SCOPE_SHADOWED);
assert.strictEqual(report.triggers.length, REAL_STEMS.length);
assert.deepStrictEqual(report.winner, { kind: 'skills', scope: 'global' });
assert.deepStrictEqual(report.shadowedSide, { kind: 'commands', scope: 'local' });
assert.deepStrictEqual(report.triggers.map((t) => t.trigger).sort(), REAL_STEMS.map((s) => `gsd-${s}`));
});
test('no report for a single scope, global only (A2)', () => {
const home = '/fixture/a2-home';
const cwd = '/fixture/a2-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['plan-phase']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
assert.strictEqual(report.reason, SHADOW_REASON.NOT_SHADOWED);
assert.deepStrictEqual(report.triggers, []);
});
test('no report for local-only (A3)', () => {
const home = '/fixture/a3-home';
const cwd = '/fixture/a3-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['plan-phase']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
assert.deepStrictEqual(report.triggers, []);
});
test('same-kind shadowing is reported as override, not a vanished tree (A4)', () => {
const home = '/fixture/a4-home';
const cwd = '/fixture/a4-cwd';
const homes = scopeHomes('cursor', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'cursor', scope: 'global', files: skillFilesFor(['plan-phase']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'cursor', scope: 'local', files: skillFilesFor(['plan-phase']) })],
]);
const report = buildShadowReport('cursor', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, true);
assert.strictEqual(report.kindsDiffer, false);
assert.deepStrictEqual(report.winner, { kind: 'skills', scope: 'global' });
assert.deepStrictEqual(report.shadowedSide, { kind: 'skills', scope: 'local' });
});
test('windsurf asymmetry does not collide (A5)', () => {
const home = '/fixture/a5-home';
const cwd = '/fixture/a5-cwd';
const homes = scopeHomes('windsurf', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'windsurf', scope: 'global', files: { 'agents/gsd-planner.md': 'a' } })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'windsurf', scope: 'local', files: { 'workflows/gsd-plan-phase.md': 'a' } })],
]);
const report = buildShadowReport('windsurf', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
});
test('same config home is not self-shadowing (A6)', () => {
const shared = '/fixture/a6-shared-home';
const homes = scopeHomes('claude', shared, shared);
assert.strictEqual(homes.global, homes.local, 'fixture assumption: both scopes collapse to one configHome');
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['plan-phase']) })],
]);
const report = buildShadowReport('claude', baseOpts(shared, shared, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
});
});
// ─── A7-A10 — SAMPLE_LIMIT boundary (limit-1, limit, limit+1) ──────────────
describe('buildShadowReport — sample-limit boundary (A7-A10)', () => {
test('zero triggers renders nothing (A7, limit-1 in the sense of "below any sample")', () => {
const home = '/fixture/a7-home';
const cwd = '/fixture/a7-cwd';
const homes = scopeHomes('claude', home, cwd);
// Both scopes installed (manifestVersion set) but with an empty `files`
// map each — `deriveStemsFromManifest` short-circuits to `[]` for an
// empty `files` BEFORE resolving a layout at all (installed-surface-
// resolver.cts's C13), so the stem union across both scopes is empty and
// no trigger is ever synthesized. NOT a disjoint-stems fixture: because
// `resolveInstalledSurfaces` unions stems across every INSTALLED scope
// (not per-scope), two scopes installed with genuinely DIFFERENT,
// non-empty stem sets still produce a shadowed entry for each stem in
// the union — see A12's comment for the same mechanism.
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: {} })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: {} })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
assert.deepStrictEqual(report.triggers, []);
assert.deepStrictEqual(renderShadowReport(report), []);
});
test('single trigger has no overflow tail (A8, limit=1)', () => {
const home = '/fixture/a8-home';
const cwd = '/fixture/a8-cwd';
const report = buildShadowReport('claude', coexistenceOpts(home, cwd, ['solo']));
assert.strictEqual(report.triggers.length, 1);
const lines = renderShadowReport(report);
// header + exactly one sample line, no "...and N more" tail, no mismatch notes.
assert.strictEqual(lines.length, 2);
});
test('sample limit exactly, 5 shadowed (A9)', () => {
const home = '/fixture/a9-home';
const cwd = '/fixture/a9-cwd';
const stems = ['s1', 's2', 's3', 's4', 's5'];
const report = buildShadowReport('claude', coexistenceOpts(home, cwd, stems));
assert.strictEqual(report.triggers.length, 5);
const lines = renderShadowReport(report);
// header + 5 samples, still no tail.
assert.strictEqual(lines.length, 6);
});
test('sample limit plus one, 6 shadowed (A10)', () => {
const home = '/fixture/a10-home';
const cwd = '/fixture/a10-cwd';
const stems = ['s1', 's2', 's3', 's4', 's5', 's6'];
const report = buildShadowReport('claude', coexistenceOpts(home, cwd, stems));
assert.strictEqual(report.triggers.length, 6);
const lines = renderShadowReport(report);
// header + 5 samples + one overflow-tail line.
assert.strictEqual(lines.length, 7);
});
});
// ─── A11-A13 — manifest content edge cases ─────────────────────────────────
describe('buildShadowReport — manifest content edge cases (A11-A13)', () => {
test('v1 manifest still reports shadowing, identical to v2, no reinstall signal in the IR (A11)', () => {
const home = '/fixture/a11-home';
const cwd = '/fixture/a11-cwd';
const homesV1 = scopeHomes('claude', home, cwd);
const byConfigHomeV1 = new Map([
[homesV1.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['plan-phase']) })],
// v1: no manifestVersion key at all, normalized to 1; no declared runtime/scope.
[homesV1.local, manifest({ manifestVersion: 1, runtime: null, scope: null, files: commandFilesFor(['plan-phase']) })],
]);
const reportV1 = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHomeV1) }));
const byConfigHomeV2 = new Map([
[homesV1.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['plan-phase']) })],
[homesV1.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['plan-phase']) })],
]);
const reportV2 = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHomeV2) }));
assert.strictEqual(reportV1.shadowed, true);
assert.deepStrictEqual(reportV1, reportV2, 'a v1-backed report must be structurally identical to its v2 counterpart');
// No reinstall/version signal anywhere in the IR's shape.
assert.ok(!('manifestVersion' in reportV1));
for (const trig of reportV1.triggers) assert.ok(!('manifestVersion' in trig));
for (const m of reportV1.mismatches) assert.ok(!('manifestVersion' in m));
});
test('empty manifest yields no triggers (A12)', () => {
const home = '/fixture/a12-home';
const cwd = '/fixture/a12-cwd';
const homes = scopeHomes('claude', home, cwd);
// Deliberately only ONE scope present, with an empty `files` map — the
// clean exercise of the empty-files short-circuit (deriveStemsFromManifest's
// C13) in isolation. A COEXISTENCE fixture (both scopes installed, one
// side's `files: {}`) does NOT stay `shadowed: false`: because
// `resolveInstalledSurfaces` unions stems across every scope it counts as
// installed (manifestVersion set, regardless of that scope's own file
// count) rather than per-scope, a real stem contributed by the OTHER,
// populated scope still gets a synthesized trigger at this empty one —
// see `installed-surface-resolver.cts`'s `stemUnion` computation. That is
// established, already-tested Phase 3 (#2872) behavior (the roster is
// assumed uniform across installed scopes), not something this row
// exercises.
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: {} })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
assert.deepStrictEqual(report.triggers, []);
});
test('unreadable manifest degrades, never throws (A13)', () => {
const home = '/fixture/a13-home';
const cwd = '/fixture/a13-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['plan-phase']) })],
]);
const readManifest = (configDir) => {
if (configDir === homes.local) throw new Error('EACCES: permission denied');
return byConfigHome.get(configDir) ?? ABSENT_MANIFEST;
};
let report;
assert.doesNotThrow(() => {
report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest }));
});
assert.strictEqual(report.shadowed, false);
});
});
// ─── A14-A15 — declared-runtime/scope mismatch surfaced, not corrected ─────
describe('buildShadowReport — mismatches are reported, never corrected (A14-A15)', () => {
test('declared runtime mismatch is surfaced (A14)', () => {
const home = '/fixture/a14-home';
const cwd = '/fixture/a14-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'cursor', scope: 'global', files: skillFilesFor(['plan-phase']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['plan-phase']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.mismatches.length, 1);
assert.strictEqual(report.mismatches[0].scope, 'global');
assert.strictEqual(report.mismatches[0].declaredRuntime, 'cursor');
assert.strictEqual(report.mismatches[0].declaredRuntimeMatchesProbe, false);
});
test('declared scope mismatch is surfaced (A15)', () => {
const home = '/fixture/a15-home';
const cwd = '/fixture/a15-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: skillFilesFor(['plan-phase']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['plan-phase']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.mismatches.length, 1);
assert.strictEqual(report.mismatches[0].scope, 'global');
assert.strictEqual(report.mismatches[0].declaredScope, 'local');
assert.strictEqual(report.mismatches[0].declaredScopeMatchesProbe, false);
});
});
// ─── A16-A17 — malformed runtime degrades, never propagates ───────────────
describe('buildShadowReport — non-installable / unknown runtime degrades (A16-A17)', () => {
test('non-installable runtime degrades to no report (A16, vscode)', () => {
const home = '/fixture/a16-home';
const cwd = '/fixture/a16-cwd';
let report;
assert.doesNotThrow(() => {
report = buildShadowReport('vscode', baseOpts(home, cwd, { readManifest: mkReadManifest(new Map()) }));
});
assert.strictEqual(report.reason, SHADOW_REASON.RESOLVER_UNAVAILABLE);
assert.strictEqual(report.shadowed, false);
assert.deepStrictEqual(renderShadowReport(report), []);
});
test('unknown runtime degrades to no report (A17)', () => {
const home = '/fixture/a17-home';
const cwd = '/fixture/a17-cwd';
let report;
assert.doesNotThrow(() => {
report = buildShadowReport('not-a-real-runtime-xyz', baseOpts(home, cwd, { readManifest: mkReadManifest(new Map()) }));
});
assert.strictEqual(report.reason, SHADOW_REASON.RESOLVER_UNAVAILABLE);
assert.strictEqual(report.shadowed, false);
});
});
// ─── A18 — caller mutation cannot corrupt a later call ─────────────────────
describe('buildShadowReport — independence across calls (A18)', () => {
test('report is not shared across calls', () => {
const home = '/fixture/a18-home';
const cwd = '/fixture/a18-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'cursor', scope: 'global', files: skillFilesFor(['plan-phase']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['plan-phase']) })],
]);
const opts = baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) });
const first = buildShadowReport('claude', opts);
const pristine = JSON.parse(JSON.stringify(first));
first.winner.kind = 'HACKED';
first.triggers[0].trigger = 'HACKED';
first.triggers.push({ trigger: 'INJECTED' });
first.mismatches[0].declaredRuntime = 'HACKED';
first.mismatches.push({ scope: 'INJECTED' });
const second = buildShadowReport('claude', opts);
assert.deepStrictEqual(second, pristine, 'a second call must be unaffected by mutation of the first result');
});
});
// ─── A19 — the production call shape ───────────────────────────────────────
describe('buildShadowReport — production call shape (A19)', () => {
test('production call shape resolves, matches the injected-dep rows\' shape', (t) => {
const home = createTempDir('gsd-shadow-a19-home-');
const cwd = createTempDir('gsd-shadow-a19-cwd-');
t.after(() => { cleanup(home); cleanup(cwd); });
const globalDir = path.join(home, '.claude');
const localDir = path.join(cwd, '.claude');
fs.mkdirSync(globalDir, { recursive: true });
fs.mkdirSync(localDir, { recursive: true });
fs.writeFileSync(path.join(globalDir, MANIFEST_NAME), JSON.stringify({
manifestVersion: 2, runtime: 'claude', scope: 'global',
files: skillFilesFor(['plan-phase']),
}));
fs.writeFileSync(path.join(localDir, MANIFEST_NAME), JSON.stringify({
manifestVersion: 2, runtime: 'claude', scope: 'local',
files: commandFilesFor(['plan-phase']),
}));
let report;
assert.doesNotThrow(() => {
// The exact production call shape: no injected registry, no injected
// readManifest — real fs, real capability registry.
report = buildShadowReport('claude', { home, cwd });
});
assert.strictEqual(report.shadowed, true);
assert.deepStrictEqual(report.winner, { kind: 'skills', scope: 'global' });
assert.deepStrictEqual(report.shadowedSide, { kind: 'commands', scope: 'local' });
assert.strictEqual(report.triggers.length, 1);
assert.deepStrictEqual(
Object.keys(report).sort(),
['kindsDiffer', 'mismatches', 'reason', 'runtime', 'shadowed', 'shadowedSide', 'triggers', 'winner'],
);
});
});
// ─── A20 — frozen reason-code enum key set is locked ───────────────────────
describe('SHADOW_REASON (A20)', () => {
test('reason enum key set is locked', () => {
assert.deepStrictEqual(
Object.keys(SHADOW_REASON).sort(),
['NOT_SHADOWED', 'RESOLVER_UNAVAILABLE', 'SCOPE_SHADOWED'],
);
});
test('is frozen', () => {
assert.strictEqual(Object.isFrozen(SHADOW_REASON), true);
});
});
// ─── A21-A24 — per-scope truth filter (cross-scope stem-union false positive) ─
describe('buildShadowReport — per-scope truth filter (A21-A24)', () => {
test('both scopes carry the same stems: every shadowed trigger reported (A21)', () => {
const home = '/fixture/a21-home';
const cwd = '/fixture/a21-cwd';
const stems = ['plan-phase', 'milestone-complete', 'phase-create'];
const report = buildShadowReport('claude', coexistenceOpts(home, cwd, stems));
assert.strictEqual(report.shadowed, true);
assert.deepStrictEqual(report.triggers.map((t) => t.trigger).sort(), stems.map((s) => `gsd-${s}`).sort());
});
test('global strict superset of local (full vs core profile): only the intersection is reported (A22)', () => {
const home = '/fixture/a22-home';
const cwd = '/fixture/a22-cwd';
const homes = scopeHomes('claude', home, cwd);
// global = 'full' profile (a, b, c) — local = 'core' profile (a only).
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['a', 'b', 'c']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['a']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, true);
// Only 'a' is a REAL local artifact — 'b' and 'c' must never be reported
// as shadowed local commands; there is no local artifact for either.
assert.deepStrictEqual(report.triggers.map((t) => t.trigger), ['gsd-a']);
});
test('local has a stem global does not: not reported as shadowed (A23)', () => {
const home = '/fixture/a23-home';
const cwd = '/fixture/a23-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['a']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['a', 'z']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, true);
// 'z' exists ONLY at local (no global artifact "wins" it) — must not
// appear in the shadowed set at all.
assert.deepStrictEqual(report.triggers.map((t) => t.trigger), ['gsd-a']);
});
test('disjoint stem sets: shadowed is false (A24)', () => {
const home = '/fixture/a24-home';
const cwd = '/fixture/a24-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(['a', 'b']) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(['x', 'y']) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
assert.strictEqual(report.shadowed, false);
assert.deepStrictEqual(report.triggers, []);
});
});
// ─── F1 — bijective property: every trigger has exactly one winner ────────
describe('buildShadowReport — property (F1)', () => {
test('every trigger has exactly one winner', () => {
const stemArb = fc.stringMatching(/^[a-z0-9]{1,6}(-[a-z0-9]{1,6}){0,2}$/);
const setArb = fc.uniqueArray(stemArb, { maxLength: 6 });
fc.assert(
fc.property(setArb, setArb, (globalStems, localStems) => {
const home = '/fixture/f1-home';
const cwd = '/fixture/f1-cwd';
const homes = scopeHomes('claude', home, cwd);
const byConfigHome = new Map([
[homes.global, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'global', files: skillFilesFor(globalStems) })],
[homes.local, manifest({ manifestVersion: 2, runtime: 'claude', scope: 'local', files: commandFilesFor(localStems) })],
]);
const report = buildShadowReport('claude', baseOpts(home, cwd, { readManifest: mkReadManifest(byConfigHome) }));
// Both scopes are always "installed" here (manifestVersion set
// regardless of file-list length), so `resolveOneRuntime`'s
// `stemUnion` still synthesizes a candidate trigger for every stem
// observed at EITHER scope. `buildShadowReport`'s per-scope truth
// filter (#2873 Task 1 — see install-shadow-report.cts's module
// comment) then narrows that down to the INTERSECTION: a trigger is
// only reported when a real artifact exists at BOTH scopes.
const expectedShadowed = globalStems
.filter((s) => localStems.includes(s))
.map((s) => `gsd-${s}`)
.sort();
const actualShadowed = report.triggers.map((t) => t.trigger).sort();
assert.deepStrictEqual(actualShadowed, expectedShadowed);
// Bijection: each shadowed trigger names exactly one winner (kind,scope).
for (const trig of report.triggers) {
assert.strictEqual(trig.winnerKind, 'skills');
assert.strictEqual(trig.winnerScope, 'global');
assert.strictEqual(trig.shadowedKind, 'commands');
assert.strictEqual(trig.shadowedScope, 'local');
}
// No trigger name appears twice in the shadowed set.
assert.strictEqual(new Set(actualShadowed).size, actualShadowed.length);
}),
// Explicit seed + bounded numRuns (CONTRIBUTING: unseeded property
// tests are a review blocker). On failure, fast-check's thrown error
// carries the pinned seed and the shrunk counterexample needed to
// replay deterministically.
{ seed: 20260814, numRuns: 50 },
);
});
});
// ─── F2/F3 — sanitizeForRender properties (idempotence, stripped-class-free) ─
describe('sanitizeForRender — properties (F2, F3)', () => {
// Explicit seed + bounded numRuns (CONTRIBUTING: unseeded property tests
// are a review blocker), matching F1/F4's seed above.
const SEED = 20260814;
const NUM_RUNS = 300;
// Hostile-input generator: ANSI CSI/OSC escape sequences, C0 controls
// (including bare \x00 and a lone unterminated \x1b), DEL/C1, Unicode bidi
// embedding/override + isolate controls, combining marks ("zalgo",
// U+0300-U+036F), zero-width characters (ZWSP/ZWNJ/ZWJ/BOM), astral-plane
// characters (surrogate-pair-backed — real emoji/supplementary-plane text,
// not just printable ASCII), CRLF/LF/CR newlines, and plain text —
// interleaved so a single generated string usually mixes several hostile
// classes at once, per the brief's "not just printable ASCII, or the
// properties are vacuous" requirement.
//
// #2873 PR review Finding 2 (MINOR): `sanitizeForRender` originally
// stripped ANSI/control/bidi only, missing combining marks and zero-width
// characters — neither is a JS `\s`, so both survived the 64-char cap and
// the whitespace-collapse step undetected. `combiningArb`/`zeroWidthArb`
// and the extended `STRIPPED_CLASS_RE` below close that generator gap.
const c0ControlArb = fc.integer({ min: 0x00, max: 0x1f }).map((c) => String.fromCharCode(c));
const delC1Arb = fc.integer({ min: 0x7f, max: 0x9f }).map((c) => String.fromCharCode(c));
const ansiCsiArb = fc.constantFrom('\x1b[31m', '\x1b[0m', '\x1b[2K', '\x1b[1;37;40m');
const ansiOscArb = fc.constantFrom('\x1b]0;title\x07', '\x1b]8;;http://example\x1b\\');
const bidiArb = fc.constantFrom(
'‪', '‫', '‬', '‭', '‮', // embedding/override
'⁦', '⁧', '⁨', '⁩', // isolates
);
const combiningArb = fc.constantFrom('\u{0300}', '\u{0301}', '\u{0302}', '\u{036F}');
const zeroWidthArb = fc.constantFrom('\u{200B}', '\u{200C}', '\u{200D}', '\u{FEFF}');
const astralArb = fc.constantFrom('\u{1F600}', '\u{1F4A9}', '\u{10000}', '\u{1F469}\u{200D}\u{1F4BB}');
const newlineArb = fc.constantFrom('\n', '\r\n', '\r');
const plainArb = fc.string({ maxLength: 12 });
const hostileChunkArb = fc.oneof(
c0ControlArb, delC1Arb, ansiCsiArb, ansiOscArb, bidiArb, combiningArb, zeroWidthArb, astralArb, newlineArb, plainArb,
);
const hostileStringArb = fc.array(hostileChunkArb, { maxLength: 10 }).map((parts) => parts.join(''));
// The stripped classes sanitizeForRender documents (ANSI escapes, C0
// controls + DEL/C1, Unicode bidi override/isolate, combining marks,
// zero-width characters), checked with an INDEPENDENT regex here rather
// than re-requiring the module's private ANSI_RE/CONTROL_RE/BIDI_RE/
// COMBINING_MARK_RE/ZERO_WIDTH_RE — so F3 is a real invariant check
// against the module's documented contract, not a tautology against its
// own internals.
// eslint-disable-next-line no-control-regex, no-misleading-character-class
const STRIPPED_CLASS_RE = /[\x00-\x1f\x7f-\x9f‪-‮⁦-⁩\u{0300}-\u{036F}\u{200B}-\u{200D}\u{FEFF}]/u;
test('sanitizer is idempotent: s(s(x)) === s(x) for arbitrary strings (F2)', () => {
let changedCount = 0;
fc.assert(
fc.property(hostileStringArb, (input) => {
const once = sanitizeForRender(input);
if (once !== input) changedCount += 1;
const twice = sanitizeForRender(once);
assert.strictEqual(
twice,
once,
`not idempotent — input: ${JSON.stringify(input)}\nonce: ${JSON.stringify(once)}\ntwice: ${JSON.stringify(twice)}`,
);
}),
{ seed: SEED, numRuns: NUM_RUNS },
);
// Non-vacuity (mirrors F4's real-shape-generator rationale above): prove
// the generator actually produced input sanitizeForRender changed at
// least once, or this property would pass trivially over inert strings.
assert.ok(changedCount > 0, 'generator never produced a string sanitizeForRender actually changed — F2 would be vacuous');
});
test('sanitizer output never contains a stripped-class character, for arbitrary input (F3)', () => {
let hostileInputCount = 0;
fc.assert(
fc.property(hostileStringArb, (input) => {
if (STRIPPED_CLASS_RE.test(input)) hostileInputCount += 1;
const output = sanitizeForRender(input);
assert.ok(
output === null || !STRIPPED_CLASS_RE.test(output),
`stripped-class character survived sanitization — input: ${JSON.stringify(input)}\noutput: ${JSON.stringify(output)}`,
);
}),
{ seed: SEED, numRuns: NUM_RUNS },
);
// Non-vacuity: prove the generator actually exercised at least one
// stripped-class character, or F3 would hold trivially over clean input.
assert.ok(hostileInputCount > 0, 'generator never produced a stripped-class character — F3 would be vacuous');
});
});
// ─── E1-E12 — resolveSpecRootReference direct unit coverage ───────────────
// Matrix section E (`.gsd/phase/feat-2873-cross-scope-shadowing/50-test-matrix.md`).
// Only the E13/E14 installed-output integration pair (in
// tests/install-runtime-artifacts.test.cjs) and F4's idempotence property
// (above) touched this exported function before this block — these rows
// exercise it DIRECTLY, one behavior at a time.
describe('resolveSpecRootReference — direct unit coverage (E1-E12)', () => {
test('global skill body: the include is replaced by the two-step imperative form naming both candidates (E1)', () => {
const body = '@~/.claude/gsd-core/workflows/plan-phase.md';
const result = resolveSpecRootReference(body);
assert.notStrictEqual(result, body);
assert.ok(!result.startsWith('@'), 'the static @-include must be gone');
assert.ok(
result.includes('.claude/gsd-core/workflows/plan-phase.md') && result.includes('~/.claude/gsd-core/workflows/plan-phase.md'),
`expected both the project-local and global candidate paths named in: ${JSON.stringify(result)}`,
);
});
test('local command body: byte-identical to today (E2)', () => {
// The REAL literal a local claude install emits for this same source
// line (verified empirically against a real --local install): the
// installer's path-prefix rewrite resolves the local scope's absolute
// config dir, never `~`, so this never has the `@~/.claude/` shape
// WORKFLOW_SPEC_ROOT_INCLUDE_RE requires in the first place — a genuine
// non-qualifying condition, not a hand-waved one.
const body = '@/Users/dev/myrepo/.claude/gsd-core/workflows/plan-phase.md';
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('every non-claude runtime, both scopes: byte-identical to today (E3)', () => {
// Real literal shapes emitted for other runtimes (verified empirically
// against a real --cursor --global install): no `.claude/` segment at
// all, so none of them ever match the claude-only spec-root regex.
const cursorGlobal = '@$HOME/gsd-core/workflows/plan-phase.md';
const genericLocal = '@./gsd-core/workflows/plan-phase.md';
assert.strictEqual(resolveSpecRootReference(cursorGlobal), cursorGlobal);
assert.strictEqual(resolveSpecRootReference(genericLocal), genericLocal);
});
test('a references/ include is a different spec root and stays static (E4)', () => {
const body = '@~/.claude/gsd-core/references/ui-brand.md';
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('an @.planning/… include is untouched (E5)', () => {
const body = '@.planning/notes.md';
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('an include inside a fenced code block is untouched, byte-identical (E6)', () => {
const body = ['```', '@~/.claude/gsd-core/workflows/plan-phase.md', '```'].join('\n');
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('an include mentioned in inline backticks is untouched, byte-identical (E7)', () => {
const body = 'See `@~/.claude/gsd-core/workflows/plan-phase.md` for the spec.';
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('a body with no workflow include is a no-op (E8)', () => {
const body = 'Just some ordinary command prose with no includes at all.';
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('two independent workflow includes both resolve (E9)', () => {
const body = [
'@~/.claude/gsd-core/workflows/plan-phase.md',
'@~/.claude/gsd-core/workflows/execute-phase.md',
].join('\n');
const result = resolveSpecRootReference(body);
assert.ok(!result.includes('@~/.claude/gsd-core/workflows/plan-phase.md'));
assert.ok(!result.includes('@~/.claude/gsd-core/workflows/execute-phase.md'));
assert.ok(result.includes('.claude/gsd-core/workflows/plan-phase.md'));
assert.ok(result.includes('.claude/gsd-core/workflows/execute-phase.md'));
});
test('prose merely mentioning gsd-core/workflows/x.md is untouched (E10)', () => {
const body = 'See gsd-core/workflows/plan-phase.md for background on how this works.';
assert.strictEqual(resolveSpecRootReference(body), body);
});
test('a CRLF body emits identically to LF, no orphaned \\r (E11)', () => {
const bodyLf = '@~/.claude/gsd-core/workflows/plan-phase.md\nSecond line.';
const bodyCrlf = '@~/.claude/gsd-core/workflows/plan-phase.md\r\nSecond line.';
const resultLf = resolveSpecRootReference(bodyLf);
const resultCrlf = resolveSpecRootReference(bodyCrlf);
assert.strictEqual(resultCrlf, resultLf.replace('\n', '\r\n'));
// Every `\r` in the result must be immediately followed by `\n` — an
// orphaned CR (one not paired with the LF that owns it) would mean the
// transform dropped or duplicated a line-ending byte.
assert.ok(!/\r(?!\n)/.test(resultCrlf), `orphaned CR found in: ${JSON.stringify(resultCrlf)}`);
});
test('applying the transform twice over an installed tree is idempotent (E12)', () => {
const body = 'intro\n@~/.claude/gsd-core/workflows/plan-phase.md\noutro';
const once = resolveSpecRootReference(body);
const twice = resolveSpecRootReference(once);
assert.strictEqual(twice, once);
});
});
// ─── E-rows — resolveSpecRootReference fence-detection regressions ────────
// (#2873 PR review Finding 1, MEDIUM): the hand-rolled `FENCE_DELIMITER_RE`
// tracker toggled open/closed on ANY delimiter line regardless of type,
// which is wrong under CommonMark (a closer must share the opener's
// delimiter character and have run length >= the opener's). The fix reuses
// `scanFencedBlocks` (`markdown-sectionizer.cts`). These cases pin the exact
// failure the review constructed plus the sibling CommonMark edge cases
// named in the review (nested fences, an unterminated fence, and a
// longer-run opener closed by a too-short run).
describe('resolveSpecRootReference — unit (fence detection, #2873 review Finding 1)', () => {
test('mismatched fence types (``` opened, ~~~ inside, ``` closes) leave BOTH includes untouched', () => {
const body = [
'```',
'@~/.claude/gsd-core/workflows/alpha.md',
'~~~',
'@~/.claude/gsd-core/workflows/beta.md',
'```',
].join('\n');
const result = resolveSpecRootReference(body);
assert.strictEqual(result, body, 'a ``` fence is not closed by a ~~~ line — both includes must stay inside the one open block');
assert.ok(!result.includes('To load this command'), 'no rewrite marker should appear when both includes are fenced');
});
test('nested fences (outer run longer than an inner same-char run) leave the enclosed include untouched', () => {
const body = [
'````',
'```',
'@~/.claude/gsd-core/workflows/nested.md',
'```',
'````',
].join('\n');
const result = resolveSpecRootReference(body);
assert.strictEqual(result, body, 'the inner 3-backtick lines are content, not closers, for a 4-backtick opener');
});
test('an unterminated fence covers to end-of-string, leaving the include untouched', () => {
const body = [
'```',
'@~/.claude/gsd-core/workflows/orphan.md',
].join('\n');
const result = resolveSpecRootReference(body);
assert.strictEqual(result, body, 'a fence with no closer is still open through EOF');
});
test('a fence opened with a longer run (````) is NOT closed by a shorter run (```)', () => {
const body = [
'````',
'@~/.claude/gsd-core/workflows/longshort.md',
'```',
'@~/.claude/gsd-core/workflows/other.md',
'````',
].join('\n');
const result = resolveSpecRootReference(body);
assert.strictEqual(
result,
body,
'a 3-backtick line cannot close a 4-backtick opener per CommonMark run-length rule — both includes stay inside the one fence',
);
});
});
// ─── F4 — resolveSpecRootReference is idempotent over arbitrary bodies ────
describe('resolveSpecRootReference — property (F4)', () => {
test('spec-root transform is idempotent over arbitrary bodies', () => {
// A bare fc.string() body would almost never contain the exact
// `@~/.claude/gsd-core/workflows/<stem>.md` shape `resolveSpecRootReference`
// matches, making the property vacuous (see this suite's F4 comment and
// CONTRIBUTING's writer-seeded-vs-document-shaped generator guidance).
// Instead, bodies are assembled from chunks that actually exercise every
// branch of the transform: a real include line (rewritten), a fenced
// block wrapping the SAME include shape (left untouched — Claude Code
// documents backticks as the way to prevent an `@`-import), a
// `@.planning/…` include (a different spec root, untouched), plain prose
// that merely MENTIONS `gsd-core/workflows/<stem>.md` without the
// line-start `@` (untouched), and arbitrary free text.
const stemArb = fc.stringMatching(/^[a-z][a-z0-9._-]{0,20}$/);
const includeLineArb = stemArb.map((s) => `@~/.claude/gsd-core/workflows/${s}.md`);
const proseMentionArb = stemArb.map((s) => `See gsd-core/workflows/${s}.md for background.`);
const planningIncludeArb = stemArb.map((s) => `@.planning/${s}.md`);
const fencedIncludeArb = fc.tuple(fc.constantFrom('```', '~~~'), stemArb).map(
([fence, s]) => `${fence}\n@~/.claude/gsd-core/workflows/${s}.md\n${fence}`,
);
// #2873 review Finding 1: a same-type-only generator is structurally
// incapable of producing the mismatched-delimiter defect the review
// constructed (a ``` fence "closed" by a ~~~ line). Also emit
// mismatched-type and nested-run shapes so the property actually
// exercises the CommonMark same-type/same-or-longer-run closing rule,
// not just the trivial same-fence-twice case.
const mismatchedFencedIncludeArb = fc.tuple(
fc.constantFrom(['```', '~~~'], ['~~~', '```']),
stemArb,
stemArb,
).map(
([[openFence, midFence], s1, s2]) =>
`${openFence}\n@~/.claude/gsd-core/workflows/${s1}.md\n${midFence}\n@~/.claude/gsd-core/workflows/${s2}.md\n${openFence}`,
);
const nestedFencedIncludeArb = fc.tuple(fc.constantFrom('```', '~~~'), stemArb).map(
([fenceChar, s]) => {
const inner = fenceChar.repeat(3);
const outer = fenceChar.repeat(4);
return `${outer}\n${inner}\n@~/.claude/gsd-core/workflows/${s}.md\n${inner}\n${outer}`;
},
);
const plainTextArb = fc.string({ maxLength: 40 });
const chunkArb = fc.oneof(
includeLineArb,
proseMentionArb,
planningIncludeArb,
fencedIncludeArb,
mismatchedFencedIncludeArb,
nestedFencedIncludeArb,
plainTextArb,
);
const bodyArb = fc.array(chunkArb, { maxLength: 12 }).map((chunks) => chunks.join('\n'));
fc.assert(
fc.property(bodyArb, (body) => {
const once = resolveSpecRootReference(body);
const twice = resolveSpecRootReference(once);
assert.strictEqual(
twice,
once,
`not idempotent — body: ${JSON.stringify(body)}\nonce: ${JSON.stringify(once)}\ntwice: ${JSON.stringify(twice)}`,
);
}),
// Explicit seed + bounded numRuns, replay data printed on failure via
// the assertion message above (fast-check's own thrown error additionally
// carries the pinned seed + shrunk counterexample needed to replay).
{ seed: 20260814, numRuns: 300 },
);
});
});