Files
msd-core/tests/executed-plan.test.cjs
Tom Boucher 107eb8c1d9 feat(#3753): run docs guards on the PR that changes the docs they read (#3787)
A PR whose diff is entirely under docs/ runs zero tests, so a guard whose INPUT
is shipped prose cannot protect the PR lane of the diffs it exists to check. Its
only firing opportunity is after merge, on the shared branch -- which is how next
went red on dacae9273 while the PR that caused it (#3746) was green on every
check.

The docs-lint job in .github/workflows/docs-required.yml -- an ALREADY-REQUIRED
context -- now selects and runs the docs guards that read the specific docs files
the PR changed.

  scripts/docs-guard-registry.cjs    test file -> the docs paths it reads (63)
  scripts/select-docs-guards.cjs     pure (changedPaths, registry) -> test files
  scripts/lint-docs-guard-registration.cjs   drift guard, wired into lint:ci

scripts/ci-test-scope.cjs is NOT touched -- `git diff origin/next --` on it is
empty -- so #764's saving stands and its 21 pinning tests are untouched.

Selection: exact path; trailing-slash directory prefix (boundary-checked --
docs/adrenaline.md does NOT match docs/adr/, which a naive startsWith gets
wrong); and '*' for the 6 entries that walk docs/ generally or read a computed
path. Unknown maps to '*' -- guessing narrow is how a guard silently stops
running. Measured: a typo fix selects 6 of 63; docs/AGENTS.md selects 12;
docs/COMMANDS.md selects 18.

Four things this got wrong first, each found by an independent reviewer or by
probe, and each having been asserted safe in a comment:

1. The registry started as a RULE in ci-test-scope.cjs's RULES, on the theory
   that classify()'s !codeChanged normalization made it inert. True for
   docs-ONLY diffs; false for MIXED docs+code diffs, where codeChanged is true
   and the normalization never runs:

     node scripts/ci-test-scope.cjs --files "docs/a.md src/semver.cts"
       with the RULE:  25 targeted_tests
       origin/next:     3 targeted_tests

   Category error: RULES is the scoped lane's input; a docs-guard registry is a
   lane manifest for a consumer that never calls classify(). Extracted; pinned
   by value.

2. The second attempt was a dedicated workflow with paths: [docs/**]. Such a
   workflow never reports on a non-docs PR, so it can never be a required
   context without hanging every non-docs PR -- and a non-required check does not
   block a merge, so the guard would have been advisory and #3753 unfixed.
   docs-required.yml already has no paths: filter, already supplies the required
   docs-lint context, already computes docs_changed, and already ran one docs
   guard gated on it. Generalizing that step needs no ruleset edit at all.

3. The registry and the drift lint were built from ONE path-segment heuristic, so
   both were blind identically -- and blind at the guard that motivated the issue.
   The reader-call regex required a character BEFORE its keyword, so a callee
   named exactly read( / load( / parse( / doc( / file( / content( could never
   match; and only an INLINE path.join(ROOT,'docs','X.md') argument was caught,
   missing the two-step-via-variable form -- the MAJORITY spelling -- plus
   template literals and concatenation. Detector 1 fired on 14 of ~450 files, so
   35 genuine guards sat unregistered while the lint reported 0 violations,
   including cursor-reviewer (reads docs/COMMANDS.md, asserts
   .includes('--cursor')) and inventory-headings-countfree. The "accepted blind
   spot" this shipped with was the common case, not a fringe.

4. With detection fixed the true population is 115 files: 63 genuine guards, 52
   incidental. Running all 63 in a REQUIRED check on a one-line typo fix is the
   cost #764 exists to avoid -- install.test.cjs is 7840 lines and reads exactly
   one docs file, docs/AGENTS.md, for its frontmatter. Dropping it reproduces the
   bug; running it for a typo elsewhere is waste. Hence the map.

Then a second review round found six more, all fixed here:

- fragment-single-edit-propagation.install.test.cjs was EXEMPTED as
  "overlay fixture only". False: it reads the real docs/registries/eos.json and
  asserts on a registry entry name, and reads the real ADR-0001 and asserts its
  H1. A docs-only PR touching either would have gone green and red next -- #3753
  shipping again, from inside the fix for it. Now registered against both paths,
  and all 52 remaining exemptions were re-audited one by one.
- The SUITES-collision guard compared RAW registry keys, but run-tests.cjs strips
  a leading `tests/` BEFORE its suite check. So it caught 'all' and missed
  'tests/all' -- the only spelling that can actually occur, since every key
  carries the prefix. One typo would have run all 824 test files inside the
  required job. Now normalized the same way run-tests.cjs normalizes.
- The lint failed OPEN on an unreadable tests dir or candidate file: 0 violations,
  ok:true. A guard that cannot read its input must never report success.
- The exemption ratchet gated identity only, so a baselined file that later
  STARTED asserting on shipped docs stayed exempt silently -- 52 permanently blind
  files. The baseline now fingerprints the docs paths each exempted file
  references and fails when that set changes, naming what changed.
- The exemption marker was still honored inside a multi-line template literal in
  the header window. The scanner now tracks template-literal and block-comment
  state.
- `git diff --name-only | grep '^docs/'` silently dropped C-quoted non-ASCII docs
  paths, making docs_changed=false a green zero-guard check. Both call sites now
  pass -c core.quotepath=false.
- The run step was gated on hashFiles(), which a force-committed
  .docs-guard-tests.txt would satisfy. The step now rm -f's both scratch files
  first and gates on an output it sets itself.

Three empty states, deliberately distinct, because conflating them rebuilds
#3753: an empty or malformed registry HARD-FAILS; docs changed with no guard
covering them logs and skips; no docs change is already gated. The middle state
must never be expressed as an empty --files-from, which prints `no tests in suite
"all"` and exits 0 -- a green check that guarded nothing. With the current
registry that state is unreachable, because the six '*' entries always match;
the branch is kept as defensive handling for a future registry and says so.

timeout-minutes: 15 bounds the required job against a hanging fork-supplied test;
it had none. npm ci was added because the job never installed dependencies -- the
previous single-file step got away without it, the registry does not.

docs/contributing/docs-guard-registration.md documents the rule, following its
sibling cross-platform-portability-rules.md, and CONTRIBUTING.md's CI Test
Quality Checks table links to it. It is also load-bearing: without a docs/ file
in the diff this PR would not have triggered its own lane, shipping an
unexercised change to a required check.

One unrelated fix, included because this PR surfaced it and CLAUDE.md forbids
deferring a defect found while working. On this branch's first CI run,
`full test (windows-latest, 24, shard 3/3)` was CANCELLED at exactly 30 minutes;
tests were still passing 0.8s before the cancel, so it is a wall-clock timeout,
not a hang, and a cancelled job reddens `Required tests`.

The cause is not this PR's test file, which costs ~60ms. Shard composition is
unstable: adding ONE file to the unit suite reshuffled 115 of 268 files between
shards, and shard 3 drew a heavier mix. Underneath that is a real pre-existing
defect. tests/ci-test-job-timeout-budget.test.cjs requires every lane's budget to
be >= 1.5x its MEASURED cost -- "a lane that got slower must be re-budgeted, not
excused" -- and its test-full entry recorded 19m from a windows-22 shard. That is
stale. Measured on `next` with none of this PR's changes present: 26m18s (run
32614439702, windows-latest/24 shard 3/3), 23m36s and 23m17s on shard 2/3. So the
lane costs ~26m and the 30-minute cap carried 1.14x headroom, not 1.5x. The gate
had been out of compliance with its own rule; this PR was merely the file
addition that reshuffled shard 3 past the cliff.

Fixed as that file prescribes: measuredMinutes 19 -> 27 with fresh evidence, and
test-full timeout-minutes 30 -> 45. The rule's minimum for 27m is 41; 45 is
deliberately above it because the reshuffle means per-shard worst case moves run
to run, and a budget pinned to the exact minimum would be re-breached by the next
test file anyone adds. Only that one job's timeout changed; test.yml's scope,
matrix and steps are untouched, so #764's saving is unaffected.

Raising that cap let the Windows shard finish (28m45s, inside 45) and uncovered
a real failure the 30-minute cancel had been masking:
`new quick-task branch branches off origin/main (#2916)` died with
`outcome=timed_out exitCode=null`, SIGTERM, at the 15000ms bound.

tests/quick-branching.test.cjs:149 `runStep` runs a `#!/usr/bin/env bash` script
executing MULTIPLE git commands, but was bound to GIT_TIMEOUT_MS (15000) -- the
norm for a SINGLE git plumbing call. tests/helpers/timeouts.cjs already documents
this exact failure and exists to fix it: HOOK_FANOUT_TIMEOUT_MS was created after
PR #3285 recorded "outcome=timed_out exitCode=null at exactly the 15000ms probe
bound while every other lane passed the same commit", and calls that "a bound
sized for the wrong class, not a slow machine". Our failure is that case
verbatim, so both sites move to the class norm rather than to a bigger number.

The same class also failed on `next` itself 21 hours earlier -- run 32608945654,
windows-latest/24 shard 1/3, `plan touching only src/ in a submodule project
keeps worktree isolation ENABLED` -- where tests/worktree-safety.test.cjs:5845
`runGate` fans out to `git config --file .gitmodules` under a hardcoded 30000.
Fixed too, since it is a defect in the tree regardless of which branch surfaced
it.

A survey of the whole tests/ tree found the same class-mismatch at further
bash fan-out sites bound under 60000ms, and the maintainer approved sweeping
them rather than leaving them latent to surface the same way one at a time. 16
fan-out sites across 16 files now use the class norm.

The sweep is class-correctness, not raising numbers until things pass. Sites
were moved ONLY where the bash body demonstrably spawns something (git, node,
npm, a CLI); self-contained shell snippets were left where they are, and are
listed as deliberately unchanged: pure if/printf bodies (copilot-install), pure
array/case builtins (code-review-pipeline-regression:638), a documented
pure-shell gsd_run stub (host-integration), single-process hook calls
(workflow-guard:222/271/302), and a deliberately tight 5000ms fast-check hook
(gsd-write-guard.property). Nothing was lowered. process-seam.test.cjs:513
(literal 300) is untouched on purpose -- it tests timeout BEHAVIOR, so raising
it would destroy what it asserts.

Shared file-level constants were the trap here, and were handled per file rather
than by redefinition: GIT_TIMEOUT_MS has ~15 users in git-base-branch and only 1
is a fan-out; WORKTREE_TIMEOUT_MS has 16 users in worktree.test.cjs and 3 are;
PROBE_TIMEOUT_MS has several in three more files. In each the CALL SITE was
changed and the constant left alone, so no single-plumbing-call site silently
inherited a 60s bound. The one exception is hooks-opt-in.test.cjs, where
HOOK_TIMEOUT_MS has exactly one consumer -- spawnHook, the fan-out itself -- so
redefining it is identical in effect and reads better.

Only two of these sites have actually been observed failing. The rest cite that
shared class and those two run ids rather than inventing evidence of their own.

Co-authored-by: sim <sim@local>
2026-08-23 21:21:21 -04:00

1320 lines
61 KiB
JavaScript

'use strict';
/**
* Executed-plan return + fs adapter seam — failing-first tests.
*
* #2874 (epic #2866 Phase 5), governed by ADR-58
* (docs/adr/58-runtime-install-policy-module.md).
*
* Design: .gsd/phase/feat-2874-executed-plan-return/40-design.md
* Test matrix: .gsd/phase/feat-2874-executed-plan-return/50-test-matrix.md
*
* This file implements the Red-first order's rows 1-3 from 50-test-matrix.md:
* - E3 (section E, "Executed-plan return shape"): the opencode-family
* early return must ALSO return an executed plan, not `undefined`.
* - E13 (section E): every runtime in the capability registry must return
* something other than `undefined` — the completeness sweep proving the
* contract has no per-runtime holes.
* - F2 (section F, "Fs adapter seam"): a full install driven by an
* injected fake adapter must touch zero real filesystem paths.
*
* All three are RED against the current tree: `installRuntimeArtifacts`
* (src/install-engine.cts:750) still returns `void` and accepts no `deps`/
* adapter parameter to route IO through. No production code is touched here
* — this package is tests only.
*/
process.env.GSD_TEST_MODE = '1';
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 crypto = require('node:crypto');
const fc = require('fast-check');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { installRuntimeArtifacts, hasExistingSymlinkBetween } = require('../gsd-core/bin/lib/install-engine.cjs');
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
const { loadSkillsManifest, resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
const runtimeArtifactLayout = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const runtimeArtifactInstallPlan = require('../gsd-core/bin/lib/runtime-artifact-install-plan.cjs');
const { withInstallFs } = require('../gsd-core/bin/lib/install-fs-adapter.cjs');
const commandRoster = require('../gsd-core/bin/lib/command-roster.cjs');
const slashCommandTransformer = require('../scripts/fix-slash-commands.cjs');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
const TEST_ATTRIBUTION = () => 'Co-Authored-By: Test <t@example.com>';
/**
* Sandbox HOME/USERPROFILE for the duration of a test. Some runtimes (e.g.
* codex) resolve a kind's `home` via os.homedir(); without this, an in-process
* install would write into the developer's real home directory.
*
* #3712: promoted to tests/helpers.cjs, from the byte-identical copy that used
* to live here. It now also sets the sandbox marker src/real-home-guard.cts
* needs to stay permissive on hosts with no readable passwd entry.
*/
const { sandboxHome } = require('./helpers.cjs');
// ─── E3 — the opencode-family early return (matrix row E3) ──────────────────
describe('installRuntimeArtifacts — E3: opencode-family early return', () => {
test('family install still returns a plan', (t) => {
const configDir = createTempDir('gsd-e3-opencode-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('opencode', configDir, 'global', RESOLVED_CORE);
assert.notStrictEqual(
result,
undefined,
'E3: the combinedFamilyInstall early return (install-engine.cts:774) must return an ' +
'executed plan, not undefined — a whole runtime family returning undefined is a hole ' +
'in the contract, not an exemption (40-design.md behavior table row 2)',
);
});
});
// ─── E13 — the all-runtimes sweep (matrix row E13) ───────────────────────────
describe('installRuntimeArtifacts — E13: no runtime returns undefined', () => {
const RUNTIMES = Object.keys(registry.runtimes);
test('registry enumerates at least one runtime to sweep', () => {
assert.ok(RUNTIMES.length > 0, 'capability-registry.cjs runtimes must be non-empty');
});
for (const runtime of RUNTIMES) {
test(`${runtime}: installRuntimeArtifacts does not return undefined`, (t) => {
const configDir = createTempDir(`gsd-e13-${runtime}-`);
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts(runtime, configDir, 'global', RESOLVED_CORE);
assert.notStrictEqual(
result,
undefined,
`E13: ${runtime} returned undefined — every runtime in the registry must return an ` +
'executed plan (40-design.md: "Legitimate undefined returns: none after this phase. ' +
'If any path can still return undefined, that path is a defect, not an exemption.")',
);
});
}
});
// ─── F2 — zero real filesystem contact (matrix row F2) ───────────────────────
// The full write+read surface installRuntimeArtifacts's call tree is known to
// reach once every gap named in the #2874 follow-up round is closed: the
// direct mkdirSync/existsSync/rmSync calls, _copyStaged's readdirSync/cpSync/
// copyFileSync/mkdirSync, _removeGsdEntries's directory scan+delete,
// _snapshotDir/_restoreDir's read/write of preserved skill dirs, the
// symlink-escape guard's lstatSync/realpathSync probes, commonjs-marker.cts's
// lstatSync/writeFileSync/unlinkSync, and installer-migrations.cts's
// existsSync/readFileSync/openSync/readSync/closeSync (readInstallManifest,
// classifyArtifact, sha256File — sha256File streams via openSync/readSync/
// closeSync, restored after a brief round-trip through readFileSync broke
// tests/installer-migrations.test.cjs's large-file-streaming contract; the
// fake below implements all three against its store so a fake-adapter
// install still never touches real fs for hashing).
//
// mkdtempSync stays poisoned as a genuine tripwire, not a reachable case:
// mkInstallTempDir (install-fs-adapter.cts) only ever calls real
// `fs.mkdtempSync` when `current === REAL_ADAPTER` (no adapter injected at
// all) — a fake-adapter call always makes `current` a distinct merged
// object, so it takes the synthesize-name-and-mkdirSync branch instead and
// never reaches this poison. If this ever fires, `current`'s identity check
// broke, not a documented gap.
//
// One exception this poison list does NOT cover: readGsdCommandNames
// (command-roster.cts) reads the PACKAGE'S OWN commands/gsd/ source tree via
// real fs.readdirSync — deliberately unrouted (see install-fs-adapter.cts's
// module doc, "DELIBERATELY NOT ROUTED"). `poisonRealFsAgainstDestination`
// below allows real calls scoped to that known package-source root and
// poisons everything else, rather than poisoning every real fs call
// wholesale regardless of path.
const REAL_FS_WRITE_SURFACE = [
'mkdirSync', 'existsSync', 'rmSync', 'readdirSync',
'cpSync', 'copyFileSync', 'readFileSync', 'writeFileSync', 'lstatSync',
'realpathSync', 'unlinkSync', 'rmdirSync',
'mkdtempSync', 'openSync', 'readSync', 'closeSync',
];
// Package-source roots a correct install is expected to read for real, even
// while a fake DESTINATION adapter is injected (40-design.md "Known limits":
// this seam makes destination IO fake-able; package-source IO stays real by
// design). Mirrors findInstallSourceRoot's/findAgentsSourceRoot's/
// readGsdCommandNames's own targets (commands/gsd/, agents/), all resolved
// the same way REAL_COMMANDS_DIR is above.
const PACKAGE_SOURCE_ROOTS = [REAL_COMMANDS_DIR, path.join(__dirname, '..', 'agents')];
function isPackageSourcePath(resolvedPath) {
return PACKAGE_SOURCE_ROOTS.some(
(root) => resolvedPath === root || resolvedPath.startsWith(root + path.sep),
);
}
/**
* F2's real-fs poisoning, derived from the rule (40-design.md "Known
* limits"/install-fs-adapter.cts's module doc) rather than aligned with it by
* coincidence: a real fs call against the install DESTINATION is a failure —
* the seam exists precisely so a fake adapter can intercept those — but a
* real call against the package's OWN source tree (commands/gsd/, agents/)
* is expected and allowed, because that read is deliberately unrouted
* (readGsdCommandNames et al.). Poisoning every real fs method wholesale,
* regardless of path, makes a correct install fail for the wrong reason.
*
* Returns a Map<method, count> of package-source hits, so a caller can
* assert POSITIVELY that the expected package-source read actually happened
* — proving the boundary was exercised, not merely tolerated.
*/
function poisonRealFsAgainstDestination(t, label) {
const packageSourceHits = new Map();
for (const method of REAL_FS_WRITE_SURFACE) {
const original = fs[method].bind(fs);
t.mock.method(fs, method, (...args) => {
const target = args[0];
const resolved = (typeof target === 'string' || target instanceof URL || Buffer.isBuffer(target))
? path.resolve(String(target))
: null;
if (resolved !== null && isPackageSourcePath(resolved)) {
packageSourceHits.set(method, (packageSourceHits.get(method) ?? 0) + 1);
return original(...args);
}
throw new Error(
`F2${label}: real fs.${method}() was reached against a non-package-source path ` +
`(${resolved ?? String(target)}) during an install driven by an injected fake adapter`,
);
});
}
return packageSourceHits;
}
/**
* A genuinely functional in-memory filesystem, not a set of no-op stubs —
* required to drive the branches F2 now exercises (opencode-family legacy-dir
* migration, a nativePlugin runtime, a retiredArtifacts runtime) far enough
* to reach commonjs-marker.cts and installer-migrations.cts's routed
* classifyArtifact/readInstallManifest, not just the happy path's first
* existsSync check. Every method operates against one flat `Map<absPath,
* entry>` store; `readdirSync` derives listings by prefix-scanning the same
* store (an entry that readdirSync reports a directory contains is, by
* construction, also existsSync-true at that exact path — same invariant a
* real filesystem holds).
*
* @param seed - Array<[absPath, {type:'file'|'dir', content?:string|Buffer}]>
* pre-populated entries.
*/
function createFakeInstallFs(seed = []) {
const store = new Map();
for (const [p, entry] of seed) store.set(path.normalize(String(p)), entry);
const fdTable = new Map();
let nextFd = 1;
const norm = (p) => path.normalize(String(p));
const childPrefix = (dir) => {
const n = norm(dir);
return n.endsWith(path.sep) ? n : n + path.sep;
};
const enoent = (p) => {
const err = new Error(`ENOENT: no such file or directory, '${p}'`);
err.code = 'ENOENT';
return err;
};
const fakeFs = {
existsSync: (p) => store.has(norm(p)),
lstatSync: (p) => {
const e = store.get(norm(p));
if (!e) throw enoent(p);
return {
isFile: () => e.type === 'file',
isDirectory: () => e.type === 'dir',
isSymbolicLink: () => e.type === 'symlink',
};
},
mkdirSync: (p) => { store.set(norm(p), { type: 'dir' }); return undefined; },
rmSync: (p) => {
const n = norm(p);
store.delete(n);
const prefix = childPrefix(n);
for (const k of [...store.keys()]) if (k.startsWith(prefix)) store.delete(k);
},
unlinkSync: (p) => {
const n = norm(p);
if (!store.has(n)) throw enoent(p);
store.delete(n);
},
rmdirSync: (p) => { store.delete(norm(p)); },
readdirSync: (p, opts) => {
const prefix = childPrefix(p);
const names = new Set();
for (const k of store.keys()) {
if (!k.startsWith(prefix)) continue;
const rest = k.slice(prefix.length);
const sepIdx = rest.indexOf(path.sep);
const name = sepIdx === -1 ? rest : rest.slice(0, sepIdx);
if (name) names.add(name);
}
const arr = [...names];
if (opts && opts.withFileTypes) {
return arr.map((name) => {
const full = norm(path.join(String(p), name));
const e = store.get(full);
return {
name,
isFile: () => (e ? e.type === 'file' : false),
isDirectory: () => (e ? e.type === 'dir' : true),
};
});
}
return arr;
},
readFileSync: (p, encoding) => {
const e = store.get(norm(p));
if (!e || e.type !== 'file') throw enoent(p);
const buf = Buffer.isBuffer(e.content) ? e.content : Buffer.from(e.content ?? '', 'utf8');
return encoding ? buf.toString(encoding) : buf;
},
// sha256File (installer-migrations.cts) streams via openSync/readSync/
// closeSync instead of readFileSync (large-file hashing must not buffer
// the whole file — tests/installer-migrations.test.cjs pins this). fdTable
// maps a synthetic fd to {buf, pos} so this fake never needs a real fd.
openSync: (p) => {
const e = store.get(norm(p));
if (!e || e.type !== 'file') throw enoent(p);
const buf = Buffer.isBuffer(e.content) ? e.content : Buffer.from(e.content ?? '', 'utf8');
const fd = nextFd++;
fdTable.set(fd, { buf, pos: 0 });
return fd;
},
readSync: (fd, buffer, offset, length, position) => {
const entry = fdTable.get(fd);
if (!entry) {
const err = new Error(`EBADF: bad file descriptor, read (fake fd ${fd})`);
err.code = 'EBADF';
throw err;
}
const readAt = position === null || position === undefined ? entry.pos : position;
const bytesToRead = Math.max(0, Math.min(length, entry.buf.length - readAt));
entry.buf.copy(buffer, offset, readAt, readAt + bytesToRead);
if (position === null || position === undefined) entry.pos += bytesToRead;
return bytesToRead;
},
closeSync: (fd) => { fdTable.delete(fd); },
writeFileSync: (p, data, opts) => {
// Emulate `{ flag: 'wx' }` (exclusive create): REAL_ADAPTER.writeFileSync
// (install-fs-adapter.cts:138) passes `opts` straight through to real
// `fs.writeFileSync`, which throws EEXIST for `wx` against an existing
// path. A fake that silently overwrote here would certify something
// the real implementation refuses — see commonjs-marker.cts's
// `ensureCommonJsMarker`, which relies on `wx` to close the
// classify-then-write gap.
const flag = typeof opts === 'object' && opts !== null ? opts.flag : undefined;
const n = norm(p);
if (flag === 'wx' && store.has(n)) {
const err = new Error(`EEXIST: file already exists, open '${p}'`);
err.code = 'EEXIST';
throw err;
}
store.set(n, { type: 'file', content: data });
},
copyFileSync: (src, dest) => {
const e = store.get(norm(src));
store.set(norm(dest), { type: 'file', content: e ? e.content : Buffer.alloc(0) });
},
cpSync: (src, dest) => {
const sn = norm(src);
const dn = norm(dest);
const e = store.get(sn);
if (e) store.set(dn, { ...e });
const prefix = childPrefix(sn);
for (const [k, v] of [...store.entries()]) {
if (k.startsWith(prefix)) store.set(dn + k.slice(sn.length), { ...v });
}
},
realpathSync: (p) => norm(p),
};
fakeFs._store = store;
return fakeFs;
}
// ─── createFakeInstallFs — wx exclusive-create emulation ────────────────────
//
// REAL_ADAPTER.writeFileSync (install-fs-adapter.cts:138) passes `opts`
// through untouched to real fs.writeFileSync, so `{ flag: 'wx' }` throws
// EEXIST against an existing target (commonjs-marker.cts's
// ensureCommonJsMarker relies on exactly this to close the
// classify-then-write TOCTOU gap). A fake that ignored `opts` would silently
// overwrite where the real adapter refuses — this covers the emulation
// itself rather than assuming it.
describe('createFakeInstallFs — wx exclusive-create emulation', () => {
test('refuses an exclusive create against an existing path (EEXIST)', () => {
const target = path.join(os.tmpdir(), 'gsd-fake-wx-existing.txt');
const fakeFs = createFakeInstallFs([[target, { type: 'file', content: 'original' }]]);
assert.throws(
() => fakeFs.writeFileSync(target, 'clobber', { flag: 'wx' }),
(err) => err.code === 'EEXIST',
'wx write against an existing fake-store path must throw EEXIST, matching real fs.writeFileSync',
);
assert.strictEqual(
fakeFs.readFileSync(target, 'utf8'),
'original',
'a refused wx write must leave the existing content untouched',
);
});
test('allows an exclusive create against an absent path', () => {
const target = path.join(os.tmpdir(), 'gsd-fake-wx-absent.txt');
const fakeFs = createFakeInstallFs();
fakeFs.writeFileSync(target, 'created', { flag: 'wx' });
assert.strictEqual(fakeFs.readFileSync(target, 'utf8'), 'created');
});
});
/** sha256 hex digest matching installer-migrations.cts's sha256File — used to
* seed a manifest entry that classifies a fake file as 'managed-pristine'. */
function sha256Hex(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
describe('installRuntimeArtifacts — F2: fake-adapter install touches no real filesystem', () => {
test('fake-adapter install touches no real filesystem (claude, skills-only)', (t) => {
// Every real fs method this call tree could reach is poisoned BY PATH
// (see poisonRealFsAgainstDestination) for the duration of this test via
// node:test's mock tracker (auto-restored when the test ends — no
// try/finally in the test body, per CONTRIBUTING.md's "Never use
// try/finally inside test bodies").
const packageSourceHits = poisonRealFsAgainstDestination(t, '');
const fakeFs = createFakeInstallFs();
// configDir deliberately never created for real — F2 asserts nothing
// real ever gets written under it.
const configDir = path.join(os.tmpdir(), `gsd-f2-must-not-exist-${crypto.randomUUID()}`);
const result = installRuntimeArtifacts(
'claude', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
);
assert.notStrictEqual(
result,
undefined,
'F2: a fake-adapter install must still return an executed plan (matrix row F1/E1 shape)',
);
// No post-hoc fs.existsSync(configDir) check follows: fs.existsSync is
// one of the poisoned (non-package-source) methods above for the
// duration of this test, so the proof of "zero real DESTINATION fs
// contact" IS that installRuntimeArtifacts returned at all without
// tripping one of the throws — not a probe that would itself have to
// touch the poisoned surface.
assert.ok(
(packageSourceHits.get('readdirSync') ?? 0) > 0,
'F2: readGsdCommandNames must have read the real, unrouted commands/gsd/ package-source ' +
'tree at least once — proving the poison boundary was exercised, not merely tolerated',
);
});
test('fake-adapter install touches no real filesystem (opencode-family legacy command/ dir migration)', (t) => {
poisonRealFsAgainstDestination(t, ' (opencode legacy migration)');
const configDir = path.join(os.tmpdir(), `gsd-f2-opencode-legacy-${crypto.randomUUID()}`);
const legacyDir = path.join(configDir, 'command');
const legacyFile = path.join(legacyDir, 'gsd-old-cmd.md');
const content = '# stale legacy command\n';
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestJson = JSON.stringify({ files: { 'command/gsd-old-cmd.md': sha256Hex(content) } });
const fakeFs = createFakeInstallFs([
[configDir, { type: 'dir' }],
[legacyDir, { type: 'dir' }],
[legacyFile, { type: 'file', content }],
[manifestPath, { type: 'file', content: manifestJson }],
]);
const result = installRuntimeArtifacts(
'opencode', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
);
assert.notStrictEqual(result, undefined, 'F2 (opencode legacy migration): must still return a plan');
// The manifest hash matches the seeded content exactly, so
// _migrateLegacyOpencodeCommandDir's classifyArtifact call must have
// classified it 'managed-pristine' and unlinked it (real
// installerMigrations.readInstallManifest/classifyArtifact/sha256File —
// all routed through installFs() — computed this via the fake, not real
// fs, or the poisoned methods above would have thrown first).
assert.strictEqual(
fakeFs._store.has(path.normalize(legacyFile)),
false,
'F2 (opencode legacy migration): the managed-pristine legacy file must have been removed via the fake store',
);
});
test('fake-adapter install touches no real filesystem (nativePlugin runtime: pi)', (t) => {
poisonRealFsAgainstDestination(t, ' (nativePlugin)');
// Resolve the SAME pluginSrc path _installNativePluginIfDeclared
// (install-engine.cts) computes for pi's declared nativePlugin, using the
// real (unrouted, package-own-source) findInstallSourceRoot — this read
// happens BEFORE the poison mocks above are installed... no: it must
// happen before `t.mock.method` calls would matter for IT, but
// findInstallSourceRoot's own walk uses `fs.statSync`, which is NOT on
// the poisoned list (see install-fs-adapter.cts's module doc — it is
// deliberately unrouted, real-fs-only, package-source introspection), so
// resolving this here is safe even after poisoning existsSync et al.
const commandsGsdDir = runtimeArtifactLayout.findInstallSourceRoot();
const repoRoot = path.dirname(path.dirname(commandsGsdDir));
const nativePlugin = registry.runtimes.pi.runtime.hostBehaviors.nativePlugin;
assert.ok(nativePlugin && nativePlugin.source, 'pi must declare hostBehaviors.nativePlugin.source (registry drifted)');
const pluginSrc = path.join(repoRoot, nativePlugin.source);
const configDir = path.join(os.tmpdir(), `gsd-f2-pi-nativeplugin-${crypto.randomUUID()}`);
const fakeFs = createFakeInstallFs([
[pluginSrc, { type: 'file', content: '// fake plugin adapter\n' }],
]);
const result = installRuntimeArtifacts(
'pi', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
);
assert.notStrictEqual(result, undefined, 'F2 (nativePlugin): must still return a plan');
assert.strictEqual(result.postSteps.nativePlugin, true, 'F2 (nativePlugin): postSteps.nativePlugin must be true for pi');
const destPath = path.join(configDir, nativePlugin.dir, nativePlugin.file);
assert.strictEqual(
fakeFs._store.has(path.normalize(destPath)),
true,
'F2 (nativePlugin): the plugin file must have been copied via the fake store (copyFileSync routed)',
);
const markerPath = path.join(configDir, nativePlugin.dir, 'package.json');
assert.strictEqual(
fakeFs._store.has(path.normalize(markerPath)),
true,
'F2 (nativePlugin): ensureCommonJsMarker (commonjs-marker.cts) must have written the CommonJS marker via the fake store',
);
});
test('fake-adapter install touches no real filesystem (retiredArtifacts runtime: cursor)', (t) => {
const packageSourceHits = poisonRealFsAgainstDestination(t, ' (retiredArtifacts)');
const retired = registry.runtimes.cursor.runtime.hostBehaviors.retiredArtifacts;
assert.ok(Array.isArray(retired) && retired.length > 0, 'cursor must declare hostBehaviors.retiredArtifacts (registry drifted)');
const { destSubpath, prefix, suffix } = retired[0];
const configDir = path.join(os.tmpdir(), `gsd-f2-cursor-retired-${crypto.randomUUID()}`);
const destDir = path.resolve(configDir, destSubpath);
const staleName = `${prefix}retired-probe${suffix}`;
const staleFile = path.join(destDir, staleName);
const content = '# stale retired artifact\n';
const relPath = `${destSubpath.replace(/\\/g, '/')}/${staleName}`;
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestJson = JSON.stringify({ files: { [relPath]: sha256Hex(content) } });
const fakeFs = createFakeInstallFs([
[configDir, { type: 'dir' }],
[destDir, { type: 'dir' }],
[staleFile, { type: 'file', content }],
[manifestPath, { type: 'file', content: manifestJson }],
]);
const result = installRuntimeArtifacts(
'cursor', configDir, 'global', RESOLVED_CORE, undefined, undefined,
{ fs: fakeFs },
);
assert.notStrictEqual(result, undefined, 'F2 (retiredArtifacts): must still return a plan');
// manifest hash matches the seeded content exactly -> classifyArtifact
// must classify 'managed-pristine' -> pruneRetiredRuntimeArtifacts
// (retired-artifact-cleanup.cts, routed) unlinks it via the fake store.
assert.strictEqual(
fakeFs._store.has(path.normalize(staleFile)),
false,
'F2 (retiredArtifacts): the managed-pristine retired artifact must have been removed via the fake store',
);
assert.ok(
(packageSourceHits.get('readdirSync') ?? 0) > 0,
'F2 (retiredArtifacts): readGsdCommandNames must have read the real, unrouted commands/gsd/ ' +
'package-source tree at least once — proving the poison boundary was exercised, not merely tolerated',
);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// #2874 follow-up round — 50-test-matrix.md rows E1/E2/E4-E12, F4-F6, G2,
// H1-H5, I1-I5, K3, L1-L2. Extends the F2/E3/E13 coverage above rather than a
// new file (install's file-count prefix is grandfathered at 8, must not grow).
// ═══════════════════════════════════════════════════════════════════════════
// ─── E. Executed-plan return shape (E1, E2, E4-E12) ──────────────────────────
describe('installRuntimeArtifacts — E1: claude global, normal install', () => {
test('returns an executed plan for a normal install', (t) => {
const configDir = createTempDir('gsd-e1-claude-global-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
assert.ok(Array.isArray(result.kinds) && result.kinds.length > 0, 'E1: plan must name at least one kind');
for (const k of result.kinds) {
assert.strictEqual(typeof k.kind, 'string', 'E1: every kind entry must name its kind');
assert.strictEqual(typeof k.sourceDir, 'string', 'E1: every kind entry must name its sourceDir');
assert.strictEqual(typeof k.destDir, 'string', 'E1: every kind entry must name its destDir');
}
});
});
describe('installRuntimeArtifacts — E2: claude local', () => {
test('executed plan records the scope', (t) => {
const configDir = createTempDir('gsd-e2-claude-local-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('claude', configDir, 'local', RESOLVED_CORE);
assert.strictEqual(result.scope, 'local', 'E2: local scope must be reflected verbatim on the returned plan');
});
});
describe('installRuntimeArtifacts — E4: kilo (second family member)', () => {
test('kilo family install still returns a plan', (t) => {
const configDir = createTempDir('gsd-e4-kilo-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('kilo', configDir, 'global', RESOLVED_CORE);
assert.notStrictEqual(
result, undefined,
'E4: kilo, the SECOND combined-family runtime, must ALSO return a plan — E3 is not a one-runtime special case',
);
// 'agents' was added here deliberately by #2875 Part 2 Task A
// (installAgentsKindStandalone, install-engine.cts:1614-1618): the
// combined-family (opencode/kilo) executed plan now also reports the
// agents kind it stages via installAgentsKindStandalone, mirroring the
// generic layout-driven loop's own top-level shape (install-engine.cts:1627-1637).
// This test was written under #2874 (Phase 5), before that kind was
// wired in — its expected list was never updated. Kilo's resolved layout
// declares an `agents` kind, so a correct plan MUST include it; a
// ['commands', 'skills']-only expectation encoded the pre-#2875 shape,
// not a real contract.
assert.deepStrictEqual(result.kinds.map((k) => k.kind).sort(), ['agents', 'commands', 'skills']);
});
});
describe('installRuntimeArtifacts — E5/E7: empty layout + nativePlugin post-step (pi)', () => {
test('empty layout returns an empty plan', (t) => {
const configDir = createTempDir('gsd-e5-pi-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('pi', configDir, 'global', RESOLVED_CORE);
assert.ok(Array.isArray(result.kinds), 'E5: kinds must be an array even when layout.kinds is empty');
assert.strictEqual(result.kinds.length, 0, 'E5: pi declares an empty artifactLayout — kinds must be [], never undefined');
});
test('native plugin post-step is recorded', (t) => {
const configDir = createTempDir('gsd-e7-pi-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('pi', configDir, 'global', RESOLVED_CORE);
assert.strictEqual(
result.postSteps.nativePlugin, true,
'E7: pi declares hostBehaviors.nativePlugin — postSteps.nativePlugin must record it as a post-step',
);
});
});
describe('installRuntimeArtifacts — E6: hermes post-step is recorded', () => {
test('hermes post-step is recorded', (t) => {
const configDir = createTempDir('gsd-e6-hermes-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('hermes', configDir, 'global', RESOLVED_CORE);
assert.strictEqual(
result.postSteps.hermesBareStemCleanup, true,
'E6: hermes must record _removeHermesBareStemDirs having run as a post-step',
);
});
});
describe('installRuntimeArtifacts — E8: preserved user skill dirs are recorded', () => {
test('preserved user skill dirs are recorded', (t) => {
const configDir = createTempDir('gsd-e8-claude-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const preservedSkillDir = path.join(configDir, 'skills', 'gsd-dev-preferences');
fs.mkdirSync(preservedSkillDir, { recursive: true });
fs.writeFileSync(path.join(preservedSkillDir, 'SKILL.md'), '# my custom prefs\n');
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
const skillsKind = result.kinds.find((k) => k.kind === 'skills');
assert.ok(skillsKind, 'E8 precondition: claude global must write a skills kind');
assert.deepStrictEqual(
skillsKind.preserved, ['gsd-dev-preferences'],
'E8: the plan must record gsd-dev-preferences as preserved',
);
assert.strictEqual(
fs.readFileSync(path.join(preservedSkillDir, 'SKILL.md'), 'utf8'),
'# my custom prefs\n',
'E8: the preserved content must actually have been restored after the prune+copy, not just recorded on the plan',
);
});
});
describe('installRuntimeArtifacts — E9: non-skills kind records its writes', () => {
test('non-skills kind records its writes', (t) => {
const configDir = createTempDir('gsd-e9-claude-local-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('claude', configDir, 'local', RESOLVED_CORE);
const commandsKind = result.kinds.find((k) => k.kind === 'commands');
assert.ok(commandsKind, 'E9 precondition: claude local must write a commands kind');
assert.strictEqual(commandsKind.destDir, path.join(configDir, 'commands'));
assert.ok(
fs.existsSync(commandsKind.destDir) && fs.readdirSync(commandsKind.destDir).length > 0,
'E9: the destDir the plan records must actually contain the copied files',
);
});
});
describe('installRuntimeArtifacts — E10: plan item naming a kind absent from layout.kinds', () => {
test('unknown kind still throws', (t) => {
const configDir = createTempDir('gsd-e10-claude-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const original = runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan;
t.after(() => { runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan = original; });
// Module-ref monkeypatch (same pattern as
// tests/runtime-artifact-layout-surface.test.cjs) — install-engine.cts
// reads this via the module reference, not a destructured local, so
// reassigning the export is observed at call time.
runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan = (args) => {
const real = original(args);
if (!real.ok) return real;
return {
ok: true,
plan: {
items: [...real.plan.items, { kind: 'not-a-real-kind', sourceDir: configDir, destDir: configDir }],
cleanupDirs: real.plan.cleanupDirs,
},
};
};
assert.throws(
() => installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE),
/unknown artifact kind/i,
'E10: a plan item naming a kind absent from layout.kinds must still throw "unknown artifact kind"',
);
});
});
describe('installRuntimeArtifacts — E11: plan is not shared across calls', () => {
test('plan is not shared across calls', (t) => {
const configDir = createTempDir('gsd-e11-claude-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const first = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
first.kinds.push({ kind: 'mutated-by-caller', sourceDir: 'x', destDir: 'y', preserved: [] });
first.postSteps.mutatedFlag = true;
const second = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
assert.notStrictEqual(second, first, 'E11: each call must return a fresh object, not the same reference');
assert.notStrictEqual(second.kinds, first.kinds, 'E11: kinds array must not be shared across calls');
assert.ok(
!second.kinds.some((k) => k.kind === 'mutated-by-caller'),
'E11: mutating the first result must not leak into the second call\'s plan',
);
assert.strictEqual(
second.postSteps.mutatedFlag, undefined,
'E11: mutating the first result\'s postSteps must not leak into the second call',
);
});
});
describe('installRuntimeArtifacts — E12: executed plan key set is locked', () => {
test('executed plan key set is locked', (t) => {
const configDir = createTempDir('gsd-e12-claude-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
assert.deepStrictEqual(
Object.keys(result).sort(),
['cleanup', 'kinds', 'postSteps', 'runtime', 'scope'],
'E12: the executed-plan top-level key set is a locked contract — an added/renamed/removed key ' +
'here is a breaking change to AC1/AC4 and must be a deliberate, reviewed decision, not an ' +
'incidental refactor',
);
});
});
// ─── F. Fs adapter seam — F4-F6 ───────────────────────────────────────────────
/**
* Build a fs object that implements EVERY InstallFsAdapter method by
* delegating to real `node:fs` (mirroring install-fs-adapter.cts's own
* REAL_ADAPTER), then applies `overrides` on top. buildGuardedAdapter
* (install-fs-adapter.cts) now throws for any method an injected partial
* omits (the module doc's "PARTIAL-ADAPTER TRAP" fix), so an end-to-end test
* that drives a REAL install against a REAL destDir (F4/I2/I5 below — these
* need real command/agent source content actually copied) while
* intercepting only one or two specific calls needs a COMPLETE fake that
* only fakes what it overrides — exactly the "documented, intended usage"
* install-fs-adapter.cts's own module doc calls out, as opposed to
* `createFakeInstallFs`'s fully in-memory store (used where the test itself
* controls all content, e.g. F2/F5/F6).
*/
function createRealDelegatingFs(overrides = {}) {
const base = {
existsSync: (p) => fs.existsSync(p),
mkdirSync: (p, opts) => fs.mkdirSync(p, opts),
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- delegate for a fake-adapter method, not test cleanup
rmSync: (p, opts) => fs.rmSync(p, opts),
readdirSync: (p, opts) => (opts ? fs.readdirSync(p, opts) : fs.readdirSync(p)),
readFileSync: (p, encoding) => (encoding ? fs.readFileSync(p, encoding) : fs.readFileSync(p)),
writeFileSync: (p, data, opts) => fs.writeFileSync(p, data, opts),
copyFileSync: (src, dest) => fs.copyFileSync(src, dest),
cpSync: (src, dest, opts) => fs.cpSync(src, dest, opts),
lstatSync: (p) => fs.lstatSync(p),
realpathSync: (p) => fs.realpathSync(p),
unlinkSync: (p) => fs.unlinkSync(p),
rmdirSync: (p) => fs.rmdirSync(p),
symlinkSync: (target, p) => fs.symlinkSync(target, p),
readlinkSync: (p) => fs.readlinkSync(p),
openSync: (p, flags) => fs.openSync(p, flags),
readSync: (fd, buffer, offset, length, position) => fs.readSync(fd, buffer, offset, length, position),
closeSync: (fd) => fs.closeSync(fd),
};
return { ...base, ...overrides };
}
describe('installRuntimeArtifacts — F4: adapter errors propagate, cleanup still runs', () => {
test('adapter errors propagate, cleanup still runs', (t) => {
const configDir = createTempDir('gsd-f4-augment-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
let capturedCleanupDir;
const fakeFs = createRealDelegatingFs({
writeFileSync: (p, data, opts) => {
if (String(p).includes('gsd-cmd-rewrites-') && capturedCleanupDir === undefined) {
capturedCleanupDir = path.dirname(p);
}
fs.writeFileSync(p, data, opts);
},
copyFileSync: (src, dest) => {
const err = new Error(`EACCES: permission denied, copyfile '${src}' -> '${dest}'`);
err.code = 'EACCES';
throw err;
},
});
assert.throws(
() => installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_FULL, TEST_ATTRIBUTION, undefined, { fs: fakeFs }),
(err) => err.code === 'EACCES',
'F4: an EACCES from the injected adapter mid-copy must propagate to the caller unchanged, exactly as a real EACCES would today',
);
assert.ok(capturedCleanupDir, 'F4 test precondition: the commands kind rewrite must have run before the copy failure');
assert.strictEqual(
fs.existsSync(capturedCleanupDir), false,
'F4: cleanup must still run (the finally block) even though the copy step threw',
);
});
});
describe('installRuntimeArtifacts — F5: fake existsSync drives the same branch', () => {
test('fake existsSync drives the same branch', () => {
const configDir = path.join(os.tmpdir(), `gsd-f5-must-not-exist-${crypto.randomUUID()}`);
const skillsDest = path.join(configDir, 'skills');
// Seed ONLY the skills destDir as a pre-existing (empty) directory in the
// fake store — configDir is never created for real, so existsSync(dest)
// reports true purely because the FAKE says so, driving the exact same
// `kind.kind === 'skills' && installFs().existsSync(dest)` pre-existing-
// dest branch a real pre-existing dir would take.
const fakeFs = createFakeInstallFs([[skillsDest, { type: 'dir' }]]);
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE, undefined, undefined, { fs: fakeFs });
assert.notStrictEqual(result, undefined, 'F5: must still return a plan');
const skillsKind = result.kinds.find((k) => k.kind === 'skills');
assert.ok(skillsKind, 'F5 precondition: claude global writes a skills kind');
assert.strictEqual(skillsKind.destDir, skillsDest);
assert.deepStrictEqual(
skillsKind.preserved, [],
'F5: the branch ran off the fake\'s existsSync=true, found an empty pre-existing dir, and preserved ' +
'nothing — the same outcome the real existsSync-true branch produces for an empty pre-existing dir',
);
});
});
describe('installRuntimeArtifacts — F6: incomplete adapter fails loudly, never silently falls back to real fs', () => {
// #2875 REVERSES this row's earlier pinned contract ("falls back to real
// fs, never silently no-ops"). That contract was itself the defect
// buildGuardedAdapter closes (install-fs-adapter.cts's "PARTIAL-ADAPTER
// TRAP" doc comment): merging an injected partial OVER the real adapter
// meant any method the partial omitted was silently REAL `node:fs` — e.g.
// user-artifact-staging.cts's `stageUserArtifacts` calling
// `installFs().rmSync(entryDir)` unconditionally, where a test fake
// missing `rmSync` would silently delete the real
// `<configDir>/.gsd-staging/<key>` on disk. Falling through to real fs is
// exactly how a fake-adapter test can end up performing real, uncontrolled
// IO — the bug, not a feature. The guarded contract instead throws
// immediately, naming the missing method, the moment the exercised path
// reaches it: never a silent no-op AND never a silent real-fs write.
test('incomplete adapter fails loudly (never silently skips the write)', () => {
const configDir = path.join(os.tmpdir(), `gsd-f6-must-not-exist-${crypto.randomUUID()}`);
// Deliberately incomplete: only mkdirSync is implemented, to prove the
// FIRST other method the call path reaches throws immediately instead of
// silently degrading to real fs or a no-op.
const incompleteFs = {
mkdirSync: () => undefined,
};
assert.throws(
() => installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE, undefined, undefined, { fs: incompleteFs }),
(err) => /does not implement it/.test(err.message) && /PARTIAL-ADAPTER TRAP/.test(err.message),
'F6: an incomplete adapter must fail loudly, naming the missing method, the moment the call path ' +
'reaches a method it does not implement — never silently no-op or fall back to real fs',
);
});
});
// ─── G. Additive contract — G2 ────────────────────────────────────────────────
describe('installRuntimeArtifacts — G2: bin/install.js production call site unchanged', () => {
test('installer call site unchanged', (t) => {
const binInstall = require('../bin/install.js');
const tmpDir = createTempDir('gsd-g2-');
const previousCwd = process.cwd();
process.chdir(tmpDir);
t.after(() => { process.chdir(previousCwd); cleanup(tmpDir); });
const result = binInstall.install(false, 'claude');
assert.strictEqual(
result.runtime, 'claude',
'G2: bin/install.js\'s production call site (6 positional args, no deps) must be unaffected by the new optional deps param',
);
// install(false, ...) is a LOCAL install — claude's local layout writes
// commands+agents, not skills (skills is global-only for claude).
assert.ok(
fs.existsSync(path.join(tmpDir, '.claude', 'commands')),
'G2: the production install must still write commands/ end-to-end',
);
binInstall.uninstall(false, 'claude');
});
});
// ─── H. Security boundaries must NOT move behind the adapter ─────────────────
describe('installRuntimeArtifacts — H1: symlink escape still refuses', () => {
test('symlink escape still refuses', (t) => {
const configDir = createTempDir('gsd-h1-');
const outsideDir = createTempDir('gsd-h1-outside-');
t.after(() => { cleanup(configDir); cleanup(outsideDir); });
sandboxHome(t, configDir);
// Pre-create the skills destDir AS a symlink pointing outside configDir —
// the guard must refuse before mkdirSync ever follows it.
fs.symlinkSync(outsideDir, path.join(configDir, 'skills'), 'dir');
assert.throws(
() => installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE),
/GSD_ALLOW_SYMLINKED_DEST/,
'H1: a destDir that is itself a symlink pointing outside the install root must be refused',
);
});
});
describe('installRuntimeArtifacts — H2: opt-in still follows', () => {
test('opt-in still follows', (t) => {
const configDir = createTempDir('gsd-h2-');
const outsideDir = createTempDir('gsd-h2-outside-');
t.after(() => { cleanup(configDir); cleanup(outsideDir); });
sandboxHome(t, configDir);
fs.symlinkSync(outsideDir, path.join(configDir, 'skills'), 'dir');
const savedOptIn = process.env.GSD_ALLOW_SYMLINKED_DEST;
process.env.GSD_ALLOW_SYMLINKED_DEST = '1';
t.after(() => {
if (savedOptIn === undefined) delete process.env.GSD_ALLOW_SYMLINKED_DEST;
else process.env.GSD_ALLOW_SYMLINKED_DEST = savedOptIn;
});
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
assert.notStrictEqual(result, undefined, 'H2: opt-in must still succeed and return a plan');
const skillsKind = result.kinds.find((k) => k.kind === 'skills');
assert.ok(skillsKind, 'H2 precondition: claude global writes a skills kind');
assert.ok(
fs.readdirSync(outsideDir).length > 0,
'H2: with the opt-in set, writes must actually follow the symlink into outsideDir',
);
});
});
describe('installRuntimeArtifacts — H3: fake adapter cannot bypass the symlink guard', () => {
test('fake adapter cannot bypass the symlink guard', () => {
// hasExistingSymlinkBetween's path-traversal refusal (install-engine.cts,
// part (a) of the guard: "resolvedFullPath !== resolvedRoot &&
// !resolvedFullPath.startsWith(resolvedRoot + path.sep)") is PURE PATH
// MATH — path.resolve/startsWith on strings, no fs call at all. Pin that
// invariant directly: even a fake adapter that lies "nothing exists,
// nothing is a symlink" everywhere cannot make this refusal pass for an
// escaping path, because this branch never asks the adapter anything.
const root = path.join(os.tmpdir(), 'gsd-h3-fake-root');
const escapingPath = path.join(root, '..', '..', 'etc', 'passwd');
const lyingFs = {
existsSync: () => false,
lstatSync: () => {
throw new Error('H3: lstatSync must never be reached — the path-traversal refusal is pure path math');
},
realpathSync: (p) => p,
};
const refused = withInstallFs(lyingFs, () => hasExistingSymlinkBetween(root, escapingPath));
assert.strictEqual(
refused, true,
'H3: a fake adapter reporting "nothing exists, nothing is a symlink" must not be able to certify ' +
'an install the real filesystem would refuse — the path-traversal decision does not consult the ' +
'adapter at all',
);
});
});
describe('installRuntimeArtifacts — H4: dest confinement still enforced', () => {
test('dest confinement still enforced', () => {
assert.throws(
() => runtimeArtifactInstallPlan.assertDestWithinConfigHome('/fake/config/home', '../../etc'),
/escapes configHome|strict subpath/i,
'H4: assertDestWithinConfigHome must still throw for a destSubpath escaping configHome',
);
});
});
describe('installRuntimeArtifacts — H5: nul byte in dest is rejected', () => {
test('nul byte in dest is rejected', () => {
assert.throws(
() => runtimeArtifactInstallPlan.assertDestWithinConfigHome('/fake/config/home', 'skills\0evil'),
/NUL/,
'H5: assertDestWithinConfigHome must still throw for a destSubpath containing a NUL byte',
);
});
});
// ─── I. Cleanup visibility ─────────────────────────────────────────────────
describe('installRuntimeArtifacts — I1: successful cleanup is recorded', () => {
test('successful cleanup is recorded', (t) => {
const configDir = createTempDir('gsd-i1-augment-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_FULL, TEST_ATTRIBUTION);
assert.ok(result.cleanup.length > 0, 'I1: augment install must produce at least one cleanupDirs entry to prove this row');
for (const entry of result.cleanup) {
assert.strictEqual(typeof entry.dir, 'string');
assert.strictEqual(entry.ok, true, `I1: successful cleanup entries must record ok:true (dir=${entry.dir})`);
assert.strictEqual(fs.existsSync(entry.dir), false, 'I1: a successfully cleaned dir must no longer exist on disk');
}
});
});
describe('installRuntimeArtifacts — I2: failed cleanup is visible, not silent', () => {
test('failed cleanup is visible, not silent', (t) => {
const configDir = createTempDir('gsd-i2-augment-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const fakeFs = createRealDelegatingFs({
rmSync: (p, opts) => {
if (String(p).includes('gsd-cmd-rewrites-')) {
throw new Error('I2: simulated cleanup failure');
}
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- delegate, not test cleanup
return fs.rmSync(p, opts);
},
});
const result = installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_FULL, TEST_ATTRIBUTION, undefined, { fs: fakeFs });
assert.notStrictEqual(result, undefined, 'I2: install must still succeed (never fail) even when cleanup throws');
assert.ok(result.cleanup.length > 0, 'I2: augment must have at least one cleanupDirs entry to fail');
assert.ok(
result.cleanup.every((c) => c.ok === false),
'I2: a cleanup rmSync throw must be reported as ok:false on the returned plan, never silently dropped',
);
});
});
describe('installRuntimeArtifacts — I3: no cleanup dirs is an empty array', () => {
test('no cleanup dirs is an empty array', (t) => {
const configDir = createTempDir('gsd-i3-claude-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE);
assert.ok(Array.isArray(result.cleanup), 'I3: cleanup must be an array even when empty');
assert.strictEqual(
result.cleanup.length, 0,
'I3: a claude/core install with no rewritten temp dirs must report an EMPTY cleanup array, not undefined/absent',
);
});
});
describe('installRuntimeArtifacts — I4: stage failure before any item cleans up and throws', () => {
test('stage failure cleans up and throws', (t) => {
const configDir = createTempDir('gsd-i4-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
assert.throws(
() => installRuntimeArtifacts('claude', configDir, 'global', { skills: 123, agents: 123 }),
(err) => err instanceof Error,
'I4: a malformed resolvedProfile that fails the FIRST kind\'s stage() (before any cleanupDirs exist) ' +
'must still surface as a thrown Error, with the (empty) cleanupDirs still swept by the finally block',
);
});
});
describe('installRuntimeArtifacts — I5: rewrite failure mid-plan cleans up and throws', () => {
test('rewrite failure cleans up and throws', (t) => {
const configDir = createTempDir('gsd-i5-augment-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
let capturedCleanupDir;
const fakeFs = createRealDelegatingFs({
mkdirSync: (p, opts) => {
if (String(p).includes('gsd-profile-runtime-skills-')) {
throw new Error('I5: simulated skills-stage failure AFTER commands already rewrote+registered a cleanup dir');
}
fs.mkdirSync(p, opts);
return undefined;
},
writeFileSync: (p, data, opts) => {
if (String(p).includes('gsd-cmd-rewrites-') && capturedCleanupDir === undefined) {
capturedCleanupDir = path.dirname(p);
}
fs.writeFileSync(p, data, opts);
},
});
assert.throws(
() => installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_FULL, TEST_ATTRIBUTION, undefined, { fs: fakeFs }),
/I5: simulated skills-stage failure/,
'I5: a failure in a LATER kind\'s stage step must still propagate as a thrown error',
);
assert.ok(capturedCleanupDir, 'I5 test precondition: the commands kind\'s rewrite dir must have been observed before the skills-stage failure');
assert.strictEqual(
fs.existsSync(capturedCleanupDir), false,
'I5: the EARLIER (successfully rewritten) commands cleanupDir must still be removed by the finally ' +
'block even though a LATER kind\'s stage step failed',
);
});
});
// ─── K. Byte-identical writes — K3 ─────────────────────────────────────────
function walkFilesRecursively(root) {
const out = new Map();
const walk = (relPath, absPath) => {
for (const entry of fs.readdirSync(absPath, { withFileTypes: true })) {
const childRel = relPath ? path.join(relPath, entry.name) : entry.name;
const childAbs = path.join(absPath, entry.name);
if (entry.isDirectory()) walk(childRel, childAbs);
else if (entry.isFile()) out.set(childRel, fs.readFileSync(childAbs));
}
};
if (fs.existsSync(root)) walk('', root);
return out;
}
describe('installRuntimeArtifacts — K3: real install before/after, full recursive diff', () => {
test('writes are byte-identical', (t) => {
for (const runtime of ['claude', 'qwen']) {
const dirA = createTempDir(`gsd-k3-${runtime}-a-`);
const dirB = createTempDir(`gsd-k3-${runtime}-b-`);
t.after(() => { cleanup(dirA); cleanup(dirB); });
sandboxHome(t, dirA);
installRuntimeArtifacts(runtime, dirA, 'global', RESOLVED_FULL);
sandboxHome(t, dirB);
installRuntimeArtifacts(runtime, dirB, 'global', RESOLVED_FULL);
const filesA = walkFilesRecursively(dirA);
const filesB = walkFilesRecursively(dirB);
assert.deepStrictEqual(
[...filesA.keys()].sort(), [...filesB.keys()].sort(),
`K3 (${runtime}): the file sets written by two independent installs must match`,
);
for (const [relPath, contentA] of filesA) {
assert.ok(
contentA.equals(filesB.get(relPath)),
`K3 (${runtime}): ${relPath} content drifted between two independent installs`,
);
}
}
});
});
// ─── L. Property tests ────────────────────────────────────────────────────
describe('installRuntimeArtifacts — L1: plan kinds mirror layout kinds (property)', () => {
test('plan kinds mirror layout kinds', (t) => {
const runtimes = Object.keys(registry.runtimes);
const RUNTIME_ARB = fc.constantFrom(...runtimes);
const SCOPE_ARB = fc.constantFrom('global', 'local');
const observedKindSets = new Set();
const createdDirs = [];
const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE;
t.after(() => {
if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome;
if (savedUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = savedUserProfile;
for (const d of createdDirs) cleanup(d);
});
// Seeded, bounded numRuns, replay data on failure (verbose:true prints
// the failing/shrunk (runtime, scope) pair fast-check found).
fc.assert(
fc.property(RUNTIME_ARB, SCOPE_ARB, (runtime, scope) => {
const configDir = createTempDir(`gsd-l1-${runtime}-`);
createdDirs.push(configDir);
process.env.HOME = configDir;
process.env.USERPROFILE = configDir;
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope);
const expectedKinds = [...new Set(layout.kinds.map((k) => k.kind))].sort();
const plan = installRuntimeArtifacts(runtime, configDir, scope, RESOLVED_CORE);
const actualKinds = [...new Set(plan.kinds.map((k) => k.kind))].sort();
observedKindSets.add(JSON.stringify(actualKinds));
assert.deepStrictEqual(
actualKinds, expectedKinds,
`L1 (${runtime}/${scope}): plan.kinds must be a bijection with layout.kinds — ` +
`plan=${JSON.stringify(actualKinds)} vs layout=${JSON.stringify(expectedKinds)}`,
);
}),
{ numRuns: 30, seed: 2874, verbose: true },
);
// Non-vacuity: the registry has runtimes with empty, single-kind, and
// multi-kind layouts (verified across the whole registry — see this
// row's PR notes) — a generator that only ever produced ONE kind-set
// would be exercising nothing.
assert.ok(
observedKindSets.size > 1,
`L1 non-vacuity: the generator must exercise more than one distinct kind-set — observed only ` +
`${observedKindSets.size} (${[...observedKindSets].join(', ')})`,
);
});
});
describe('installRuntimeArtifacts — L2: plan is deterministic (property)', () => {
function normalizePlanForIdempotence(plan) {
// mkInstallTempDir names every rewrite/staging temp dir with a random hex
// suffix (install-fs-adapter.cts) — expected to differ between two
// independent calls even when everything else about the plan is
// identical. Normalize those away; everything else must match exactly.
const stripTemp = (p) => (typeof p === 'string' && p.startsWith(os.tmpdir()) ? '<TEMP>' : p);
return {
runtime: plan.runtime,
scope: plan.scope,
kinds: plan.kinds.map((k) => ({
kind: k.kind, sourceDir: stripTemp(k.sourceDir), destDir: k.destDir,
preserved: k.preserved, written: k.written,
})),
cleanup: plan.cleanup.map((c) => ({ dir: stripTemp(c.dir), ok: c.ok })),
postSteps: plan.postSteps,
};
}
test('plan is deterministic', () => {
const runtimes = Object.keys(registry.runtimes);
const RUNTIME_ARB = fc.constantFrom(...runtimes);
const SCOPE_ARB = fc.constantFrom('global', 'local');
let hits = 0;
fc.assert(
fc.property(RUNTIME_ARB, SCOPE_ARB, (runtime, scope) => {
// configDir is never created for real — both calls run against fresh,
// independent fake adapters, so no real fs cleanup is needed here.
const configDir = path.join(os.tmpdir(), `gsd-l2-${runtime}-${crypto.randomUUID()}`);
const planA = installRuntimeArtifacts(runtime, configDir, scope, RESOLVED_CORE, undefined, undefined, { fs: createFakeInstallFs() });
const planB = installRuntimeArtifacts(runtime, configDir, scope, RESOLVED_CORE, undefined, undefined, { fs: createFakeInstallFs() });
hits++;
assert.deepStrictEqual(
normalizePlanForIdempotence(planA),
normalizePlanForIdempotence(planB),
`L2 (${runtime}/${scope}): two installs against fresh fake adapters with the same inputs must ` +
'yield structurally identical plans (temp-dir names normalized — see normalizePlanForIdempotence)',
);
}),
{ numRuns: 30, seed: 2874, verbose: true },
);
assert.strictEqual(hits, 30, 'L2 non-vacuity: every generated (runtime, scope) pair must actually have exercised a comparison');
});
});
// ─── readGsdCommandNames — single-source parity ──────────────────────────────
//
// command-roster.cts's readGsdCommandNames reimplements
// scripts/fix-slash-commands.cjs's readCmdNames' directory-scan rule against
// the injectable install-fs seam instead of delegating to it — see
// command-roster.cts's module comment for why (readCmdNames is deliberately
// a zero-dependency standalone CLI/library with no build-order dependency on
// gsd-core/bin/lib, so it cannot itself require the compiled
// install-fs-adapter.cjs). Two implementations of one filtering rule is this
// repo's recorded Generative Fix Divergence class; this test is the
// enforcement the coordinator required in exchange for keeping the
// reimplementation: it fails the moment the two disagree about which stems
// commands/gsd/ contains.
describe('readGsdCommandNames — single-source parity (command-roster.cts vs scripts/fix-slash-commands.cjs)', () => {
test('both implementations report the identical stem set for commands/gsd/', () => {
const fromCommandRoster = [...commandRoster.readGsdCommandNames()].sort();
const fromSlashCommandTransformer = [...slashCommandTransformer.readCmdNames()].sort();
assert.deepStrictEqual(
fromCommandRoster,
fromSlashCommandTransformer,
'command-roster.cts readGsdCommandNames() and scripts/fix-slash-commands.cjs readCmdNames() ' +
'diverged — these are two implementations of the SAME directory-scan rule (Generative Fix ' +
'Divergence); fix the one that is wrong, do not just silence this test',
);
assert.ok(fromCommandRoster.length > 0, 'sanity: commands/gsd/ must contain at least one command');
});
});