feat(#1138): make runtime descriptors authoritative (#1157)

This commit is contained in:
Tom Boucher
2026-06-13 01:49:25 -04:00
committed by GitHub
parent bd349aa88e
commit aec3374bc2
40 changed files with 696 additions and 154 deletions

View File

@@ -197,11 +197,14 @@ function capturePhaseComplete(cwd, phaseNum) {
// directly and redirect output capture.
const chunks = [];
const origWrite = process.stdout.write.bind(process.stdout);
const origErrWrite = process.stderr.write.bind(process.stderr);
process.stdout.write = (chunk) => { chunks.push(chunk); return true; };
process.stderr.write = () => true;
try {
cmdPhaseComplete(cwd, phaseNum, false);
} finally {
process.stdout.write = origWrite;
process.stderr.write = origErrWrite;
}
return chunks.join('');
}

View File

@@ -109,8 +109,11 @@ describe('backwards-compat: legacy Phase N roadmap entries', () => {
const roadmapPath = path.join(tmpDir, '.planning', 'ROADMAP.md');
const before = fs.readFileSync(roadmapPath, 'utf-8');
// Trigger a load — must not silently migrate the file.
getMilestonePhaseFilter(tmpDir);
// Trigger a load — must not silently migrate the file. The deprecation
// warning is covered by the previous test, so keep this fixture quiet.
captureConsole(() => {
getMilestonePhaseFilter(tmpDir);
});
const after = fs.readFileSync(roadmapPath, 'utf-8');
assert.equal(after, before, 'ROADMAP.md must not be rewritten during load');

View File

@@ -225,5 +225,20 @@ describe('bug-131: runNpm isolates HOME from the caller environment', () => {
typeof env.npm_config_userconfig === 'string' && env.npm_config_userconfig.startsWith(env.HOME),
`npm_config_userconfig ${env.npm_config_userconfig} should be under isolated HOME ${env.HOME}`,
);
assert.equal(
env.npm_config_loglevel,
'error',
'isolatedNpmEnv() should suppress npm notice/warn chatter in test gates',
);
assert.equal(
env.npm_config_update_notifier,
'false',
'isolatedNpmEnv() should disable npm update-notifier notices in test gates',
);
assert.equal(
env.NO_UPDATE_NOTIFIER,
'1',
'isolatedNpmEnv() should disable npm update-notifier notices for npm versions that honor NO_UPDATE_NOTIFIER',
);
});
});

View File

