Files
msd-core/tests/runtime-homes-descriptor-drive.test.cjs
Tom Boucher b10e56818b feat(#1169): complete ADR-857 phase 6 — migrate features to Capabilities, revive dead gates, harden conformance gate (#1183)
* test(#1168): make phase-6 gate un-gameable — reject empty stubs + require loop shrink

The migration assertion previously checked only role==feature, so a registration-only stub (empty hooks, logic left inline) would turn the gate green while phase 6 stayed incomplete — the exact false-completion pattern this gate exists to prevent. Strengthen it: each ADR-named feature must OWN its behavior (>=1 hook, or a command family); and plan-phase.md/execute-phase.md must shrink strictly below their frozen pre-phase-6 sizes (94519/93166 LF bytes), which also defeats double-run gaming (declare a hook but keep the inline block -> file does not shrink -> red).

Gate now 5 pass / 4 fail (orphaned execute:wave:post, empty/unregistered features, config-key leaks, no shrink). Green is now reachable only by REAL migration. Refs #1168, #1169.

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

* feat(#1169): migrate gap-analysis to a Capability (plan:post gate)

First real ADR-857 phase-6 migration (pattern-defining tracer). gap-analysis moves from an inline post_planning_gaps branch in plan-phase.md to a real plan:post gate Capability:

- capabilities/gap-analysis/capability.json: role:feature, plan:post gate (when=workflow.post_planning_gaps, blocking:false advisory), OWNS workflow.post_planning_gaps (federated out of central schema). - plan-phase.md: inline config-get + gsd_run gap-analysis block replaced with a plan:post render-hooks call site dispatching the gate; file shrinks 94519->93279. - src/check-command-router.cts: cmdGapAnalysisPlanPost runs the real gap analysis via gap-checker. - post_planning_gaps removed from central manifest; resolves via federated config (default true preserved). - tests/post-planning-gaps-2493: re-pointed to assert capability ownership.

Verified: gate 5 pass / 4 fail (gap-analysis cleared from migration, plan:post-orphan, config-leak, and plan-phase shrink checks); loadConfig still returns post_planning_gaps=true; check command runs real analysis; 392/392 in the config/registry/federation/router net. Refs #1169.

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

* feat(#1169): migrate profile-pipeline to a command-family Capability

ADR-857 Decision 7: profile-pipeline becomes a command-family Capability (like audit/intel/graphify). capabilities/profile-pipeline/capability.json declares an 8-command family (scan-sessions, extract-messages, profile-sample, write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md) backed by a new gsd-core/bin/lib/profile-pipeline-command-router.cjs; the inline case arms are removed from gsd-tools.cjs. Owns profile-pipeline.enabled (federated).

Verified: registry shows role:feature with commands.length=8; scan-sessions/profile-sample run live via the family; gate cleared profile-pipeline from the empty-stub failure (only tdd/schema-gate/drift remain); 296/296 registry+inventory+gsd-tools tests; lint 0 errors. Refs #1169.

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

* fix(#1167): wire execute:wave:post + implement ui.safety-gate check

Revives the second dead gate from #1167: ui.gates@execute:wave:post was declared but never dispatched AND its check.query (ui.safety-gate) was unimplemented. Adds the per-wave execute:wave:post render-hooks call site in execute-phase.md (fires after each wave's merge/cleanup, before the next forks) and implements cmdUiSafetyGate (frontend + UI-SPEC aware, mirrors cmdUiPlanGate) in check-command-router. +17 regression tests.

Verified: phase-6 orphaned-points conformance test now PASSES (gate 6 pass / 3 fail); ui-safety-gate routable in dot+hyphen forms; check-ui-safety-gate 17/17, check-ui-plan-gate 18/18; lint 0 errors. Refs #1167, #1168.

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

* feat(#1169): migrate drift (schema + codebase) to execute:wave:post gates

Removes the inline schema_drift_gate + codebase_drift_gate steps (77 lines) from execute-phase.md; drift becomes a Capability with two execute:wave:post gates (verify.schema-drift blocking, verify.codebase-drift advisory) dispatched via the per-wave render-hooks call site. check-command-router routes verify.schema-drift / verify.codebase-drift to the real detectors. Federates workflow.drift_threshold / drift_action / schema_drift_gate out of central.

Also fixes the execute:wave:post dispatch prose to run NON-blocking (advisory) gates too — the prior version only ran blocking gates, which would have silently dropped the codebase-drift advisory after its inline step was removed. Behavior preserved.

Verified: gate 7 pass / 2 fail (drift cleared from stub + config-leak; execute-phase.md 92297 < 93166 frozen -> shrink passes); both drift checks run real detection; loadConfig defaults preserved (threshold=3, action=warn, gate=true); drift-detection 56/56 + schema-drift 34/34; lint 0 errors. Refs #1169.

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

* feat(#1169): migrate tdd to a Capability (plan:pre contribution + execute:post gate)

tdd becomes a real Capability: a plan:pre contribution injects the <tdd_mode_active> planner guidance (rendered from PLAN_PRE_HOOKS_JSON like security's contribution), and an execute:post gate (tdd.review-checkpoint, advisory) runs the real end-of-phase RED/GREEN review via a new check-command handler. Inline tdd_mode reads + the inline planner block + the tdd_review_checkpoint step are removed; workflow.tdd_mode is federated out of central. The MVP+TDD per-task RED-commit gate is preserved — TDD_MODE is now derived from the execute:post hooks (capId==tdd active), not an inline config-get.

BEHAVIOR CHANGE (documented, not silent): the --tdd CLI flag now persists workflow.tdd_mode=true via config-set instead of being per-invocation. Rationale: tdd is now a config-toggled Capability, and env vars do not persist across the workflow's separate bash blocks (config does), so an ephemeral override isn't cleanly achievable; --tdd therefore enables the tdd capability, consistent with how all capabilities are toggled.

Verified: gate 7 pass / 2 fail (tdd cleared from stub + config-leak; plan-phase + execute-phase both < frozen sizes); contribution injection + execute:post gate dispatch wired; MVP+TDD gate preserved; tdd.review-checkpoint runs real review; full unit suite 556/0; lint 0 errors. Refs #1169.

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

* feat(#1169): migrate schema-gate to a plan:pre contribution Capability

The plan-time schema-push detection (former plan-phase.md §5.7) becomes a schema-gate Capability: a plan:pre contribution (into:planner, when:workflow.schema_push_detection) whose fragment carries the full ORM-detection + [BLOCKING] schema-push-task injection logic, rendered into the planner via the existing plan:pre render-hooks dispatch. The inline §5.7 block is removed (plan-phase.md 94519->90445). workflow.schema_push_detection is a new capability-owned (federated) key, default true. (The execute-side schema-drift gate was migrated separately into the drift capability.)

Verified: registry inlines the fragment (len 2704) so it is actually delivered at plan:pre; gate 8 pass / 1 fail — all 5 ADR-named features now real Capabilities, only the config-leak test remains (intel/security, next unit). Refs #1169.

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

* feat(#1169): close the 3 capability config-key leaks — phase-6 gate now GREEN

Removes the last inline config-get reads of capability-owned keys from plan-phase.md. security_asvs_level/security_block_on now flow through the security plan:pre contribution via a new loop-resolver configValues mechanism (resolves declared config keys with the same 4-level precedence as activation and attaches them to the rendered hook); the §5.55 banner reads them from PLAN_PRE_HOOKS_JSON. intel.enabled becomes a real intel plan:pre step (ref.command: intel api-surface) dispatched via render-hooks; the inline intel branch is gone. gen-capability-registry now validates ref.command as a third dispatch shape.

Verified: phase-6 capstone conformance gate is FULLY GREEN (9/0); 3 leaks gone (grep=0); security configValues resolve to {2,medium}/default {1,high}; intel step present only when enabled; loop-render-hooks 62/0, capability-registry 287/0, capability-state/federated-config 113/0; lint 0 errors. Closes the migration half of #1169. Refs #1139, #1167, #1168.

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

* fix(#1169): address adversarial review — restore schema-drift block, generic planner injection, uniform gate contract

Adversarial review caught 2 real regressions the green gate missed: (1) schema-drift no longer blocked — the execute:wave:post dispatch read GATE_RESULT.block but verify.schema-drift emitted drift_detected/blocking, and onError:skip wrongly bypassed positive blocks; (2) only tdd's plan:pre contribution was injected into the planner, dropping schema-gate's schema-push detection and security's threat-model guidance.

Fixes: (A) every gate check returns a uniform boolean 'block' under --raw (the dispatch form), with advisory gates (tdd/gap) carrying their report in 'message'; (B) gate-dispatch contract corrected at all sites — onError governs command errors only, a blocking gate's positive block always halts; (C) generic planner injection of all plan:pre contributions where into=='planner' (tdd + schema-gate + security incl configValues); (D) two new conformance assertions: planner contributions injected generically + every gate check.query returns boolean block under --raw.

Verified: gate 11/11; all 6 gate checks return boolean block under --raw; full suite 595/0; lint 0 errors. Refs #1167, #1168, #1169.

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

* fix(#1169): restore MVP+TDD end-of-phase blocking escalation (2nd adversarial pass)

The migrated tdd execute:post gate is statically blocking:false, but the contract (references/execute-mvp-tdd.md + CONTEXT.md) requires the end-of-phase TDD review to ESCALATE from advisory to blocking when MVP_MODE && TDD_MODE && a TDD plan misses a RED/GREEN commit. The migration prose had downgraded this to a 'strong advisory recommendation' — silent loss of the blocking escalation. Restore it: the tdd-gate dispatch now refuses to mark the phase complete (Phase blocked message) under MVP+TDD when GATE_RESULT.block is true; advisory otherwise.

Also strengthen tests/execute-mvp-tdd-gate.test.cjs: hasBlockingEscalation previously matched any line with 'blocking'+'mvp+tdd' (so 'advisory (blocking: false) ... under MVP+TDD' was a false green); now it requires the real refusal semantics ('refuse to mark the phase complete' / 'phase blocked'). Caught by 2nd adversarial review pass.

Verified: execute-phase.md 92702 < 93166 frozen; mvp-tdd-gate + phase-6 gate 19/0; full suite green; lint 0 errors. Refs #1169.

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

* fix(#1169): restore MVP+TDD proceed-block, codebase auto-remap, schema skip-flag (3rd adversarial pass)

3rd adversarial pass found 4 more silent regressions: (1) the tdd MVP+TDD 'refuse to mark complete' was nullified by a downstream 'ALWAYS proceed regardless of gate results' line — proceed is now conditional (stops on an active MVP+TDD block); (2) the test now asserts the proceed is NOT an unconditional override; (3) codebase-drift auto-remap (spawn gsd-codebase-mapper when drift_action=auto-remap) was dropped — the execute:wave:post advisory dispatch now consumes spawn_mapper/directive; (4) GSD_SKIP_SCHEMA_CHECK bypass was lost from the gate path — cmdVerifySchemaDrift now honors the env var (block:false when set).

Verified: no unconditional proceed; GSD_SKIP_SCHEMA_CHECK=true -> block:false; gate 11/11 + mvp-tdd 9/9; full suite 569/0; lint 0; execute-phase.md 93109 < 93166. Refs #1169.

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

* fix(#1169): init.cts reads federated config keys from nested path (4th adversarial pass)

Config federation moved tdd_mode/research/nyquist_validation from flat config.<key> to nested config.workflow.<key>, but src/init.cts still read them flat — so init.plan-phase/init.execute-phase emitted tdd_mode:false / research_enabled:undefined / nyquist:undefined regardless of config (a public command-contract regression; the migrated loops use render-hooks so enforcement was unaffected). Read via config.workflow (type-safe Record cast). Now init reflects the same resolved values + federated defaults (research/nyquist default true) as the render-hooks path.

Verified: build clean; init.plan-phase emits tdd_mode:true/research:false/nyquist:false for set config, defaults true for empty; full suite 591/0; lint 0. Refs #1169.

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

* docs(#1169): add changeset for ADR-857 phase-6 completion (PR #1183)

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

* fix(#1169): complete phase-6 migration fallout — restore TEXT_MODE, fix registry .claude leak, re-point stale workflow-contract tests

The capability migration left real regressions and stale consumer tests that
the per-module unit suite missed but the full cross-platform suite caught (27
failing tests):

Real source regressions (fixed):
- execute-phase.md lost its AskUserQuestion TEXT_MODE plain-text fallback when
  the inline schema_drift_gate step was removed — non-Claude runtimes would
  stall. Restored, and the execute:post gate-dispatch prose de-duplicated to
  cite the execute:wave:post contract (loop body shrinks below the frozen
  pre-phase-6 ceiling while keeping every onError/blocking nuance).
- capabilities/tdd inline fragment hardcoded `@~/.claude/gsd-core/references/tdd.md`,
  baked verbatim into the committed capability-registry.cjs and leaked the
  install path on 11 non-Claude runtimes (registry .cjs is copied, not
  path-converted). Made the fragment path-free; regenerated the registry. The
  phase-6 conformance gate now guards this (no ~/.claude install path in any
  capability source or the generated registry).
- plan-phase.md: removed a §5.7 stub re-added in error and routed Branch 2 to
  step 6 (schema-gate is a plan:pre capability, §5.7 is gone).

Stale workflow-contract tests re-pointed to the capability dispatch they now
must assert (behavior verified preserved in source first, assertions kept
equal-or-stronger): bug-621 + bug-2851 (gap-analysis via gsd_run render-hooks
plan:post + registry binding), feat-2527 (tdd_mode federated out of central),
phase6-planning + plan-phase-ui-redirect (§5.6 bounded by ## 6.),
plan-phase-drift-guard (intel when:intel.enabled skip branch).

profile-pipeline-command-router.cjs un-ignored from eslint (hand-written, no
TS source) + stale disable comments removed. Size baseline regenerated.

Verified: full suite 15140 tests / 0 fail; lint 0 errors; conformance gate green
legitimately. Refs #1139, #1167, #1168, #1169.

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

* test(#1169): add ADR-857 E2E content-test coverage for the 12 loop points + capability deliverables

Grounds the capability engine in behavioral E2E tests (drive the real
render-hooks/check CLI + the real registry, assert typed result content — no
source-grep), structured around what ADR-857 says to deliver. 207 tests; each
genuineness-checked (flip the expectation, confirm it fails).

Per-loop-point dispatch (7 files): empty-point negative-space across the 6
no-hook points; verify:post 3-step resolution+ordering+onError; plan:pre
contribution/configValues + ui.plan-gate + intel; plan:post gap-analysis;
execute:wave:post drift+ui gates via the check route (schema-drift block/skip,
codebase-drift threshold BVA, auto-remap); execute:post tdd.review-checkpoint
RED/GREEN; ship:pre security gate resolution + frontmatter-get predicate pieces.

ADR-deliverable coverage (4 files): predicate boundary held (edge/prohibition
probes stay core, not off-by-default Feature Capabilities — phase-6 exception);
core loop runs with zero capabilities (all 12 points empty, init bundles
resolve); contribution merge (multiple ordered <contribution from=> blocks);
federated-config key removal on uninstall.

federated-config allowlisted for its 3-file split (unit + integration +
lifecycle). Refs #1139, #1167, #1168, #1169.

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

* fix(#1169): remove dead drifted converter dups + address adversarial review

Lint cleanup (root-caused, not waved off): src/runtime-artifact-conversion.cts
carried 11 agent-converter functions (+5 orphaned consts/helpers) that were
never exported, never called, and had silently DRIFTED from the live
hand-authored copies in bin/install.js (one even referenced an undefined
`claudeToCopilotTools`). Deleted the dead duplicates; install.js's live copies
are untouched (it never imported these). Lint now 0 errors / 0 warnings.

Adversarial-review (Codex) findings fixed:
- HIGH: execute-phase.md TDD_MODE used `jq ... || echo false`, silently
  disabling the MVP+TDD blocking gate on jq-less runtimes. Reverted to the
  `node -e` form (node is guaranteed; matches the file's other node-e usages) so
  a missing optional tool can no longer fail-open a blocking safety path.
- MEDIUM: federated-config-key-removal orphan-key test was vacuous (it skipped
  the orphan assertion). Now asserts the removed capability's key is genuinely
  not surfaced/validated after uninstall.
- LOW: phase-6 conformance leak regex broadened to catch absolute-home and
  Windows-backslash `.claude/(gsd-core|commands|agents|hooks)` paths, not only
  `~`/`$HOME` forward-slash forms.
- LOW: bug-2851 plan:post dispatch assertion now requires `--raw` (matched its
  stated contract).
- nit: plan-pre intel-step test duplicate assertion replaced with a distinct
  structured-output check.

Size baseline regenerated (execute-phase.md 93089 < 93166 frozen). Refs #1167, #1169.

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

* test(#1169): make runtime-homes-descriptor-drive titles environment-independent

The descriptor-equivalence test embedded the absolute golden config path
(`os.homedir()`-derived) directly in each `test(...)` title, so titles differed
between macOS (`/Users/x/.claude`) and Docker (`/home/gsdtest/.claude`). Every
test PASSES on both platforms (15885/0 leaf tests each), but gsd-test-summary
compares results by title and reported 29+29 false "only in Mac / only in
Docker" discrepancies for tests that actually pass everywhere.

Move the golden path out of the title and into the assertion message (still
shown on failure); titles are now byte-identical across platforms so the
cross-platform comparator matches them. No assertion logic or golden values
changed. Refs #1169.

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

* fix(#1169): derive TDD_MODE via gsd_run --active-cap, not node -e (fix prompt-injection CI gate)

The prior fix reverted execute-phase.md:181 from jq to `node -e` to close a
Codex HIGH (jq||echo-false silently disabling the MVP+TDD blocking gate on
jq-less runtimes) — but the CI prompt-injection scanner BLOCKS new `node -e` in
workflow markdown (inline code-exec = injection vector), turning the security
gate red. Both forms were wrong: node -e fails the scanner; jq fail-opens a
blocking safety gate; `config-get workflow.tdd_mode` is forbidden by the
conformance leak gate (tdd_mode is capability-owned).

Correct fix (what Codex recommended): a gsd_run-native boolean. Add an
`--active-cap <capId>` flag to `loop render-hooks <point>` that resolves hooks
the normal way and prints exactly `true`/`false` for whether a capId is active
— scanner-safe (canonical launcher, no inline code), node-reliable (no optional
jq to fail-open), and leak-free (render-hooks resolution, not config-get).
execute-phase.md:181 now `TDD_MODE=$(gsd_run loop render-hooks execute:post
--active-cap tdd)`. +5 behavioral tests for the flag.

Verified: prompt-injection-scan --diff origin/next → 0 findings; conformance
gate 13/13 (execute-phase.md 92934 < 93166); execute-mvp-tdd + tdd-mode +
loop-render-hooks 87/0; lint 0/0. Refs #1167, #1169.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:07:55 -04:00

710 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Equivalence proof for ADR-857 phase 5b: descriptor-driven getGlobalConfigDir.
*
* For every runtime in the 16-entry capability registry, plus grok and unknown
* runtime, this test asserts that getGlobalConfigDir() produces exactly the
* same path that the old hardcoded switch produced (golden expected values
* captured from the switch BEFORE any edits). All assertions are byte-identical.
*
* The injected opts seam on resolveConfigHomeFromDescriptor is used to control:
* - the env record (avoid ambient env var pollution)
* - the home directory (make tests hermetic)
* - existsSync (control probe-hit / probe-miss scenarios)
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const os = require('node:os');
const fs = require('node:fs');
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();
// ── Helper: run fn with process.env temporarily mutated ──────────────────────
function withEnv(overrides, fn) {
const saved = {};
for (const [k, v] of Object.entries(overrides)) {
saved[k] = process.env[k];
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
try {
return fn();
} finally {
for (const [k] of Object.entries(overrides)) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
}
}
// All env vars for all runtimes — cleared in each test that calls getGlobalConfigDir directly
const ALL_ENV_KEYS = [
'CLAUDE_CONFIG_DIR', 'CURSOR_CONFIG_DIR', 'GEMINI_CONFIG_DIR', 'CODEX_HOME',
'GROK_AGENTS_HOME', 'COPILOT_CONFIG_DIR', 'COPILOT_HOME', 'ANTIGRAVITY_CONFIG_DIR',
'WINDSURF_CONFIG_DIR', 'AUGMENT_CONFIG_DIR', 'TRAE_CONFIG_DIR', 'QWEN_CONFIG_DIR',
'HERMES_HOME', 'CODEBUDDY_CONFIG_DIR', 'CLINE_CONFIG_DIR', 'KIMI_CONFIG_DIR',
'OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG', 'KILO_CONFIG_DIR', 'KILO_CONFIG',
'XDG_CONFIG_HOME',
];
function clearAllEnvKeys() {
const saved = {};
for (const k of ALL_ENV_KEYS) {
saved[k] = process.env[k];
delete process.env[k];
}
return saved;
}
function restoreEnvKeys(saved) {
for (const k of ALL_ENV_KEYS) {
if (saved[k] !== undefined) process.env[k] = saved[k];
else delete process.env[k];
}
}
// ── STEP 0: golden scenarios captured from old switch BEFORE edits ────────────
// GOLDEN DEFAULTS (no env vars set, no existsSync probe hits).
// kimi is NOT included here because it depends on real filesystem probing —
// its probe-miss/hit scenarios are covered separately via injected existsSync.
// antigravity default also depends on probing; the default assumes NO dirs exist.
const GOLDEN_DEFAULTS = {
claude: path.join(HOME, '.claude'),
cursor: path.join(HOME, '.cursor'),
gemini: path.join(HOME, '.gemini'),
codex: path.join(HOME, '.codex'),
grok: path.join(HOME, '.agents'),
copilot: path.join(HOME, '.copilot'),
antigravity: path.join(HOME, '.gemini', 'antigravity'), // probe-miss → first candidate
windsurf: path.join(HOME, '.codeium', 'windsurf'),
augment: path.join(HOME, '.augment'),
trae: path.join(HOME, '.trae'),
qwen: path.join(HOME, '.qwen'),
hermes: path.join(HOME, '.hermes'),
codebuddy: path.join(HOME, '.codebuddy'),
cline: path.join(HOME, '.cline'),
opencode: path.join(HOME, '.config', 'opencode'),
kilo: path.join(HOME, '.config', 'kilo'),
};
// ── GOLDEN DEFAULTS ────────────────────────────────────────────────────────────
describe('descriptor-driven equivalence: defaults (no env vars, no probe hits)', () => {
// kimi is excluded: its default depends on real filesystem probing (probe-hit/miss
// vary by machine). kimi probe scenarios are covered in the generic-agents-root suite
// with injected existsSync.
// antigravity is excluded: it also depends on real fs probing (probe candidates
// ~/.gemini/antigravity, ~/.gemini/antigravity-ide, ~/.gemini/antigravity-cli);
// a machine that has antigravity-ide or antigravity-cli but not antigravity gets a
// different result. antigravity probe scenarios are covered in the dot-home-nested
// suite with injected existsSync.
for (const [runtime, expected] of Object.entries(GOLDEN_DEFAULTS).filter(
([r]) => r !== 'antigravity',
)) {
test(`${runtime} default resolves to its golden config dir`, () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(getGlobalConfigDir(runtime), expected, `${runtime} default → ${expected}`);
} finally {
restoreEnvKeys(saved);
}
});
}
test('unknown runtime falls back to ~/.claude (CLAUDE_CONFIG_DIR unset)', () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(getGlobalConfigDir('totally-unknown-runtime-xyz'), path.join(HOME, '.claude'));
} finally {
restoreEnvKeys(saved);
}
});
});
// ── GOLDEN ENV OVERRIDES ──────────────────────────────────────────────────────
describe('descriptor-driven equivalence: env-var overrides', () => {
const cases = [
{ runtime: 'claude', envKey: 'CLAUDE_CONFIG_DIR', value: '/custom/claude' },
{ runtime: 'cursor', envKey: 'CURSOR_CONFIG_DIR', value: '/custom/cursor' },
{ runtime: 'gemini', envKey: 'GEMINI_CONFIG_DIR', value: '/custom/gemini' },
{ runtime: 'codex', envKey: 'CODEX_HOME', value: '/custom/codex' },
{ runtime: 'grok', envKey: 'GROK_AGENTS_HOME', value: '/custom/grok' },
{ runtime: 'augment', envKey: 'AUGMENT_CONFIG_DIR', value: '/custom/augment' },
{ runtime: 'trae', envKey: 'TRAE_CONFIG_DIR', value: '/custom/trae' },
{ runtime: 'qwen', envKey: 'QWEN_CONFIG_DIR', value: '/custom/qwen' },
{ runtime: 'hermes', envKey: 'HERMES_HOME', value: '/custom/hermes' },
{ runtime: 'codebuddy', envKey: 'CODEBUDDY_CONFIG_DIR', value: '/custom/codebuddy' },
{ runtime: 'cline', envKey: 'CLINE_CONFIG_DIR', value: '/custom/cline' },
{ runtime: 'windsurf', envKey: 'WINDSURF_CONFIG_DIR', value: '/custom/windsurf' },
{ runtime: 'antigravity', envKey: 'ANTIGRAVITY_CONFIG_DIR', value: '/custom/antigravity' },
{ runtime: 'kimi', envKey: 'KIMI_CONFIG_DIR', value: '/custom/kimi' },
{ runtime: 'opencode', envKey: 'OPENCODE_CONFIG_DIR', value: '/custom/opencode' },
{ runtime: 'kilo', envKey: 'KILO_CONFIG_DIR', value: '/custom/kilo' },
];
for (const { runtime, envKey, value } of cases) {
test(`${runtime}: ${envKey} override → ${value}`, () => {
const saved = clearAllEnvKeys();
process.env[envKey] = value;
try {
assert.strictEqual(getGlobalConfigDir(runtime), value);
} finally {
restoreEnvKeys(saved);
}
});
}
// copilot: COPILOT_CONFIG_DIR takes precedence over COPILOT_HOME
test('copilot: COPILOT_CONFIG_DIR override (first env wins)', () => {
const saved = clearAllEnvKeys();
process.env['COPILOT_CONFIG_DIR'] = '/custom/copilot-dir';
process.env['COPILOT_HOME'] = '/should/not/win';
try {
assert.strictEqual(getGlobalConfigDir('copilot'), '/custom/copilot-dir');
} finally {
restoreEnvKeys(saved);
}
});
test('copilot: COPILOT_HOME fallback when COPILOT_CONFIG_DIR absent', () => {
const saved = clearAllEnvKeys();
process.env['COPILOT_HOME'] = '/custom/copilot-home';
try {
assert.strictEqual(getGlobalConfigDir('copilot'), '/custom/copilot-home');
} finally {
restoreEnvKeys(saved);
}
});
});
// ── GOLDEN TILDE EXPANSION ─────────────────────────────────────────────────────
describe('descriptor-driven equivalence: tilde expansion in env overrides', () => {
test('claude: CLAUDE_CONFIG_DIR=~/foo expands to homedir/foo', () => {
withEnv({ CLAUDE_CONFIG_DIR: '~/foo' }, () => {
assert.strictEqual(getGlobalConfigDir('claude'), path.join(HOME, 'foo'));
});
});
test('kimi: KIMI_CONFIG_DIR=~/kimi expands to homedir/kimi', () => {
withEnv({ KIMI_CONFIG_DIR: '~/kimi' }, () => {
assert.strictEqual(getGlobalConfigDir('kimi'), path.join(HOME, 'kimi'));
});
});
});
// ── GOLDEN XDG SCENARIOS ──────────────────────────────────────────────────────
describe('descriptor-driven equivalence: xdg runtimes (opencode, kilo)', () => {
// opencode
test('opencode: OPENCODE_CONFIG (file-path) → dirname', () => {
const saved = clearAllEnvKeys();
process.env['OPENCODE_CONFIG'] = '/home/u/cfg/opencode.json';
try {
assert.strictEqual(getGlobalConfigDir('opencode'), '/home/u/cfg');
} finally {
restoreEnvKeys(saved);
}
});
test('opencode: OPENCODE_CONFIG_DIR takes precedence over OPENCODE_CONFIG', () => {
const saved = clearAllEnvKeys();
process.env['OPENCODE_CONFIG_DIR'] = '/dir/wins';
process.env['OPENCODE_CONFIG'] = '/file/loses.json';
try {
assert.strictEqual(getGlobalConfigDir('opencode'), '/dir/wins');
} finally {
restoreEnvKeys(saved);
}
});
test('opencode: OPENCODE_CONFIG takes precedence over XDG_CONFIG_HOME', () => {
const saved = clearAllEnvKeys();
process.env['OPENCODE_CONFIG'] = '/cfg/opencode.json';
process.env['XDG_CONFIG_HOME'] = '/xdg/should/lose';
try {
assert.strictEqual(getGlobalConfigDir('opencode'), '/cfg');
} finally {
restoreEnvKeys(saved);
}
});
test('opencode: XDG_CONFIG_HOME → ~/.config/opencode subdir', () => {
const saved = clearAllEnvKeys();
process.env['XDG_CONFIG_HOME'] = '/xdg';
try {
assert.strictEqual(getGlobalConfigDir('opencode'), path.join('/xdg', 'opencode'));
} finally {
restoreEnvKeys(saved);
}
});
test('opencode: tilde in OPENCODE_CONFIG → dirname expands tilde', () => {
const saved = clearAllEnvKeys();
process.env['OPENCODE_CONFIG'] = '~/cfg/opencode.json';
try {
assert.strictEqual(getGlobalConfigDir('opencode'), path.join(HOME, 'cfg'));
} finally {
restoreEnvKeys(saved);
}
});
// kilo
test('kilo: KILO_CONFIG (file-path) → dirname', () => {
const saved = clearAllEnvKeys();
process.env['KILO_CONFIG'] = '/home/u/cfg/kilo.json';
try {
assert.strictEqual(getGlobalConfigDir('kilo'), '/home/u/cfg');
} finally {
restoreEnvKeys(saved);
}
});
test('kilo: KILO_CONFIG_DIR takes precedence over KILO_CONFIG', () => {
const saved = clearAllEnvKeys();
process.env['KILO_CONFIG_DIR'] = '/dir/wins';
process.env['KILO_CONFIG'] = '/file/loses.json';
try {
assert.strictEqual(getGlobalConfigDir('kilo'), '/dir/wins');
} finally {
restoreEnvKeys(saved);
}
});
test('kilo: KILO_CONFIG takes precedence over XDG_CONFIG_HOME', () => {
const saved = clearAllEnvKeys();
process.env['KILO_CONFIG'] = '/cfg/kilo.json';
process.env['XDG_CONFIG_HOME'] = '/xdg/should/lose';
try {
assert.strictEqual(getGlobalConfigDir('kilo'), '/cfg');
} finally {
restoreEnvKeys(saved);
}
});
test('kilo: XDG_CONFIG_HOME → ~/.config/kilo subdir', () => {
const saved = clearAllEnvKeys();
process.env['XDG_CONFIG_HOME'] = '/xdg';
try {
assert.strictEqual(getGlobalConfigDir('kilo'), path.join('/xdg', 'kilo'));
} finally {
restoreEnvKeys(saved);
}
});
});
// ── GOLDEN DOT-HOME-NESTED (antigravity probe) ────────────────────────────────
describe('descriptor-driven equivalence: dot-home-nested antigravity probe hit/miss', () => {
test('antigravity probe-miss → ~/.gemini/antigravity (first candidate)', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-antigravity-miss-'));
try {
// no candidates exist → fallback to first
const result = resolveConfigHomeFromDescriptor(
{
kind: 'dot-home-nested',
name: 'antigravity',
parent: '.gemini',
env: ['ANTIGRAVITY_CONFIG_DIR'],
probe: ['antigravity', 'antigravity-ide', 'antigravity-cli'],
},
{ env: {}, home: tmpHome, existsSync: () => false },
);
assert.strictEqual(result, path.join(tmpHome, '.gemini', 'antigravity'));
} finally {
cleanup(tmpHome);
}
});
test('antigravity probe-hit antigravity → returns ~/.gemini/antigravity', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-antigravity-hit-'));
try {
const hitPath = path.join(tmpHome, '.gemini', 'antigravity');
const result = resolveConfigHomeFromDescriptor(
{
kind: 'dot-home-nested',
name: 'antigravity',
parent: '.gemini',
env: ['ANTIGRAVITY_CONFIG_DIR'],
probe: ['antigravity', 'antigravity-ide', 'antigravity-cli'],
},
{ env: {}, home: tmpHome, existsSync: (p) => p === hitPath },
);
assert.strictEqual(result, hitPath);
} finally {
cleanup(tmpHome);
}
});
test('antigravity probe-hit antigravity-ide → returns ~/.gemini/antigravity-ide', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-antigravity-ide-'));
try {
const hitPath = path.join(tmpHome, '.gemini', 'antigravity-ide');
const result = resolveConfigHomeFromDescriptor(
{
kind: 'dot-home-nested',
name: 'antigravity',
parent: '.gemini',
env: ['ANTIGRAVITY_CONFIG_DIR'],
probe: ['antigravity', 'antigravity-ide', 'antigravity-cli'],
},
{ env: {}, home: tmpHome, existsSync: (p) => p === hitPath },
);
assert.strictEqual(result, hitPath);
} finally {
cleanup(tmpHome);
}
});
test('antigravity probe-hit antigravity-cli (only cli exists) → returns ~/.gemini/antigravity-cli', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-antigravity-cli-'));
try {
const hitPath = path.join(tmpHome, '.gemini', 'antigravity-cli');
const result = resolveConfigHomeFromDescriptor(
{
kind: 'dot-home-nested',
name: 'antigravity',
parent: '.gemini',
env: ['ANTIGRAVITY_CONFIG_DIR'],
probe: ['antigravity', 'antigravity-ide', 'antigravity-cli'],
},
{ env: {}, home: tmpHome, existsSync: (p) => p === hitPath },
);
assert.strictEqual(result, hitPath);
} finally {
cleanup(tmpHome);
}
});
test('antigravity: ANTIGRAVITY_CONFIG_DIR env override wins over any probe', () => {
const result = resolveConfigHomeFromDescriptor(
{
kind: 'dot-home-nested',
name: 'antigravity',
parent: '.gemini',
env: ['ANTIGRAVITY_CONFIG_DIR'],
probe: ['antigravity', 'antigravity-ide', 'antigravity-cli'],
},
{ env: { ANTIGRAVITY_CONFIG_DIR: '/custom/ag' }, home: '/home/u', existsSync: () => true },
);
assert.strictEqual(result, '/custom/ag');
});
test('windsurf (no probe) → ~/.codeium/windsurf regardless of existsSync', () => {
const result = resolveConfigHomeFromDescriptor(
{
kind: 'dot-home-nested',
name: 'windsurf',
parent: '.codeium',
env: ['WINDSURF_CONFIG_DIR'],
},
{ env: {}, home: '/home/u', existsSync: () => true },
);
assert.strictEqual(result, path.join('/home/u', '.codeium', 'windsurf'));
});
});
// ── GOLDEN GENERIC-AGENTS-ROOT (kimi probe) ───────────────────────────────────
describe('descriptor-driven equivalence: generic-agents-root kimi probe hit/miss', () => {
test('kimi probe-miss → recommended root ~/.config/agents', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-kimi-miss-'));
try {
const result = resolveConfigHomeFromDescriptor(
{
kind: 'generic-agents-root',
name: 'agents',
env: ['KIMI_CONFIG_DIR'],
probe: ['~/.config/agents', '~/.agents'],
probeExists: 'skills',
},
{ env: {}, home: tmpHome, existsSync: () => false },
);
assert.strictEqual(result, path.join(tmpHome, '.config', 'agents'));
} finally {
cleanup(tmpHome);
}
});
test('kimi probe-hit on recommended root ~/.config/agents/skills', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-kimi-recommended-'));
try {
const recommended = path.join(tmpHome, '.config', 'agents');
const result = resolveConfigHomeFromDescriptor(
{
kind: 'generic-agents-root',
name: 'agents',
env: ['KIMI_CONFIG_DIR'],
probe: ['~/.config/agents', '~/.agents'],
probeExists: 'skills',
},
{
env: {},
home: tmpHome,
existsSync: (p) => p === path.join(recommended, 'skills'),
},
);
assert.strictEqual(result, recommended);
} finally {
cleanup(tmpHome);
}
});
test('kimi probe-hit on fallback ~/.agents/skills (recommended does not exist)', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-kimi-fallback-'));
try {
const fallback = path.join(tmpHome, '.agents');
const result = resolveConfigHomeFromDescriptor(
{
kind: 'generic-agents-root',
name: 'agents',
env: ['KIMI_CONFIG_DIR'],
probe: ['~/.config/agents', '~/.agents'],
probeExists: 'skills',
},
{
env: {},
home: tmpHome,
existsSync: (p) => p === path.join(fallback, 'skills'),
},
);
assert.strictEqual(result, fallback);
} finally {
cleanup(tmpHome);
}
});
test('kimi: KIMI_CONFIG_DIR env override wins over any probe', () => {
const result = resolveConfigHomeFromDescriptor(
{
kind: 'generic-agents-root',
name: 'agents',
env: ['KIMI_CONFIG_DIR'],
probe: ['~/.config/agents', '~/.agents'],
probeExists: 'skills',
},
{ env: { KIMI_CONFIG_DIR: '/custom/kimi' }, home: '/home/u', existsSync: () => true },
);
assert.strictEqual(result, '/custom/kimi');
});
// Verify resolveKimiGlobalDir wrapper delegates correctly
test('resolveKimiGlobalDir wrapper: probe-miss → recommended root', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-rkgd-miss-'));
try {
assert.strictEqual(
resolveKimiGlobalDir({ env: {}, home: tmpHome, existsSync: () => false }),
path.join(tmpHome, '.config', 'agents'),
);
} finally {
cleanup(tmpHome);
}
});
test('resolveKimiGlobalDir wrapper: fallback probe-hit', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-rkgd-hit-'));
try {
const fallback = path.join(tmpHome, '.agents');
assert.strictEqual(
resolveKimiGlobalDir({
env: {},
home: tmpHome,
existsSync: (p) => p === path.join(fallback, 'skills'),
}),
fallback,
);
} finally {
cleanup(tmpHome);
}
});
// Verify resolveAntigravityGlobalDir wrapper delegates correctly
test('resolveAntigravityGlobalDir wrapper: probe-miss → ~/.gemini/antigravity', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-ragd-miss-'));
try {
assert.strictEqual(
resolveAntigravityGlobalDir({ env: {}, home: tmpHome, existsSync: () => false }),
path.join(tmpHome, '.gemini', 'antigravity'),
);
} finally {
cleanup(tmpHome);
}
});
test('resolveAntigravityGlobalDir wrapper: probe-hit antigravity-ide', () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-equiv-ragd-hit-'));
try {
const hitPath = path.join(tmpHome, '.gemini', 'antigravity-ide');
assert.strictEqual(
resolveAntigravityGlobalDir({
env: {},
home: tmpHome,
existsSync: (p) => p === hitPath,
}),
hitPath,
);
} finally {
cleanup(tmpHome);
}
});
});
// ── GOLDEN EXPLICIT DIR OVERRIDE ──────────────────────────────────────────────
describe('descriptor-driven equivalence: explicitDir short-circuit', () => {
test('explicitDir absolute path returned as-is (any runtime)', () => {
assert.strictEqual(getGlobalConfigDir('claude', '/tmp/explicit'), '/tmp/explicit');
assert.strictEqual(getGlobalConfigDir('opencode', '/tmp/explicit'), '/tmp/explicit');
assert.strictEqual(getGlobalConfigDir('kimi', '/tmp/explicit'), '/tmp/explicit');
assert.strictEqual(getGlobalConfigDir('grok', '/tmp/explicit'), '/tmp/explicit');
});
test('explicitDir with ~ is expanded', () => {
assert.strictEqual(
getGlobalConfigDir('claude', '~/foo'),
path.join(HOME, 'foo'),
);
});
test('explicitDir wins even when env var is set', () => {
withEnv({ CLAUDE_CONFIG_DIR: '/should/not/win' }, () => {
assert.strictEqual(getGlobalConfigDir('claude', '/explicit/wins'), '/explicit/wins');
});
});
});
// ── GOLDEN GROK (not in registry, hardcoded) ──────────────────────────────────
describe('descriptor-driven equivalence: grok (not in registry)', () => {
test('grok default → ~/.agents', () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(getGlobalConfigDir('grok'), path.join(HOME, '.agents'));
} finally {
restoreEnvKeys(saved);
}
});
test('grok: GROK_AGENTS_HOME override', () => {
withEnv({ GROK_AGENTS_HOME: '/custom/grok-agents' }, () => {
assert.strictEqual(getGlobalConfigDir('grok'), '/custom/grok-agents');
});
});
test('grok: GROK_AGENTS_HOME tilde expansion', () => {
withEnv({ GROK_AGENTS_HOME: '~/grok' }, () => {
assert.strictEqual(getGlobalConfigDir('grok'), path.join(HOME, 'grok'));
});
});
});
// ── GOLDEN UNKNOWN RUNTIME (Claude fallback) ──────────────────────────────────
describe('descriptor-driven equivalence: unknown runtime fallback', () => {
test('unknown runtime → ~/.claude default', () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(getGlobalConfigDir('no-such-runtime'), path.join(HOME, '.claude'));
} finally {
restoreEnvKeys(saved);
}
});
test('unknown runtime → CLAUDE_CONFIG_DIR if set', () => {
withEnv({ CLAUDE_CONFIG_DIR: '/custom/claude-for-unknown' }, () => {
assert.strictEqual(getGlobalConfigDir('no-such-runtime'), '/custom/claude-for-unknown');
});
});
});
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', () => {
// This is the hardest assertion: it drives getGlobalConfigDir() (which calls
// the registry internally) and compares against GOLDEN_DEFAULTS captured from
// the old switch. Any discrepancy means a regression.
// kimi is excluded because its default depends on real filesystem probing.
// antigravity is excluded because it also depends on real fs probing — a machine
// with ~/.gemini/antigravity-ide or ~/.gemini/antigravity-cli (but not
// ~/.gemini/antigravity) gets a different result. Probe scenarios are covered in
// the dot-home-nested suite with injected existsSync.
// grok is excluded because it is not in the registry (hardcoded branch).
const registryRuntimes = Object.keys(GOLDEN_DEFAULTS).filter(
r => r !== 'grok' && r !== 'antigravity',
);
for (const runtime of registryRuntimes) {
test(`${runtime} via getGlobalConfigDir matches its golden default`, () => {
const saved = clearAllEnvKeys();
try {
assert.strictEqual(
getGlobalConfigDir(runtime),
GOLDEN_DEFAULTS[runtime],
`${runtime} via getGlobalConfigDir matches golden: ${GOLDEN_DEFAULTS[runtime]}`,
);
} finally {
restoreEnvKeys(saved);
}
});
}
});