Files
msd-core/tests/executed-plan.test.cjs
Tom Boucher 3ab0007164 enh(#2875): materialization primitives — durable user-artifact staging and descriptor-authoritative agents (#3600)
* fix(#2875): stage user artifacts durably across install wipes (#1874-F19)

preserveUserArtifacts held user files only in an in-memory Map across the
wipe, so any process death between preserve and restore lost them outright.

Seven call sites, not the four the issue records. Three of them never called
the helper at all - they open-coded the same read/wipe/write - so searching
for callers under-counted by construction; the extra sites were found by
sweeping for the pattern instead.

The worst is the mainline install path, where the crash window spans the
entire gsd-core tree copy rather than a single rmSync.

Adds src/user-artifact-staging.cts: durable on-disk staging with a record
written after the copies land as the commit point, plus recovery of orphaned
batches on the next run - without recovery the staged bytes survive but the
user's file is still gone, which would pass its own test while delivering
nothing.

Routes copyPreservingSymlink through installFs() so staging cannot bypass the
install fs seam, and reunites its symlink-safety docblock with the function it
documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): amend ADR-3574 with four claims disproved by implementation

Implementing Phase 6 disproved four statements the ADR rests on. The central
decision - no single materializer - is unaffected and stands.

Corrected: decision 3 was already satisfied, so nothing was extracted; the
agents-bypass runtime set omitted claude, kilo and opencode, and closing it
needed three new pieces of descriptor contract rather than proceeding on its
own terms; three of the four blockers the layout comment names were already
stale; and F19 is seven call sites, not four.

Records the generalizable lesson: the defect is the pattern of holding user
data in memory across a wipe, not the helper, so searching for callers of the
helper under-counts by construction.

Also resolves the ADR's open question on USER_OWNED_ARTIFACTS membership, and
notes that copyPreservingSymlink needed routing through the install fs seam
before it could be reused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): close dangling-symlink blind spot and harden staging recovery

An adversarial review found the F19 staging work shipped red and unsafe.

Root cause, shared by two arbitrary-write findings: hasExistingSymlinkBetween
missed dangling symlinks in both its root check and its per-segment walk,
because it probed with existsSync, which is false for a link whose target does
not exist. Fixing only the new module would have reused a guard that was
itself blind. This guard protects the whole install tree.

Recovery no longer throws: it degrades per entry and per file, so one bad
batch cannot block the others. Previously an unrecoverable entry propagated
out of the first statement of install and uninstall, before the cleanup that
would have removed it - wedging the installer permanently.

Partial fs adapters now throw on any omitted method instead of silently
reaching the real filesystem, closing the trap that let a test poison list
pass while real IO happened.

Staged names must be flat, recovery refuses a dangling destination symlink,
and a batch whose recovery genuinely failed is no longer swept - it was
discarding the only durable copy of the file it had just failed to restore.

Replaces three tests that could not fail, including the one labelled negative
proof.

Known limitation, documented not closed: concurrent installs sharing a staging
key can still lose a batch. A real fix needs a cross-process lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* enh(#2875): make the descriptor authoritative for the agents kind

Deletes the inline agent-staging loop in bin/install.js and the
_DESCRIPTOR_AGENTS_RUNTIMES set, so every runtime materializes agents from
its capability descriptor instead of an inline hostBehaviors dispatch.

Closing it needed three pieces of contract the descriptor pipeline never had,
all reducible to one missing input - per-agent resolution context: a
frontmatter-extensions step for claude's effort and disallowedTools, per-agent
model-override resolution for kilo and opencode, and a named branding
converter for hermes, whose rewrite data was already declared.

Seven runtimes were on the loop, not the six the design recorded - kimi-code
was found by a golden fixture, not by analysis. claude-local and kimi-code
both silently lost their agents mid-change; the fixtures caught both and the
cause was fixed rather than the fixtures regenerated.

A parity harness gates the migration: both pipelines over identical inputs,
byte-identical output including filenames, per runtime. It is demonstrated
red before being trusted. Surface and install paths converge for all seven,
which also fixes surface previously writing no agents for these runtimes.

Codex's config.toml strip stays put - it mutates host config, which no
descriptor kind models.

Also routes install-model-override-resolver and install-effort-resolver
through the install fs seam. Both leaked real filesystem IO from the install
call tree; the stricter adapter is what exposed them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): record the agents-descriptor migration and correct the ADR count

The _DESCRIPTOR_AGENTS_RUNTIMES allow-list no longer exists, so the host
integration guide told readers to join a set that is gone. Replaces that with
what is now true - declare an agents entry and it installs, on the surface
path as well as install - and points anyone needing a per-agent transform at
the three extension points rather than at a new inline branch.

Corrects the ADR amendment: seven runtimes were on the inline loop, not six.
kimi-code was found by a golden fixture going red, not by reading. That is the
third short count this phase, all from enumerating by symbol or set membership
when the thing that matters is a behavior.

Adds the Changed changeset for the surface-path convergence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): amend ADR-2866 - claude global always wrote agents on disk

The claude row's global=[skills] described what capability.json declared, not
what the installer wrote. bin/install.js's inline agent-staging loop was never
scope-gated and never consulted the descriptor, so a claude --global install
has always written agents/gsd-*.md.

