Files
msd-core/tests/executed-plan.test.cjs
Tom Boucher a4a02a7a01 enhance(#2874): return the executed plan and route install IO through a seam (#3568)
* test(#2874): add failing-first gate for the executed-plan return

Four rows from the matrix's red-first order. E3 pins the one early return,
for the opencode family, where a void-shaped hole would otherwise survive
unnoticed. E13 sweeps every runtime in the registry - enumerated from the
registry rather than hardcoded, so a runtime added later cannot slip past.
F2 proves absence of real filesystem contact rather than merely that the
happy path ran, which is the difference between a complete seam and a
partial one.

G1 and G3 are the additive guard and must be green before and after. G3
deliberately leaves the two existing adapter test doubles untouched: if
this change required editing them it would not be additive, and the
acceptance criterion would be unmet.

No production code. All 19 runtimes install without throwing today, so
E3 and E13 fail on the undefined comparison alone.

Refs #2874

* feat(#2874): return the executed plan and route install IO through a seam

installRuntimeArtifacts returned void, so its correctness was observable
only by re-reading disk. It now returns what it executed - per kind, per
scope - including on the combinedFamilyInstall path, which was the one
early return where a void-shaped hole would have survived unnoticed.

Failure still throws rather than becoming an ok:false return, so control
flow is unchanged for both existing callers. A best-effort cleanup that
fails is still swallowed, but is now visible in the returned value rather
than silently absent.

The fs seam is ambient rather than threaded. Explicit deps through
install-profiles and the 3000-line conversion module was impractical; the
tradeoff, the synchronous-only re-entrancy assumption, the restore
guarantee and the partial-adapter fallback trap are all documented at the
seam. findInstallSourceRoot and its sibling stay unrouted by design -
they locate the package's own source, not the install destination.

readCmdNames keeps a second implementation because the standalone CLI
that owns the original cannot require the compiled adapter without a
build-order dependency on its own output. A parity test fails if the two
ever disagree.

Refs #2874

* chore(#2874): gitignore the new build artifact

install-fs-adapter.cjs is tsc output from src/install-fs-adapter.cts, not
a tracked source file. It was added to eslint's ignore list but not to
.gitignore, so it landed as a tracked file - the third time this step of
the new-.cts ripple has been missed on this epic.

Refs #2874

* fix(#2874): close two seam leaks and correct a false comment

A correctness review found the seam still leaked in two places, both
subtler than the three already closed.

readGsdCommandNames was routed when it should not have been: it reads the
package's own commands directory, which a destination-fake is never
seeded with, so under a fake adapter it returned an empty or wrong roster
instead of failing loudly. It now reads real fs, matching the precedent
already documented for findInstallSourceRoot.

cleanupStagedSkills ran raw rmSync from a process exit handler, which is
real filesystem work deferred past the point where withInstallFs has
restored - the one thing the synchronous-only contract exists to
exclude. Staging now captures the adapter that created each directory and
cleanup replays it, so a real install cleans up exactly as before and a
fake-staged path never reaches the real filesystem.

Also corrected a comment claiming the migration reads were an unrouted,
untested residual gap. They are routed and exercised; a comment
understating the seam is as corrosive as one overstating it in a module
whose trust rests on being honestly documented.

Refs #2874

* test(#2874): migrate the exemplar group and cover the matrix

AC3's exemplar migration lands in place: the qwen install group now
asserts skills and agents destinations from the returned plan in one
deepStrictEqual instead of probing the filesystem for each.

Nine facts the old probes established were enumerated first. Two moved to
the value assertion; seven were retained deliberately - per-file SKILL.md
existence, the VERSION file written outside this function, the manifest
content, and the post-uninstall absence checks all sit outside the plan's
per-kind contract. A migration that quietly asserts less looks like a win
and is a regression, so the enumeration is the guard rather than the
line count.

Also implements the rest of the matrix: the executed-plan shape, adapter
failure modes, the security-boundary rows including a fake that cannot
certify an install the real filesystem would refuse, cleanup visibility,
and two seeded property tests. Only the two external CI gates are left
unticked, because self-certifying them would be a claim rather than a
check.

Refs #2874

* fix(#2874): restore streaming hashes and derive F2 from the boundary rule

The checkpoint found three things reasoning had missed.

sha256File had been converted from raw-fd streaming to a single
readFileSync on the assumption that GSD artifacts are never large. A test
named for exactly that contract already existed and went red. Streaming is
restored, now routed through the adapter, which gains openSync, readSync
and closeSync. The contract was the specification; the assumption was not.

Three existing tests inject faults by monkeypatching real fs. They broke
because mkInstallTempDir stopped calling real mkdtempSync, not because of
any binding subtlety - the real adapter was already late-bound. It now
calls the real function when no fake is injected, so a monkeypatch applied
after import is still seen and the additive contract holds.

F2 poisoned real fs by method, so a deliberately unrouted package-source
read failed a correct design. It now poisons by path: destination IO is
forbidden, package-source IO is allowed and positively asserted. The claim
was always zero real destination IO, and the test now derives from that
rule instead of coincidentally matching it.

Refs #2874

* docs(#2874): add the contributor how-to for plan-based test migration

The phase gate caught a real gap. The docs plan was Reference plus
Explanation only, and every CI check would have passed, because the
docs-required lint only verifies that some file under docs/ moved.

But this phase exists to demonstrate a pattern for follow-on work, and
that work is other contributors migrating probing test groups. The
sequence has two live traps - a partial fake silently falls back to real
fs, and the seam is ambient and synchronous-only - plus one discipline
nobody infers: enumerate the facts before converting, or you assert less
and call it a win.

The page carries the qwen migration's arithmetic, nine facts enumerated
and only two converted, because a reader seeing only the diff would
reasonably conclude the pattern is to replace probes wholesale.

No locale mirrors: none of the four carries any contributor-only how-to,
so a single translated file would manufacture parity rather than provide
it.

Refs #2874

* chore(#2874): backfill changeset pr number

* test(#2874): normalize both sides of the G1 tree comparison

G1 failed on Windows only, deterministically on both shards. The defect
was in the test helper, not production.

_computePathPrefix posix-normalizes the resolved config dir
unconditionally, so on Windows the path embedded in every emitted
SKILL.md body is forward-slash form. hashDirTree stripped against the raw
backslash path from mkdtempSync, so the substring never matched and each
install's unique temp suffix stayed baked into every file - all fifteen
skill bodies hashed differently for two runs that had written identical
bytes.

Both sides are now normalized unconditionally rather than gated on
path.sep, matching the rule this repo already records: backslash paths
arrive on Linux too.

Production code is untouched and was verified correct. Normalizing this
away on the production side would have hidden a real portability bug if
one had existed.

Refs #2874

---------

Co-authored-by: sim <sim@local>
2026-08-16 02:48:24 -04:00

1286 lines
58 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',
);
assert.deepStrictEqual(result.kinds.map((k) => k.kind).sort(), ['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 ───────────────────────────────────────────────
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 = {
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 falls back to real fs, never silently no-ops', () => {
test('incomplete adapter fails loudly (never silently skips the write)', (t) => {
// install-fs-adapter.cts's documented PARTIAL-ADAPTER TRAP: withInstallFs
// merges the injected partial OVER the real adapter — any method the
// partial omits resolves to REAL node:fs, silently. This pins the
// dangerous alternative it guards against: an omitted method must never
// degrade into a silent no-op (skipping the operation, pretending
// success) — it must actually execute, for real.
const configDir = createTempDir('gsd-f6-claude-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
let realMkdirCalls = 0;
// Deliberately incomplete: only mkdirSync is overridden (to prove this
// partial is genuinely merged over real fs, not a full copy of it) —
// every other method (existsSync/readdirSync/writeFileSync/
// copyFileSync/cpSync/...) is omitted entirely.
const incompleteFs = {
mkdirSync: (p, opts) => { realMkdirCalls++; return fs.mkdirSync(p, opts); },
};
const result = installRuntimeArtifacts('claude', configDir, 'global', RESOLVED_CORE, undefined, undefined, { fs: incompleteFs });
assert.notStrictEqual(result, undefined, 'F6: an incomplete adapter must not silently produce no result');
assert.ok(realMkdirCalls > 0, 'F6 test precondition: mkdirSync must have been called');
const skillsKind = result.kinds.find((k) => k.kind === 'skills');
assert.ok(skillsKind, 'F6 precondition: claude global writes a skills kind');
assert.ok(
fs.existsSync(skillsKind.destDir) && fs.readdirSync(skillsKind.destDir).length > 0,
'F6: every method the incomplete adapter omitted fell back to REAL fs and actually wrote real ' +
'content — an incomplete fake never silently no-ops the operations it does not implement',
);
});
});
// ─── 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 = {
rmSync: (p, opts) => {
if (String(p).includes('gsd-cmd-rewrites-')) {
throw new Error('I2: simulated cleanup failure');
}
// This is a fake fs-adapter METHOD delegating to real fs for paths
// it does not intentionally poison, not a test's own directory-
// cleanup call (which still goes through t.after(() => cleanup(...))
// above).
// 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 = {
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');
});
});