test(#2003): add regression tests for --runtime override on capability state/loop render-hooks
Mirrors the #1160 installed-layout block for the runtime auto-detection gap. Covers: runtimeOverride='claude' bypasses persisted config.runtime:'codex'; no override still honours persisted runtime (regression guard); alias canonicalization (codex-app -> codex); and an end-to-end CLI test proving 'capability state --runtime claude' resolves the Claude config dir despite a persisted runtime:'codex'. Expected RED against unfixed resolveCapabilityRuntimeState (no runtimeOverride param) and unfixed gsd-tools.cjs (no --runtime parsing for capability state / loop render-hooks).
This commit is contained in:
@@ -15,10 +15,11 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
resolveCapabilityState,
|
||||
resolveCapabilityRuntimeState,
|
||||
isCapabilityActive,
|
||||
_isSafePropKey,
|
||||
_loadInstalledSkillsManifest,
|
||||
@@ -1810,3 +1811,93 @@ describe('#1459 IC-04: capability-state threads gsdHome to the overlay loader',
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('regressions: --runtime override bypasses persisted runtime (#2003)', () => {
|
||||
// #2003: `capability state` and `loop render-hooks` parsed only --config-dir,
|
||||
// never --runtime. resolveCapabilityRuntimeState derived the config dir from
|
||||
// resolveRuntime(cwd) (GSD_RUNTIME → config.runtime → 'claude'), so a repo
|
||||
// with persisted runtime:"codex" resolved the config dir to ~/.codex — where
|
||||
// the Claude skill isn't installed → surfaced:false. Fix: thread an explicit
|
||||
// --runtime override through both commands into resolveCapabilityRuntimeState
|
||||
// so it bypasses the persisted-runtime fallback (mirrors the update-context /
|
||||
// effort sync precedent).
|
||||
|
||||
function writePersistedRuntime(tmpDir, runtime) {
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ runtime }),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
test('resolveCapabilityRuntimeState: runtimeOverride="claude" bypasses persisted config.runtime:"codex"', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-override-'));
|
||||
try {
|
||||
writePersistedRuntime(tmpDir, 'codex');
|
||||
const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
||||
const expectedClaudeDir = runtimeHomes.getGlobalConfigDir('claude');
|
||||
const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex');
|
||||
// Persisted runtime is codex; explicit override is claude. The override
|
||||
// MUST win (resolveRuntime is never consulted when an override is given,
|
||||
// so GSD_RUNTIME env cannot interfere either).
|
||||
const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, 'claude');
|
||||
assert.strictEqual(result.runtimeConfigDir, expectedClaudeDir,
|
||||
'--runtime claude must override persisted runtime:"codex"');
|
||||
assert.notStrictEqual(result.runtimeConfigDir, expectedCodexDir,
|
||||
'must NOT resolve to the codex config dir when --runtime claude is explicit');
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolveCapabilityRuntimeState: no override still honours persisted config.runtime (unchanged, regression guard)', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-no-override-'));
|
||||
// Control GSD_RUNTIME so resolveRuntime deterministically reads config.runtime.
|
||||
const savedGsdRuntime = process.env.GSD_RUNTIME;
|
||||
delete process.env.GSD_RUNTIME;
|
||||
try {
|
||||
writePersistedRuntime(tmpDir, 'codex');
|
||||
const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
||||
const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex');
|
||||
// No override → persisted codex wins (existing behavior preserved).
|
||||
const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, undefined);
|
||||
assert.strictEqual(result.runtimeConfigDir, expectedCodexDir,
|
||||
'without --runtime, persisted config.runtime:"codex" still drives resolution (unchanged)');
|
||||
} finally {
|
||||
if (savedGsdRuntime === undefined) delete process.env.GSD_RUNTIME;
|
||||
else process.env.GSD_RUNTIME = savedGsdRuntime;
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('resolveCapabilityRuntimeState: runtimeOverride canonicalizes aliases (codex-app -> codex)', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-alias-'));
|
||||
try {
|
||||
writePersistedRuntime(tmpDir, 'claude');
|
||||
const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
||||
const expectedCodexDir = runtimeHomes.getGlobalConfigDir('codex');
|
||||
// Alias "codex-app" canonicalizes to "codex" via runtime-name-policy.
|
||||
const result = resolveCapabilityRuntimeState(tmpDir, undefined, undefined, 'codex-app');
|
||||
assert.strictEqual(result.runtimeConfigDir, expectedCodexDir,
|
||||
'--runtime codex-app (alias) must canonicalize to codex');
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI: `capability state --runtime claude` reports the Claude config dir despite persisted runtime:"codex"', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-rt-cli-'));
|
||||
try {
|
||||
writePersistedRuntime(tmpDir, 'codex');
|
||||
const result = runGsdTools('capability state --runtime claude --raw', tmpDir);
|
||||
assert.ok(result.success, `capability state --runtime should succeed: ${result.error || ''}`);
|
||||
const parsed = JSON.parse(result.output);
|
||||
const runtimeHomes = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
||||
assert.strictEqual(parsed.runtimeConfigDir, runtimeHomes.getGlobalConfigDir('claude'),
|
||||
'`capability state --runtime claude` must resolve to the Claude config dir, not the persisted codex dir');
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user