Phase 6 closes the gap by deleting that loop and declaring agents on claude's
descriptor at global scope. On-disk bytes are unchanged - the golden fixtures
did not move, which is the evidence that the descriptor, not the installer,
was incomplete.

#2218 is unaffected: agents are not trigger-bearing, so the wider row does not
introduce a new shadowing case.

Records the warning that an incomplete descriptor is invisible while a second
code path silently does its work, and only surfaces when the two are forced
into agreement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): close review findings across staging, agents and the parity harness

Two independent reviews of this branch found defects the local gates missed.

Security: a dangling symlink at a migration destination allowed writing
outside configDir - the same class this change claimed to close, missed at the
terminal write of the flow being added. The staging-root resolver threw as the
first statement of install and uninstall, so a hostile symlink bricked both,
and symlinked-configDir users lost uninstall as well as install; it now
degrades instead of aborting. Recovery gained a source-side symlink check and
now refuses a relative destDir, which resolved against cwd. Converter dispatch
gained a runtime allowlist - lint-time validation stopped mattering once this
branch promoted that dispatch from the surface path to real installs.

Correctness: claude --local --minimal exited 1 because the minimal profile
legitimately yields zero agents and the new path treated that as a failure.
cline --local silently lost its agents - its descriptor declared none while
the deleted loop wrote them unconditionally. The agents prune was widened to
any gsd-* entry and destroyed user files it never owned.

The parity harness, on which the migration's safety argument rested, drove a
synthetic registry and never byte-compared the shipped descriptors; two of its
trap rows could not fail. It now drives the real registry across 13
runtime-scope rows including kimi-code and cline-local, and its red-proof is
demonstrated by corrupting a live capability.json. Three goldens that had
encoded the cline regression as expected behavior were corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): close findings from both mandated review engines

/security-review found the staging source-side walk honouring
GSD_ALLOW_SYMLINKED_DEST, an opt-in documented as relaxing only the write
destination. A symlinked files/ component dereferenced because
copyPreservingSymlink lstats the leaf only, so an intermediate link is
followed. The source walk no longer honours the opt-in; the destination check
still does.

/code-review spec axis found this branch had reintroduced its own bug:
migrateLegacyDevPreferencesToSkill's new symlink refusal threw unguarded after
the legacy dir was wiped and before the staged batch was restored, so a
planted symlink bricked uninstall permanently and orphaned the batch. Refusal
kept, abort removed.

kimi-code local silently lost its agents, the same class as the cline bug, and
the parity harness recorded that exclusion as intentional - the third test in
this branch to pin a regression as correct.

--minimal now creates an empty agents/ dir that never existed. Behaviour
restored rather than softening the changeset, so its byte-identical claim
stays true.

Standards axis: try/finally removed from twelve test bodies, fast-check
properties added for parseOwnerPid, boundary coverage at the grace window and
the ancestor-probe depth, a parity assertion for the staging-root helper
duplicated across two files, and the 8-deep config walk deduplicated.

Records 60-review.json with every finding and disposition from five passes,
including the smells left unfixed and why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): prune stale agents unconditionally in minimal mode

The previous round stopped an empty agents/ directory being created when the
resolved profile yields no agents. That was implemented by skipping the agents
kind entirely, which also skipped its stale-agent prune - so a full to minimal
downgrade left stale gsd-* agents behind.

The deleted inline loop pruned unconditionally and only skipped writing. Those
are three separate conditions, not one: prune always, write only when there is
something to write, create the directory only when writing.

Both call sites now run _removeGsdEntries before the empty-staged early exit.
The symlink-escape guard moved with it, since the prune also touches dest.
Codex .toml agents and the config.toml stanzas are cleaned again, and
user-owned agents are still preserved.

The agents/ directory is left in place after a prune empties it, matching
every sibling kind - none of them remove the destination directory itself.

Golden fixtures confirmed byte-identical: the prune is a no-op on a fresh
install, so fixture generation is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): document interrupted-install recovery for user-owned files

The durable-staging fix is invisible to the user it protects. Someone whose
install died mid-flight has no way to know USER-PROFILE.md was staged before
the delete, that the next run restores it, or that recovery happens at the
start of that run rather than in the background.

Written as the task the user has - finish the interrupted command - rather
than as a description of the mechanism, and states what it will not do:
overwrite a file already present, or touch staging belonging to another
install still running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#2875): backfill changeset pr number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#2875): assert the J8 model override without building a regex

CodeQL flagged incomplete string escaping: the assertion interpolated the
override value into a RegExp while escaping only forward slashes, which is
meaningless in a constructor, leaving real metacharacters unescaped.

The failure direction was the dangerous one - a metacharacter would have made
the match more permissive, so the row would pass when it should fail. That
matters here because J8 exists precisely because an earlier revision was a
tautology; the rewrite reintroduced a different way for the same assertion to
stop discriminating.

Replaced with a line-wise exact match, so no regex is constructed at all.
Swept the other test files this branch adds; no sibling instances.

lint:ci passed on the original - lint-no-adhoc-regex-escape matches a full
metachar-escape copy, so a single slash replace slipped under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:25:53 -04:00

1328 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.
* Mirrors tests/install-runtime-artifacts.test.cjs's sandboxHome().
*/
function sandboxHome(t, dir) {
const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE;
process.env.HOME = dir;
process.env.USERPROFILE = dir;
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;
});
}
// ─── 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');
});
});