From 944873687270197a57fed6ba7483dc47894c81d5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 16 Aug 2026 01:14:26 -0400 Subject: [PATCH] fix(#3547): exercise the real global config-home shape in the install harness (#3567) * test(#3547): failing-first regression for collapsed global install shape * fix(#3547): exercise the real global config-home shape in the install harness * test(#3547): align ripple suites with the real global install shape * test(#3547): update stale collapsed-shape pins in provenance and migration suites * fix(#3547): bump emitted-baseline schema version for the real install shape --------- Co-authored-by: sim --- tests/emitted-provenance.test.cjs | 12 ++-- tests/fixtures/install-tree/antigravity.json | 1 - tests/fixtures/install-tree/augment.json | 1 - tests/fixtures/install-tree/cline.json | 2 - tests/fixtures/install-tree/codebuddy.json | 1 - tests/fixtures/install-tree/codex.json | 72 ------------------- tests/fixtures/install-tree/copilot.json | 1 - tests/fixtures/install-tree/cursor.json | 1 - tests/fixtures/install-tree/hermes.json | 1 - tests/fixtures/install-tree/kilo.json | 1 - tests/fixtures/install-tree/kimi-code.json | 68 +++++++++--------- tests/fixtures/install-tree/kimi.json | 34 --------- tests/fixtures/install-tree/opencode.json | 1 - tests/fixtures/install-tree/pi.json | 1 - tests/fixtures/install-tree/qwen.json | 1 - tests/fixtures/install-tree/trae.json | 1 - tests/fixtures/install-tree/windsurf.json | 1 - tests/fixtures/install-tree/zcode.json | 1 - tests/golden-install-tree.test.cjs | 62 ++++++++++++++++ tests/helpers/emitted-baseline.cjs | 10 ++- tests/helpers/emitted-provenance.cjs | 25 +++---- tests/helpers/install-shared.cjs | 55 ++++++++++---- tests/kimi-upgrades.test.cjs | 25 ++++--- tests/opencode-command-dir-plural.test.cjs | 16 +++-- ...rkflow-fragments-emission.install.test.cjs | 10 ++- 25 files changed, 197 insertions(+), 207 deletions(-) diff --git a/tests/emitted-provenance.test.cjs b/tests/emitted-provenance.test.cjs index 6a8f30fab..fe448515d 100644 --- a/tests/emitted-provenance.test.cjs +++ b/tests/emitted-provenance.test.cjs @@ -323,9 +323,10 @@ test('spot-check: cline rules are code-derived (attributable), not exempt', () = assert.equal(got.kind, 'code-derived', `${rel} must stay attributable`); assert.deepEqual(got.sources, [CLINE_BODY_SRC]); } - const agentsMd = attributeEmittedPath('.agents/AGENTS.md', 'cline'); - assert.equal(agentsMd.kind, 'code-derived'); - assert.deepEqual(agentsMd.sources, [CLINE_BODY_SRC]); + // #3547 dropped the `.agents/AGENTS.md` spot-check: that path only entered a + // manifest while the harness's collapsed --config-dir walked cline's + // ~/.agents sibling; with the real `.cline` config home it can never occur, + // and the rule was removed by the dead-rule arm. }); test('spot-check: install-time state is exempt with an empty source list', () => { @@ -334,7 +335,6 @@ test('spot-check: install-time state is exempt with an empty source list', () => ['gsd-core/VERSION', 'claude'], ['gsd-core/.gsd-runtime', 'claude'], ['package.json', 'opencode'], - ['.gsd/defaults.json', 'opencode'], ['opencode.json', 'opencode'], ]) { const got = attributeEmittedPath(rel, rt); @@ -620,7 +620,9 @@ test('emitted paths that could traverse out of the repo are rejected', () => { assert.throws(() => attributeEmittedPath('', 'claude'), /non-empty string/); // A dot-prefixed segment is NOT traversal — this must still resolve normally. - assert.doesNotThrow(() => attributeEmittedPath('.gsd/defaults.json', 'opencode')); + // #3547: '.gsd/defaults.json' left the manifests with the collapsed shape; a + // dot-prefixed STILL-EMITTED path serves the same not-traversal example. + assert.doesNotThrow(() => attributeEmittedPath('.gsd-profile', 'opencode')); }); test('sampleLimit truncation is exact at limit-1 / limit / limit+1', () => { diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 28947622d..1359e6aee 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 2962913f2..a1d4dfe54 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 1ab375a67..18f1846aa 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -1,9 +1,7 @@ [ - ".agents/AGENTS.md", ".clinerules/gsd.md", ".clinerules/hooks/PreToolUse", ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 6285b7c9e..7e4dac904 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index ad211f83a..cae3ee976 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -1,77 +1,5 @@ [ - ".agents/skills/gsd-add-tests/SKILL.md", - ".agents/skills/gsd-ai-integration-phase/SKILL.md", - ".agents/skills/gsd-audit-fix/SKILL.md", - ".agents/skills/gsd-audit-milestone/SKILL.md", - ".agents/skills/gsd-audit-uat/SKILL.md", - ".agents/skills/gsd-autonomous/SKILL.md", - ".agents/skills/gsd-capture/SKILL.md", - ".agents/skills/gsd-cleanup/SKILL.md", - ".agents/skills/gsd-code-review/SKILL.md", - ".agents/skills/gsd-complete-milestone/SKILL.md", - ".agents/skills/gsd-config/SKILL.md", - ".agents/skills/gsd-debug/SKILL.md", - ".agents/skills/gsd-discuss-phase/SKILL.md", - ".agents/skills/gsd-docs-update/SKILL.md", - ".agents/skills/gsd-eval-review/SKILL.md", - ".agents/skills/gsd-execute-phase/SKILL.md", - ".agents/skills/gsd-explore/SKILL.md", - ".agents/skills/gsd-extract-learnings/SKILL.md", - ".agents/skills/gsd-fast/SKILL.md", - ".agents/skills/gsd-forensics/SKILL.md", - ".agents/skills/gsd-graphify/SKILL.md", - ".agents/skills/gsd-health/SKILL.md", - ".agents/skills/gsd-help/SKILL.md", - ".agents/skills/gsd-import/SKILL.md", - ".agents/skills/gsd-inbox/SKILL.md", - ".agents/skills/gsd-ingest-docs/SKILL.md", - ".agents/skills/gsd-manager/SKILL.md", - ".agents/skills/gsd-map-codebase/SKILL.md", - ".agents/skills/gsd-mempalace-capture/SKILL.md", - ".agents/skills/gsd-mempalace-recall/SKILL.md", - ".agents/skills/gsd-milestone-summary/SKILL.md", - ".agents/skills/gsd-mvp-phase/SKILL.md", - ".agents/skills/gsd-new-milestone/SKILL.md", - ".agents/skills/gsd-new-project/SKILL.md", - ".agents/skills/gsd-next/SKILL.md", - ".agents/skills/gsd-ns-context/SKILL.md", - ".agents/skills/gsd-ns-ideate/SKILL.md", - ".agents/skills/gsd-ns-manage/SKILL.md", - ".agents/skills/gsd-ns-project/SKILL.md", - ".agents/skills/gsd-ns-review/SKILL.md", - ".agents/skills/gsd-ns-workflow/SKILL.md", - ".agents/skills/gsd-onboard/SKILL.md", - ".agents/skills/gsd-pause-work/SKILL.md", - ".agents/skills/gsd-phase/SKILL.md", - ".agents/skills/gsd-plan-phase/SKILL.md", - ".agents/skills/gsd-plan-review-convergence/SKILL.md", - ".agents/skills/gsd-pr-branch/SKILL.md", - ".agents/skills/gsd-profile-user/SKILL.md", - ".agents/skills/gsd-progress/SKILL.md", - ".agents/skills/gsd-quick/SKILL.md", - ".agents/skills/gsd-resume-work/SKILL.md", - ".agents/skills/gsd-review-backlog/SKILL.md", - ".agents/skills/gsd-review/SKILL.md", - ".agents/skills/gsd-secure-phase/SKILL.md", - ".agents/skills/gsd-settings/SKILL.md", - ".agents/skills/gsd-ship/SKILL.md", - ".agents/skills/gsd-sketch/SKILL.md", - ".agents/skills/gsd-spec-phase/SKILL.md", - ".agents/skills/gsd-spike/SKILL.md", - ".agents/skills/gsd-stats/SKILL.md", - ".agents/skills/gsd-surface/SKILL.md", - ".agents/skills/gsd-thread/SKILL.md", - ".agents/skills/gsd-ui-phase/SKILL.md", - ".agents/skills/gsd-ui-review/SKILL.md", - ".agents/skills/gsd-ultraplan-phase/SKILL.md", - ".agents/skills/gsd-undo/SKILL.md", - ".agents/skills/gsd-update/SKILL.md", - ".agents/skills/gsd-validate-phase/SKILL.md", - ".agents/skills/gsd-verify-work/SKILL.md", - ".agents/skills/gsd-workspace/SKILL.md", - ".agents/skills/gsd-workstreams/SKILL.md", ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-advisor-researcher.toml", "agents/gsd-ai-researcher.md", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 6771b5508..408ee77ea 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.agent.md", "agents/gsd-ai-researcher.agent.md", "agents/gsd-assumptions-analyzer.agent.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index eaff0bd35..9b72023d1 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index b780830e3..085531ad1 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 0d8d99d63..d086797a5 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index 00672c92d..fab9ade7e 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -1,39 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", - ".kimi-code/hooks/gsd-agent-isolation-guard.js", - ".kimi-code/hooks/gsd-check-update-worker.js", - ".kimi-code/hooks/gsd-check-update.js", - ".kimi-code/hooks/gsd-config-reload.js", - ".kimi-code/hooks/gsd-context-monitor.js", - ".kimi-code/hooks/gsd-cursor-post-tool.js", - ".kimi-code/hooks/gsd-cursor-pre-tool.js", - ".kimi-code/hooks/gsd-cursor-session-start.js", - ".kimi-code/hooks/gsd-cursor-stop.js", - ".kimi-code/hooks/gsd-cursor-subagent-start.js", - ".kimi-code/hooks/gsd-cursor-subagent-stop.js", - ".kimi-code/hooks/gsd-ensure-canonical-path.js", - ".kimi-code/hooks/gsd-graphify-update.sh", - ".kimi-code/hooks/gsd-phase-boundary.sh", - ".kimi-code/hooks/gsd-prompt-guard.js", - ".kimi-code/hooks/gsd-read-guard.js", - ".kimi-code/hooks/gsd-read-injection-scanner.js", - ".kimi-code/hooks/gsd-session-state.sh", - ".kimi-code/hooks/gsd-statusline.js", - ".kimi-code/hooks/gsd-update-banner.js", - ".kimi-code/hooks/gsd-validate-commit.sh", - ".kimi-code/hooks/gsd-windsurf-pre-command.js", - ".kimi-code/hooks/gsd-windsurf-pre-write.js", - ".kimi-code/hooks/gsd-workflow-guard.js", - ".kimi-code/hooks/gsd-worktree-path-guard.js", - ".kimi-code/hooks/gsd-write-guard.js", - ".kimi-code/hooks/lib/cursor-workspace.js", - ".kimi-code/hooks/lib/git-cmd.js", - ".kimi-code/hooks/lib/gsd-graphify-rebuild.sh", - ".kimi-code/hooks/lib/injection-patterns.js", - ".kimi-code/hooks/lib/isolation-sentinel.js", - ".kimi-code/hooks/managed-hooks-registry.cjs", - ".kimi-code/hooks/package.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", @@ -68,6 +34,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "config.toml", "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", @@ -402,6 +369,39 @@ "gsd-core/workflows/verify-work.md", "gsd-core/workflows/verify-work/steps/automated-ui-verification.md", "gsd-core/workflows/verify-work/steps/mvp-uat-framing.md", + "hooks/gsd-agent-isolation-guard.js", + "hooks/gsd-check-update-worker.js", + "hooks/gsd-check-update.js", + "hooks/gsd-config-reload.js", + "hooks/gsd-context-monitor.js", + "hooks/gsd-cursor-post-tool.js", + "hooks/gsd-cursor-pre-tool.js", + "hooks/gsd-cursor-session-start.js", + "hooks/gsd-cursor-stop.js", + "hooks/gsd-cursor-subagent-start.js", + "hooks/gsd-cursor-subagent-stop.js", + "hooks/gsd-ensure-canonical-path.js", + "hooks/gsd-graphify-update.sh", + "hooks/gsd-phase-boundary.sh", + "hooks/gsd-prompt-guard.js", + "hooks/gsd-read-guard.js", + "hooks/gsd-read-injection-scanner.js", + "hooks/gsd-session-state.sh", + "hooks/gsd-statusline.js", + "hooks/gsd-update-banner.js", + "hooks/gsd-validate-commit.sh", + "hooks/gsd-windsurf-pre-command.js", + "hooks/gsd-windsurf-pre-write.js", + "hooks/gsd-workflow-guard.js", + "hooks/gsd-worktree-path-guard.js", + "hooks/gsd-write-guard.js", + "hooks/lib/cursor-workspace.js", + "hooks/lib/git-cmd.js", + "hooks/lib/gsd-graphify-rebuild.sh", + "hooks/lib/injection-patterns.js", + "hooks/lib/isolation-sentinel.js", + "hooks/managed-hooks-registry.cjs", + "hooks/package.json", "scripts/changeset/README.md", "scripts/changeset/cli.cjs", "scripts/changeset/github-release-notes.cjs", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 29d78ac04..c4fea9eea 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -1,39 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", - ".kimi/hooks/gsd-agent-isolation-guard.js", - ".kimi/hooks/gsd-check-update-worker.js", - ".kimi/hooks/gsd-check-update.js", - ".kimi/hooks/gsd-config-reload.js", - ".kimi/hooks/gsd-context-monitor.js", - ".kimi/hooks/gsd-cursor-post-tool.js", - ".kimi/hooks/gsd-cursor-pre-tool.js", - ".kimi/hooks/gsd-cursor-session-start.js", - ".kimi/hooks/gsd-cursor-stop.js", - ".kimi/hooks/gsd-cursor-subagent-start.js", - ".kimi/hooks/gsd-cursor-subagent-stop.js", - ".kimi/hooks/gsd-ensure-canonical-path.js", - ".kimi/hooks/gsd-graphify-update.sh", - ".kimi/hooks/gsd-phase-boundary.sh", - ".kimi/hooks/gsd-prompt-guard.js", - ".kimi/hooks/gsd-read-guard.js", - ".kimi/hooks/gsd-read-injection-scanner.js", - ".kimi/hooks/gsd-session-state.sh", - ".kimi/hooks/gsd-statusline.js", - ".kimi/hooks/gsd-update-banner.js", - ".kimi/hooks/gsd-validate-commit.sh", - ".kimi/hooks/gsd-windsurf-pre-command.js", - ".kimi/hooks/gsd-windsurf-pre-write.js", - ".kimi/hooks/gsd-workflow-guard.js", - ".kimi/hooks/gsd-worktree-path-guard.js", - ".kimi/hooks/gsd-write-guard.js", - ".kimi/hooks/lib/cursor-workspace.js", - ".kimi/hooks/lib/git-cmd.js", - ".kimi/hooks/lib/gsd-graphify-rebuild.sh", - ".kimi/hooks/lib/injection-patterns.js", - ".kimi/hooks/lib/isolation-sentinel.js", - ".kimi/hooks/managed-hooks-registry.cjs", - ".kimi/hooks/package.json", "agents/gsd.md", "agents/gsd.yaml", "agents/subagents/gsd-advisor-researcher.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index ec97504e3..443ca1000 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index 3aeb29bad..4f97d5963 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "extensions/gsd.js", "extensions/package.json", "gsd-core/.gsd-runtime", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index e88b85544..e1cd21e60 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index fb1bc5982..f1bd8b99e 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 322930e24..d23da609b 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 0434cd3e5..8d55bc3f7 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -1,6 +1,5 @@ [ ".gsd-profile", - ".gsd/defaults.json", "agents/gsd-advisor-researcher.md", "agents/gsd-ai-researcher.md", "agents/gsd-assumptions-analyzer.md", diff --git a/tests/golden-install-tree.test.cjs b/tests/golden-install-tree.test.cjs index 2a73bcde7..8d072aff6 100644 --- a/tests/golden-install-tree.test.cjs +++ b/tests/golden-install-tree.test.cjs @@ -151,3 +151,65 @@ test('install tree — claude (local legacy layout)', async (t) => { assert.deepEqual(actual, fixture, lines.join('\n')); } }); + +// ─── #3547: the harness's global install must exercise the REAL config-home shape ─── +// +// runMinimalInstall's global scope used to pass `--config-dir ` where the +// same was also the sandbox HOME — collapsing configDir onto HOME. A real +// global install resolves its config home to a STRICT SUBDIRECTORY of $HOME +// (/.claude, /.codex, /.config/opencode, …), and that is what +// computePathPrefix's `isGlobal && posixTarget.startsWith(posixHome)` branch +// needs to emit `$HOME//`-shaped prefixes. Under the collapsed shape the +// prefix was bare `$HOME/`, so every emitted global artifact referenced +// `$HOME/gsd-core/…` — a path that exists on no real install — and the entire +// emitted-artifact apparatus (ADR-2719 differential, install-tree fixtures, +// 19-family baseline) was structurally blind to drift confined to the real +// global shape (#3544's fix rewrote 54 includes on live installs with ZERO +// manifest/fixture diffs). + +test('#3547 global harness install uses the real config-home shape (claude)', async (t) => { + const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' }); + t.after(() => cleanup(root)); + + // Strict-subdirectory shape: /.claude, never the collapsed . + assert.equal(configDir, path.join(root, RUNTIME_META.claude.globalSuffix), + 'global configDir must be the runtime\'s real global dir under the sandbox HOME'); + assert.ok(path.dirname(configDir) === root, 'configDir must be a direct subdirectory of the sandbox HOME'); + + // The deployed contract: emitted agent text references $HOME/.claude/gsd-core/, + // never the unsuffixed $HOME/gsd-core/ form (posixNormalized on every platform). + const planner = fs.readFileSync(path.join(configDir, 'agents', 'gsd-planner.md'), 'utf-8'); + assert.match(planner, /\$HOME\/\.claude\/gsd-core\//, + 'emitted agents must carry the real global prefix shape'); + assert.doesNotMatch(planner, /\$HOME\/gsd-core\//, + 'the collapsed bare-$HOME prefix must not appear in real-shape installs'); +}); + +test('#3547 codex global harness install resolves the .codex subdirectory', async (t) => { + const { configDir, root } = runMinimalInstall({ runtime: 'codex', scope: 'global' }); + t.after(() => cleanup(root)); + assert.equal(configDir, path.join(root, RUNTIME_META.codex.globalSuffix)); +}); + +test('#3547 opencode global harness install resolves the XDG subdirectory', async (t) => { + const { configDir, root } = runMinimalInstall({ runtime: 'opencode', scope: 'global' }); + t.after(() => cleanup(root)); + assert.equal(configDir, path.join(root, '.config', 'opencode')); +}); + +test('#3547 local scope configDir unchanged', async (t) => { + const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'local' }); + t.after(() => cleanup(root)); + assert.equal(configDir, path.join(root, '.claude'), + 'local installs keep the root/ shape — untouched by #3547'); +}); + +test('#3547 unknown runtime global scope fails loudly', () => { + // grok is in the capability registry but not RUNTIME_META: a silent + // `path.join(root, undefined)` is the #3023 failure mode this guards. + assert.throws( + () => runMinimalInstall({ runtime: 'grok', scope: 'global' }), + /grok|RUNTIME_META/, + 'a runtime without a known global dir must fail loudly before any install spawns', + ); +}); diff --git a/tests/helpers/emitted-baseline.cjs b/tests/helpers/emitted-baseline.cjs index 289cc9fa6..34605f62a 100644 --- a/tests/helpers/emitted-baseline.cjs +++ b/tests/helpers/emitted-baseline.cjs @@ -47,8 +47,14 @@ const BASELINE_ENV = 'GSD_EMITTED_BASELINE'; /** Default on-disk cache location, relative to the repo root. */ const DEFAULT_CACHE_PATH = '.gsd-cache/emitted-baseline.json'; -/** Baseline artifact schema version — pinned so a format change fails loudly. */ -const BASELINE_VERSION = 1; +/** Baseline artifact schema version — pinned so a format change fails loudly. + * v2 (#3547): the harness now measures every global family in the REAL + * config-home shape (/ instead of the collapsed + * ), so manifests from a v1 baseline — built with the collapsed + * harness — are not comparable (HOME-level paths present, prefix bytes + * absent). Bumping refuses v1 caches and forces the same-schema fallback + * build; the push-to-next job republishes at v2 when this merges. */ +const BASELINE_VERSION = 2; /** * Validate a baseline artifact's shape and freshness. diff --git a/tests/helpers/emitted-provenance.cjs b/tests/helpers/emitted-provenance.cjs index b418b2cae..5394bbbbb 100644 --- a/tests/helpers/emitted-provenance.cjs +++ b/tests/helpers/emitted-provenance.cjs @@ -530,13 +530,17 @@ const PROVENANCE_RULES = [ pattern: /^(gsd\.md|hooks\/PreToolUse)$/, sources: () => [CLINE_BODY_SRC], }, - { - id: 'agents-md-code-derived', - kind: 'code-derived', - roots: ['.agents'], - pattern: /^AGENTS\.md$/, - sources: () => [CLINE_BODY_SRC], - }, + // #3547 removed `agents-md-code-derived` (roots: ['.agents'], + // ^AGENTS\.md$) and `synthesized-gsd-defaults` (^\.gsd/defaults\.json$): + // both paths only appeared in a manifest while the harness collapsed + // configDir onto the sandbox HOME, walking HOME-level siblings + // (cline's ~/.agents/AGENTS.md, every non-Claude runtime's + // ~/.gsd/defaults.json). With the harness installing into each runtime's + // real global subdirectory those files sit outside the walked configDir, + // the rules matched nothing, and the totality guard's dead-rule arm fired + // — exactly as designed. The files are still written, still covered by the + // existence/config suites (kimi-upgrades, codex-config, install suites); + // they are simply no longer manifest members to attribute. { id: 'hermes-category-description', kind: 'code-derived', @@ -568,13 +572,6 @@ const PROVENANCE_RULES = [ pattern: /^(\.gsd-profile|package\.json|\.kimi(-code)?\/package\.json|gsd-core\/VERSION|gsd-core\/\.gsd-runtime)$/, sources: () => [], }, - { - id: 'synthesized-gsd-defaults', - kind: 'synthesized', - roots: null, - pattern: /^\.gsd\/defaults\.json$/, - sources: () => [], - }, { id: 'synthesized-host-config', kind: 'synthesized', diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 299e7f548..125ae8d60 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -191,18 +191,22 @@ const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hook // platform-stable `[features] hooks = true` flag — the real hook commands // live in Codex's separate hooks.json, already excluded above). Blanket- // excluding the 'config.toml' basename would silently blind Codex's fixture -// to any future regression there. Kimi's config.toml instead lives OUTSIDE -// its GSD configDir at runtime (resolveKimiHooksTomlDir resolves ~/.kimi, a -// sibling of the configDir ~/.config/agents) — it only appears inside this -// harness's walked tree at all because runMinimalInstall sets HOME to the -// same temp root used as --config-dir, collapsing the two into one directory -// for the isolated test run. So it is excluded by its exact relative path -// under that collapsed root, not by basename. -// Both Kimi products' native config.toml embeds a platform-varying node-runner -// command, so neither belongs in the golden-tracked emitted manifest. kimi-code -// resolves its own root since #2755 — listing only `.kimi/config.toml` here made -// kimi-code's config.toml newly manifest-visible and unattributable. -const HOOK_CONFIG_RELATIVE_PATHS = new Set(['.kimi/config.toml', '.kimi-code/config.toml']); +// to any future regression there — and it would blind kimi-code's too: since +// #3547 the harness installs into each runtime's REAL global subdirectory, so +// kimi-code's hooks config.toml sits at its configDir root (rel `config.toml`) +// and is legitimately manifest-visible. Tracking it is safe now: the +// install-tree fixture carries paths only, and the ADR-2719 differential +// compares base-vs-current on the same machine, so the platform-varying +// node-runner command embedded in the TOML never crosses platforms inside a +// gate (that was a golden-content-era hazard, and the goldens are gone). +// Kimi CLI's config.toml (KIMI_SHARE_DIR root ~/.kimi) lives OUTSIDE its GSD +// configDir (~/.config/agents) and never enters the walk. The pre-#3547 +// relative-path exclusions ('.kimi/config.toml', '.kimi-code/config.toml') +// existed only for the collapsed shape — where the walked root was the HOME +// itself and those HOME-level siblings were inside it; with no walker rooting +// at HOME anymore they matched nothing and were removed (#3547). kimi-code +// resolves its own root since #2755. +const HOOK_CONFIG_RELATIVE_PATHS = new Set(); // Path prefixes excluded from the parity manifest. `gsd-core/bin/lib/` holds the // tsc-built runtime artifacts (compiled from src/*.cts) that the install COPIES @@ -570,8 +574,31 @@ function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INS let cwd = process.cwd(); const args = [installScript, `--${runtime}`]; if (scope === 'global') { - args.push('--global', '--config-dir', root); - configDir = root; + // #3547 — install into the runtime's REAL global config home: the strict + // subdirectory of the sandbox HOME a genuine global install resolves + // (RUNTIME_META.globalSuffix mirrors the registry's getGlobalConfigDir + // for every runtime). The previous `--config-dir ` collapsed + // configDir onto HOME, so computePathPrefix emitted bare `$HOME/` + // prefixes and the emitted bytes referenced `$HOME/gsd-core/…` — a path + // no real install produces — leaving every emitted-artifact gate + // (ADR-2719 differential, install-tree fixtures, the 19-family baseline) + // blind to drift confined to the real global shape (#3544 evidence: 54 + // includes rewritten on live installs, zero manifest/fixture diffs). The + // explicit flag stays: hermeticity-by-override is immune to ambient + // redirect envs (CI runners export XDG_CONFIG_HOME, which redefines the + // opencode/kilo XDG descriptors' resolution when no explicit dir wins). + const globalMeta = RUNTIME_META[runtime]; + if (!globalMeta || !globalMeta.globalSuffix) { + // #3023 lesson: a silent `path.join(root, undefined)` here throws a + // bare TypeError naming neither the runtime nor the map at fault; a + // runtime without a known global home must fail loudly before any + // install spawns. + throw new Error( + `runMinimalInstall: no RUNTIME_META.globalSuffix for runtime "${runtime}" — refusing to guess a global config dir (#3547)`, + ); + } + configDir = path.join(root, globalMeta.globalSuffix); + args.push('--global', '--config-dir', configDir); } else { args.push('--local'); cwd = root; diff --git a/tests/kimi-upgrades.test.cjs b/tests/kimi-upgrades.test.cjs index 2c2d921d2..140d606ac 100644 --- a/tests/kimi-upgrades.test.cjs +++ b/tests/kimi-upgrades.test.cjs @@ -285,11 +285,13 @@ test('UPGRADE 2: a corrupted/undeclared dispatch still fails closed to inline (s // docs/reference/host-integration-capability-matrix.md's kimi EoS-status // paragraph) — the installer's deliverable stops at the Agent-tool grant plus // the negotiated backgroundDispatch axis asserted above. -test('UPGRADE 2 (installer-testable proxy): kimi --global install with subagents present grants kimi_cli.tools.agent:Agent on the root agent', (t) => { - const { root } = runMinimalInstall({ runtime: 'kimi', scope: 'global' }); - t.after(() => cleanup(root)); + test('UPGRADE 2 (installer-testable proxy): kimi --global install with subagents present grants kimi_cli.tools.agent:Agent on the root agent', (t) => { + const { configDir, root } = runMinimalInstall({ runtime: 'kimi', scope: 'global' }); + t.after(() => cleanup(root)); - const rootYamlPath = path.join(root, 'agents', 'gsd.yaml'); + // #3547 — the root agent lives under the runtime's real global config + // home (/.config/agents), not the sandbox HOME itself. + const rootYamlPath = path.join(configDir, 'agents', 'gsd.yaml'); assert.ok(fs.existsSync(rootYamlPath), 'kimi: agents/gsd.yaml must exist'); const rootYaml = fs.readFileSync(rootYamlPath, 'utf8'); @@ -420,8 +422,11 @@ describe('kimi vs kimi-code hooks-TOML root (#2755)', () => { assert.ok(hasGsdHooksBlock(path.join(altHome, 'config.toml')), 'KIMI_CODE_HOME must redirect the hooks block'); - assert.ok(!fs.existsSync(path.join(root, '.kimi-code')), - 'the default kimi-code root must not be used when the env var is set'); + // #3547 — /.kimi-code legitimately exists as kimi-code's GSD config + // home in the harness's real global shape; what must NOT happen is the + // DEFAULT hooks root receiving the GSD block while the override is set. + assert.ok(!hasGsdHooksBlock(path.join(root, '.kimi-code', 'config.toml')), + 'the default kimi-code hooks root must not receive the GSD hooks block when the env var is set'); assert.ok(!fs.existsSync(path.join(root, '.kimi')), "Kimi CLI's root must not be touched either"); }); @@ -488,8 +493,10 @@ describe('kimi vs kimi-code hooks-TOML root (#2755)', () => { assert.ok(hasGsdHooksBlock(path.join(codeAlt, 'config.toml')), 'kimi-code must honor KIMI_CODE_HOME while KIMI_SHARE_DIR is also set'); assert.ok(!fs.existsSync(path.join(root, '.kimi')), - 'neither default root may be used when both overrides are set'); - assert.ok(!fs.existsSync(path.join(root, '.kimi-code')), - 'neither default root may be used when both overrides are set'); + 'neither default hooks root may be used when both overrides are set'); + // #3547 — /.kimi-code is kimi-code's GSD config home now; assert the + // hooks BLOCK stayed off the default root instead of directory absence. + assert.ok(!hasGsdHooksBlock(path.join(root, '.kimi-code', 'config.toml')), + 'neither default hooks root may receive the GSD block when both overrides are set'); }); }); diff --git a/tests/opencode-command-dir-plural.test.cjs b/tests/opencode-command-dir-plural.test.cjs index c51aa68a5..3989101a7 100644 --- a/tests/opencode-command-dir-plural.test.cjs +++ b/tests/opencode-command-dir-plural.test.cjs @@ -53,12 +53,16 @@ const EXPECTED_COMMAND_DIR = 'commands'; /** Re-run the installer against an EXISTING configDir/root to simulate an * upgrade/reapply pass (the installer runs its migration planner on every * invocation — see bin/install.js installAllRuntimes -> install() -> - * runInstallerMigrations, unconditional, not gated on first-install). */ -function reinstallOpencode(root, scope = 'global') { + * runInstallerMigrations, unconditional, not gated on first-install). + * #3547 — `configDir` is the runtime's REAL global home (/), + * the same shape runMinimalInstall now installs into; reinstalling into the + * bare would target a different config home and never see the + * fabricated legacy state. */ +function reinstallOpencode(root, configDir = null, scope = 'global') { const args = [INSTALL_SCRIPT, '--opencode']; let cwd = process.cwd(); if (scope === 'global') { - args.push('--global', '--config-dir', root); + args.push('--global', '--config-dir', configDir || root); } else { args.push('--local'); cwd = root; @@ -305,7 +309,7 @@ describe('#2329: upgrading an install with an orphaned command/ dir migrates it assert.ok(gsdMdFiles(legacyDir).length >= 60, 'sanity: legacyDir must be populated before reinstall'); assert.ok(!fs.existsSync(pluralDir), 'sanity: pluralDir must not exist before reinstall (fabricated pre-fix state)'); - const result = reinstallOpencode(root, 'global'); + const result = reinstallOpencode(root, configDir, 'global'); assert.strictEqual( result.status, 0, `reinstall (upgrade) must exit 0\nstdout: ${result.stdout}\nstderr: ${result.stderr}` @@ -341,7 +345,7 @@ describe('#2329: upgrading an install with an orphaned command/ dir migrates it const userFileLegacy = path.join(legacyDir, 'my-notes.md'); fs.writeFileSync(userFileLegacy, userContent, 'utf8'); - const result = reinstallOpencode(root, 'global'); + const result = reinstallOpencode(root, configDir, 'global'); assert.strictEqual( result.status, 0, `reinstall (upgrade) must exit 0\nstdout: ${result.stdout}\nstderr: ${result.stderr}` @@ -406,7 +410,7 @@ describe('#2329: upgrading an install with an orphaned command/ dir migrates it `sanity: manifest must record >=60 "command/" keys before reinstall, got ${legacyManifestKeys.length}` ); - const result = reinstallOpencode(root, 'global'); + const result = reinstallOpencode(root, configDir, 'global'); assert.strictEqual( result.status, 0, `reinstall (upgrade) must exit 0\nstdout: ${result.stdout}\nstderr: ${result.stderr}` diff --git a/tests/workflow-fragments-emission.install.test.cjs b/tests/workflow-fragments-emission.install.test.cjs index d2d10568c..fc2b2b445 100644 --- a/tests/workflow-fragments-emission.install.test.cjs +++ b/tests/workflow-fragments-emission.install.test.cjs @@ -80,6 +80,12 @@ const RUNTIMES = Object.keys(RUNTIME_META); * assert success — callers decide (row 36 expects failure). */ function spawnGlobalInstall(installScript, runtime, extraArgs = []) { const root = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2930-dest-${runtime}-`)); + // #3547 — same real config-home shape as runMinimalInstall: the strict + // / subdirectory, not the collapsed . The size + // comparisons below normalize each side's own root out of the text; a + // collapsed stub against a real-shape install bakes in a `.claude`-style + // suffix delta that has nothing to do with the compose wiring under test. + const configDir = path.join(root, RUNTIME_META[runtime].globalSuffix); const args = [ '--preserve-symlinks', '--preserve-symlinks-main', @@ -87,7 +93,7 @@ function spawnGlobalInstall(installScript, runtime, extraArgs = []) { `--${runtime}`, '--global', '--config-dir', - root, + configDir, ...extraArgs, ]; const seamResult = runNode(args, { @@ -96,7 +102,7 @@ function spawnGlobalInstall(installScript, runtime, extraArgs = []) { timeoutMs: INSTALL_TIMEOUT_MS, }); const result = { status: seamResult.exitCode, stdout: seamResult.stdout, stderr: seamResult.stderr }; - return { result, configDir: root, root }; + return { result, configDir, root }; } /** Convert native path separators to POSIX forward slashes, unconditionally