@@ -28,7 +28,7 @@ const { execFileSync } = require('child_process');
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
const BUILD_SCRIPT = path.join(__dirname, '..', 'scripts', 'build-hooks.js');
const { install, finishInstall } = require(INSTALL_SRC);
const { cleanup } = require('./helpers.cjs');
const { cleanup, captureConsole } = require('./helpers.cjs');
// ─── Ensure hooks/dist/ is populated before install tests ────────────────────
before(() => {
@@ -63,13 +63,20 @@ describe('#2248: local Claude install does not clobber profile-level statusLine'
// Phase 2: configure settings.local.json (mirrors installAllRuntimes → finalize)
// #338: local Claude installs now write to settings.local.json, not settings.json.
// shouldInstallStatusline=true mirrors what handleStatusline picks for a fresh install
finishInstall(
result.settingsPath,
result.settings,
result.statuslineCommand,
true, // shouldInstallStatusline
'claude',
false // isGlobal=false → local install
const { stdout } = captureConsole(() => {
finishInstall(
result.settingsPath,
result.settings,
result.statuslineCommand,
true, // shouldInstallStatusline
'claude',
false // isGlobal=false -> local install
);
});
assert.match(
stdout,
/Skipping statusLine for local install/,
'Local install must explain that it skipped statusLine unless --force-statusline is passed'
);
// #338: local installs write to settings.local.json, not settings.json

View File

@@ -63,6 +63,8 @@ describe('bug #2554 — getMilestonePhaseFilter decimal phase dirs', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'## Roadmap v1.0: Current',
'',
'### Phase 01: One',
'**Goal:** g',
'',

View File

@@ -15,9 +15,8 @@
* directory already exists with managed-shape content, skip the local copy
* and emit a clear warning explaining the conflict avoidance.
*
* Tests are structural: they assert on the post-install filesystem shape
* (existence and overlap count of typed paths), not on warning-message
* substrings.
* Tests assert on the post-install filesystem shape and capture the skip
* warning so the full test log remains warning-clean.
*/
'use strict';
@@ -28,7 +27,7 @@ const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { createTempDir, cleanup, captureConsole } = require('./helpers.cjs');
const { install } = require('../bin/install.js');
@@ -78,8 +77,12 @@ describe('bug #3037: Gemini global+local install must not create duplicate comma
return out.sort();
}
function runInstall(...args) {
return captureConsole(() => install(...args));
}
test('global install populates HOME/.gemini/commands/gsd', () => {
install(true, 'gemini');
runInstall(true, 'gemini');
const globalCmds = path.join(tmpHome, '.gemini', 'commands', 'gsd');
const files = listCommandFiles(globalCmds);
assert.ok(
@@ -90,14 +93,19 @@ describe('bug #3037: Gemini global+local install must not create duplicate comma
test('local install after global does NOT populate PROJECT/.gemini/commands/gsd (avoids /gsd:* namespace conflict)', () => {
// Step 1: global install
install(true, 'gemini');
runInstall(true, 'gemini');
const globalCmds = path.join(tmpHome, '.gemini', 'commands', 'gsd');
const globalFiles = listCommandFiles(globalCmds);
assert.ok(globalFiles.length > 0, 'precondition: global install must succeed');
// Step 2: local install in a temp project
process.chdir(tmpProject);
install(false, 'gemini');
const { stdout } = runInstall(false, 'gemini');
assert.match(
stdout,
/Skipping commands\/gsd\/ for local install/,
'local install must explain why it skips duplicate Gemini commands'
);
// Assertion: the local commands/gsd/ directory must NOT exist (or must
// be empty) so Gemini's conflict detection has nothing to rename. The
@@ -117,7 +125,7 @@ describe('bug #3037: Gemini global+local install must not create duplicate comma
// No global install first — local should proceed normally so users who
// only ever run --local still get GSD commands in their project.
process.chdir(tmpProject);
install(false, 'gemini');
runInstall(false, 'gemini');
const localCmds = path.join(tmpProject, '.gemini', 'commands', 'gsd');
const localFiles = listCommandFiles(localCmds);
@@ -145,7 +153,7 @@ describe('bug #3037: Gemini global+local install must not create duplicate comma
);
process.chdir(tmpProject);
install(false, 'gemini');
runInstall(false, 'gemini');
const localCmds = path.join(tmpProject, '.gemini', 'commands', 'gsd');
const localFiles = listCommandFiles(localCmds);
@@ -171,7 +179,7 @@ describe('bug #3037: Gemini global+local install must not create duplicate comma
);
process.chdir(tmpProject);
install(false, 'gemini');
runInstall(false, 'gemini');
const localCmds = path.join(tmpProject, '.gemini', 'commands', 'gsd');
const localFiles = listCommandFiles(localCmds);

View File

@@ -38,6 +38,7 @@ function run(args, cwd) {
cwd,
timeout: 15000,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
}),
ok: true,
};

View File

@@ -51,6 +51,7 @@ const {
VALID_INSTALL_SURFACES,
VALID_EXTENDED_HOOK_EVENTS,
VALID_PERMISSION_WRITERS,
validateRuntimeCompat,
validateRuntimeBody,
loadCentralConfigKeys,
} = require('../scripts/gen-capability-registry.cjs');
@@ -98,6 +99,11 @@ describe('UI pilot capability', () => {
// capabilities.ui exists
assert.ok(registry.capabilities.ui, 'registry.capabilities.ui should exist');
assert.strictEqual(registry.version, SCHEMA_VERSION);
assert.deepStrictEqual(
registry.capabilities.ui.runtimeCompat,
{ supported: ['*'], unsupported: [] },
'feature runtime compatibility contract should be preserved in the registry',
);
// bySkill maps ui-phase and ui-review to 'ui'
assert.strictEqual(registry.bySkill['ui-phase'], 'ui');
@@ -200,6 +206,32 @@ describe('validateCapability adversarial cases', () => {
assert.ok(errors.some((e) => e.includes('role')));
});
test('feature capability without runtimeCompat is rejected', () => {
const cap = { ...UI_CAP };
delete cap.runtimeCompat;
const errors = validateCapability(cap, 'ui');
assert.ok(
errors.some((e) => e.includes('runtimeCompat')),
'Expected missing runtimeCompat validation error, got: ' + JSON.stringify(errors),
);
});
test('runtimeCompat.supported must be a non-empty array', () => {
const errors = validateRuntimeCompat('ui', { supported: [], unsupported: [] });
assert.ok(
errors.some((e) => e.includes('runtimeCompat.supported')),
'Expected supported-array validation error, got: ' + JSON.stringify(errors),
);
});
test('runtimeCompat.supported wildcard cannot be mixed with runtime ids', () => {
const errors = validateRuntimeCompat('ui', { supported: ['*', 'claude'], unsupported: [] });
assert.ok(
errors.some((e) => e.includes('wildcard')),
'Expected wildcard validation error, got: ' + JSON.stringify(errors),
);
});
test('bad tier enum rejected', () => {
const cap = { ...UI_CAP, tier: 'premium' };
const errors = validateCapability(cap, 'ui');
@@ -284,6 +316,40 @@ describe('validateCrossCapability adversarial cases', () => {
assert.ok(errors.some((e) => e.includes('nonexistent-cap')));
});
test('runtimeCompat explicit runtime ids must exist', () => {
const cap = {
...UI_CAP,
runtimeCompat: { supported: ['claude', 'future-runtime'], unsupported: [] },
};
const runtime = {
id: 'claude',
role: 'runtime',
title: 'Claude',
description: 'Runtime fixture.',
tier: 'core',
requires: [],
runtime: {
configHome: { kind: 'dot-home', name: '.claude', env: ['CLAUDE_CONFIG_DIR'] },
configFormat: 'settings-json',
artifactLayout: { global: [], local: [] },
commandStyle: 'slash-hyphen',
hooksSurface: 'settings-json',
sandboxTier: 'none',
supportTier: 1,
installSurface: 'settings-json',
writesSharedSettings: true,
permissionWriter: null,
extendedHookEvents: [],
},
};
const capMap = new Map([['ui', cap], ['claude', runtime]]);
const errors = validateCrossCapability(capMap, new Set());
assert.ok(
errors.some((e) => e.includes('runtimeCompat.supported') && e.includes('future-runtime')),
'Expected unknown runtimeCompat runtime error, got: ' + JSON.stringify(errors),
);
});
test('requires cycle rejected', () => {
const capA = { ...UI_CAP, id: 'cap-a', tier: 'standard', requires: ['cap-b'] };
const capB = {
@@ -382,6 +448,7 @@ describe('topological step ordering', () => {
title: 'Consumer',
tier: 'full',
requires: [],
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [],
agents: [],
hooks: [],
@@ -1260,6 +1327,7 @@ describe('S1: fragment.path traversal guard', () => {
description: 'Synthetic fixture for fragment path materialization.',
tier: 'full',
requires: [],
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [],
agents: [],
hooks: [],
@@ -1304,6 +1372,7 @@ describe('S1: fragment.path traversal guard', () => {
description: 'Synthetic fixture for step fragment materialization.',
tier: 'standard',
requires: [],
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [],
agents: ['gsd-phase-researcher'],
hooks: [],
@@ -2674,6 +2743,7 @@ function makeCommandCap(id, commands) {
description: 'Synthetic capability for ADR-959 command tests.',
tier: 'full',
requires: [],
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [],
agents: [],
hooks: [],

View File

@@ -1137,7 +1137,7 @@ describe('getMilestonePhaseFilter', () => {
test('handles letter-suffix phases (e.g. 3A)', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'### Phase 3A: Sub-feature\n**Goal:** Sub work\n'
'## Roadmap v1.0: Current\n\n### Phase 3A: Sub-feature\n**Goal:** Sub work\n'
);
const filter = getMilestonePhaseFilter(tmpDir);
@@ -1150,7 +1150,7 @@ describe('getMilestonePhaseFilter', () => {
test('handles decimal phases (e.g. 5.1)', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'### Phase 5: Main\n**Goal:** Main work\n\n### Phase 5.1: Patch\n**Goal:** Patch work\n'
'## Roadmap v1.0: Current\n\n### Phase 5: Main\n**Goal:** Main work\n\n### Phase 5.1: Patch\n**Goal:** Patch work\n'
);
const filter = getMilestonePhaseFilter(tmpDir);
@@ -1163,7 +1163,7 @@ describe('getMilestonePhaseFilter', () => {
test('returns false for non-phase directory names', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'### Phase 1: Init\n**Goal:** Start\n'
'## Roadmap v1.0: Current\n\n### Phase 1: Init\n**Goal:** Start\n'
);
const filter = getMilestonePhaseFilter(tmpDir);
@@ -1175,7 +1175,7 @@ describe('getMilestonePhaseFilter', () => {
test('phaseCount reflects ROADMAP phase count', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'### Phase 5: Auth\n### Phase 6: Dashboard\n### Phase 7: Polish\n'
'## Roadmap v1.0: Current\n\n### Phase 5: Auth\n### Phase 6: Dashboard\n### Phase 7: Polish\n'
);
const filter = getMilestonePhaseFilter(tmpDir);

View File

@@ -8,6 +8,9 @@ const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const PKG_PATH = path.join(ROOT, 'package.json');
const INSTALL_PATH = path.join(ROOT, 'bin', 'install.js');
const ACTIVE_GUIDANCE_PATHS = [
'docs/contributing/bootstrap.md',
];
function readPackageJson() {
return JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
@@ -45,3 +48,23 @@ test('enhancement #191: installer does not maintain gsd-sdk shim compatibility p
assert.equal(/installSdkIfNeeded\(\{/.test(installJs), false,
'bin/install.js must not run installSdkIfNeeded during installation');
});
test('enhancement #191: root AGENTS.md is not an active source of truth', () => {
assert.equal(
fs.existsSync(path.join(ROOT, 'AGENTS.md')),
false,
'root AGENTS.md must not exist; use CONTEXT.md and docs/adr/ as the repository source of truth',
);
});
test('enhancement #191: active contributor guidance does not reference retired SDK build steps', () => {
for (const relPath of ACTIVE_GUIDANCE_PATHS) {
const body = fs.readFileSync(path.join(ROOT, relPath), 'utf8');
assert.equal(
/\bbuild:sdk\b|\bcd sdk\b|\bsdk\/dist\b|\bsdk\/src\b/.test(body),
false,
`${relPath} must not direct contributors or agents to use the retired SDK package workflow`,
);
}
});

View File

@@ -296,6 +296,9 @@ function runNpm(args, options = {}) {
HOME: _npmIsolatedHome,
npm_config_cache: path.join(_npmIsolatedHome, '.npm'),
npm_config_userconfig: path.join(_npmIsolatedHome, '.npmrc'),
npm_config_loglevel: 'error',
npm_config_update_notifier: 'false',
NO_UPDATE_NOTIFIER: '1',
};
const defaults = {
encoding: 'utf-8',
@@ -326,6 +329,9 @@ function isolatedNpmEnv() {
HOME: _npmIsolatedHome,
npm_config_cache: path.join(_npmIsolatedHome, '.npm'),
npm_config_userconfig: path.join(_npmIsolatedHome, '.npmrc'),
npm_config_loglevel: 'error',
npm_config_update_notifier: 'false',
NO_UPDATE_NOTIFIER: '1',
};
}

View File

@@ -199,6 +199,35 @@ for (const { runtime, scope, skillsSub, prefix } of NEST) {
}
}
});
test(`${runtime}: installed nested skill bodies contain no leaked Claude home paths`, () => {
const skillsDir = path.join(tmpDir, skillsSub);
const leakedPathRegex = /(?:~|\$HOME)\/\.claude\b/g;
const leaks = [];
const scan = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
scan(fullPath);
continue;
}
if (!entry.name.endsWith('.md')) continue;
const relPath = path.relative(skillsDir, fullPath);
const content = fs.readFileSync(fullPath, 'utf8');
const matches = content.match(leakedPathRegex);
if (matches) leaks.push(`${relPath} (${matches.length})`);
}
};
scan(skillsDir);
assert.deepStrictEqual(
leaks,
[],
`${runtime} nested skills must not leak Claude home paths: ${leaks.join(', ')}`,
);
});
});
}

View File

@@ -3395,6 +3395,7 @@ function runPhaseComplete(tmpDir, { phase = '1', tolerateExit = false } = {}) {
cwd: tmpDir,
timeout: 60000,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (err) {
// A signal/timeout kill terminated the process before it finished writing —

View File

@@ -23,8 +23,7 @@
* the STEP-0 golden matches the descriptor exactly and is left unchanged.
*
* Unknown runtime case:
* ALLOWED_RUNTIMES guard throws TypeError BEFORE the descriptor lookup,
* so unknown runtimes still throw:
* Missing runtime descriptors throw the same TypeError as the old table:
* TypeError: Unknown runtime: 'grok' — add to runtime-artifact-layout.cjs table
*/
@@ -33,7 +32,10 @@ const assert = require('node:assert/strict');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const { resolveRuntimeArtifactLayout } = require(
const {
resolveRuntimeArtifactLayout,
resolveRuntimeArtifactLayoutFromRegistry,
} = require(
path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-layout.cjs'),
);
@@ -262,10 +264,10 @@ for (const runtime of RUNTIMES) {
}
// ── Unknown runtime ───────────────────────────────────────────────────────────
// ALLOWED_RUNTIMES guard fires BEFORE descriptor lookup — reproduces old behaviour.
// Missing descriptors reproduce the old loud-fail behaviour.
describe('resolveRuntimeArtifactLayout — unknown runtime (descriptor-driven)', () => {
test('throws TypeError for grok (not in ALLOWED_RUNTIMES)', () => {
test('throws TypeError for grok (no artifact layout descriptor)', () => {
assert.throws(
() => resolveRuntimeArtifactLayout('grok', FAKE_DIR, 'global'),
(err) => {
@@ -291,6 +293,48 @@ describe('resolveRuntimeArtifactLayout — unknown runtime (descriptor-driven)',
});
});
describe('resolveRuntimeArtifactLayout — descriptor-only future runtime', () => {
test('accepts a synthetic descriptor-backed runtime without a parallel allowlist update', () => {
const registry = {
runtimes: {
futurecli: {
runtime: {
artifactLayout: {
global: [
{
kind: 'commands',
destSubpath: 'commands',
prefix: 'gsd-',
nesting: 'flat',
recursive: false,
converter: null,
},
],
local: [],
},
},
},
},
};
const layout = resolveRuntimeArtifactLayoutFromRegistry(
registry,
'futurecli',
FAKE_DIR,
'global',
);
assert.strictEqual(layout.runtime, 'futurecli');
assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.scope, 'global');
assert.strictEqual(layout.kinds.length, 1);
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[0].destSubpath, 'commands');
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
});
});
// ── Scope default ─────────────────────────────────────────────────────────────
// resolveRuntimeArtifactLayout(runtime, configDir) with no scope arg → 'global'.

View File

@@ -10,6 +10,8 @@ const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const {
resolveRuntimeConfigIntent,
resolveInstallPlan,
resolveInstallPlanFromRuntimes,
ALLOWED_CONFIG_RUNTIMES,
INSTALL_SURFACES,
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-config-adapter-registry.cjs'));
@@ -250,3 +252,55 @@ describe('INSTALL_SURFACES export', () => {
assert.deepStrictEqual(new Set(INSTALL_SURFACES), EXPECTED_SURFACES);
});
});
describe('resolveInstallPlan — hooksSurface is descriptor-owned', () => {
test('real descriptor-owned none surface is preserved for opencode and kilo', () => {
assert.strictEqual(resolveInstallPlan('opencode').hooksSurface, 'none');
assert.strictEqual(resolveInstallPlan('kilo').hooksSurface, 'none');
});
test('synthetic descriptor resolves hooksSurface without runtime-name fallback', () => {
const runtimes = {
futurecli: {
runtime: {
installSurface: 'settings-json',
writesSharedSettings: true,
permissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: ['Stop'],
hooksSurface: 'settings-json',
sandboxTier: 'none',
},
},
};
assert.deepStrictEqual(resolveInstallPlanFromRuntimes(runtimes, 'futurecli'), {
runtime: 'futurecli',
installSurface: 'settings-json',
writesSharedSettings: true,
finishPermissionWriter: null,
hookEvents: 'claude',
extendedHookEvents: ['Stop'],
hooksSurface: 'settings-json',
sandboxTier: 'none',
});
});
test('missing hooksSurface fails loudly instead of falling back from runtime name', () => {
const runtimes = {
opencode: {
runtime: {
installSurface: 'settings-json',
writesSharedSettings: true,
permissionWriter: 'opencode',
extendedHookEvents: [],
},
},
};
assert.throws(
() => resolveInstallPlanFromRuntimes(runtimes, 'opencode'),
/runtime\.hooksSurface/,
);
});
});

View File

@@ -24,9 +24,11 @@ const { cleanup } = require('./helpers.cjs');
const ROOT = path.join(__dirname, '..');
const {
getGlobalConfigDir,
getGlobalSkillsBase,
resolveAntigravityGlobalDir,
resolveKimiGlobalDir,
resolveConfigHomeFromDescriptor,
resolveSkillsBaseFromDescriptor,
} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-homes.cjs'));
const HOME = os.homedir();
@@ -632,6 +634,48 @@ describe('descriptor-driven equivalence: unknown runtime fallback', () => {
});
});
describe('descriptor-driven global skills base', () => {
test('hermes skills base is derived from descriptor artifact layout', () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(getGlobalSkillsBase('hermes'), path.join(HOME, '.hermes', 'skills', 'gsd'));
} finally {
restoreEnvKeys(saved);
}
});
test('kilo skills base is derived from configHome.skillsHome descriptor', () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(getGlobalSkillsBase('kilo'), path.join(HOME, '.kilo', 'skills'));
} finally {
restoreEnvKeys(saved);
}
});
test('synthetic runtime skillsHome descriptor resolves without a runtime-name branch', () => {
const base = resolveSkillsBaseFromDescriptor(
{
kind: 'xdg',
name: 'futurecli',
env: ['FUTURE_CONFIG_DIR', 'FUTURE_CONFIG', 'XDG_CONFIG_HOME'],
skillsHome: {
kind: 'dot-home',
name: '.futurecli',
env: ['FUTURE_SKILLS_HOME'],
},
},
{
env: { FUTURE_SKILLS_HOME: '/custom/future-skills' },
home: '/home/u',
existsSync: () => false,
},
);
assert.strictEqual(base, path.join('/custom/future-skills', 'skills'));
});
});
// ── GOLDEN PARITY: getGlobalConfigDir via process.env for all 16 registry runtimes ──
describe('descriptor-driven parity: 14 non-probe registry runtimes × no-env-vars = golden defaults', () => {

View File

@@ -732,8 +732,20 @@ describe('stateReplaceFieldWithFallback', () => {
test('returns content unchanged when neither field matches', () => {
const content = '# State\n\n**Phase:** 3\n';
const result = stateReplaceFieldWithFallback(content, 'Status', 'state', 'New');
let warning = '';
const origErrWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = (chunk) => {
warning += String(chunk);
return true;
};
let result;
try {
result = stateReplaceFieldWithFallback(content, 'Status', 'state', 'New');
} finally {
process.stderr.write = origErrWrite;
}
assert.strictEqual(result, content, 'content should be unchanged');
assert.match(warning, /STATE\.md field "Status"/, 'missing field warning should be emitted');
});
test('prefers primary over fallback when both exist', () => {