fix(hud): sync hooks/lib in installers (#1490) — v5.6.2 hotfix#1490
Merged
Conversation
Introduces the canonical install primitive for asset+lib syncs used by both the UserPromptSubmit hook installer and the HUD statusLine installer. The helper guarantees: 1. The script file is copied and chmod 0755. 2. The sibling lib/ directory is **atomically replaced** (rmtree + copytree) so renamed or removed modules from prior plugin versions cannot linger in the target directory. 3. __pycache__, *.pyc, *.pyo, .pytest_cache, test_*.py, and *.egg-info are excluded from the runtime lib so sys.path stays clean. Why rmtree-then-copytree (not dirs_exist_ok=True): copytree's dirs_exist_ok mode only writes; it does not remove files that existed before but are gone now. A renamed module (e.g. hud_old.py → hud_new.py) would remain in the target lib and could be imported first, causing subtle regressions. session-start runs once per Claude Code session, so the rmtree cost is negligible. This commit only introduces the helper and migrates _install_hook_with_lib to use it (no behavior change for the UserPromptSubmit hook beyond stale-safe lib sync). The HUD installer migration follows in the next commit, gated on its own test suite. TDD: 9 RED tests in tests/test_atomic_sync_with_lib.py written first, then GREEN by implementing the helper. Existing TestHookLibCopy/TestRegisterHookInSettings/TestEnsureMcpJson regression suites all still pass (34 prior tests + 9 new = 43). Refs #1490
The v5.6.0 refactor extracted 11 hud_*.py modules + tiny_actor_presets
into hooks/lib/, but _install_statusline still ran a single shutil.copy
on codingbuddy-hud.py and never touched the sibling lib/ directory.
End-state on every upgraded user machine:
~/.claude/hud/codingbuddy-hud.py ← updated
~/.claude/hud/lib/ ← MISSING
The script tries `from hud_buddy import BUDDY_FACE` first; the
ImportError trips its outer try/except, which prints the bare
fallback `◕‿◕ CodingBuddy`. Every Wave 1/2/3 status line feature
shipped in v5.6.0 / v5.6.1 was therefore invisible to all users.
Fix:
- Replace shutil.copy(...) with _atomic_sync_with_lib(source, hud_dir)
so the script and the entire sibling lib/ are synced as a single
unit on every session start.
- Write ~/.claude/hud/.version stamp so health_check (next commit)
can detect drift.
- Honour CODINGBUDDY_HUD_DEBUG=1 to surface installer errors on
stderr (default still silent so session start is never blocked).
Test coverage (test_session_start_hud.py):
- TestSyncHudAssets (8 tests): copies all 12 required modules,
excludes __pycache__/*.pyc/.pytest_cache/test_*.py, replaces
stale renamed modules, idempotent re-invocation, writes version
stamp, gracefully skips lib when absent, preserves settings.json
update.
- TestHudInstallE2ERegressionGate (4 parametrized scenarios):
runs the installed script as a real subprocess and asserts the
output is NOT '◕‿◕ CodingBuddy'. Scenarios cover clean install,
partial (current v5.6.1 user state), stale lib (renamed module),
and fresh idempotent re-run. This is the single regression gate
that would have caught the v5.6.0 / v5.6.1 ship.
Total: 6 prior + 14 new = 20 statusline tests. All green.
Refs #1490
…nc (#1490) Sister-bug to the HUD installer fix: _install_hook_with_lib used copytree(dirs_exist_ok=True), which writes new files but never removes files that existed before but are gone now. A renamed module (e.g. mode_engine.py → mode_engine_v2.py) would leave the old file in ~/.claude/hooks/lib/ indefinitely, where Python's import system could pick it up first. Commit 1 already migrated _install_hook_with_lib to delegate to _atomic_sync_with_lib (which performs an atomic rmtree + copytree). This commit adds the explicit regression gate so future refactors cannot reintroduce the stale-write pattern unnoticed. Coverage (test_install_hook_with_lib.py, 4 cases): - test_replaces_stale_lib_modules — stale renamed module purged - test_excludes_pycache_pyc_pytest_cache_and_test_files — pollutants from source lib never enter target lib - test_renames_source_to_target_filename — user-prompt-submit.py source lands at codingbuddy-mode-detect.py target, source name does NOT linger - test_idempotent_double_invocation — two consecutive installs yield identical state Refs #1490
Adds check #11 to HealthChecker so the v5.6.0/v5.6.1 failure mode is detectable from the user's existing health check workflow without having to read installer source. What it verifies: 1. ~/.claude/hud/codingbuddy-hud.py is present 2. ~/.claude/hud/lib/ exists and contains the seven critical modules: hud_buddy, hud_state, hud_helpers, tiny_actor_presets, hud_version, hud_rate_limits, hud_layout 3. Subprocess render smoke test catches the case where every file looks present but imports still fail at runtime (permission issues, partial copy, .pyc clash). The smoke test fails if the script returns the bare '◕‿◕ CodingBuddy' fallback. 4. ~/.claude/hud/.version stamp is reported in the PASS message so users can confirm which plugin version installed the assets. Coverage (test_health_check.py, +5 cases): - test_fail_when_script_missing - test_fail_when_lib_directory_missing - test_fail_when_required_modules_missing — names the missing module - test_fail_when_smoke_test_returns_fallback — uses a stub script that always prints the fallback face - test_pass_with_real_plugin_install — end-to-end: runs the real _install_statusline against the real codingbuddy-hud.py source in tmpdir, then asserts check_hud_installation returns PASS run_all now returns 11 results (was 10); test renamed accordingly. Refs #1490
) Pre-release regression script that simulates a fresh user receiving cache 5.6.x and starting Claude Code for the first time. Runs the real _install_statusline against the in-tree hooks/ source in an isolated tmpdir, then executes the installed script as a subprocess and asserts the output is the full status line — not the bare '◕‿◕ CodingBuddy' fallback face. Why this complements pytest: - The pytest E2E gate runs inside the test runner's process and fixtures. This shell script exercises the exact code path a user hits, including filesystem layout, chmod, and a fresh subprocess. - It can be run by maintainers locally before pushing release branches without needing to set up a Python venv with pytest. - It is invoked from canary CI in the next commit so PRs cannot merge with a broken installer. Exit codes: 0 — full status line rendered, all assertions passed 1 — fallback face detected or required tokens missing 2 — install crashed before render Local verification at commit time: $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh [verify-install-simulation] PASS: full status line rendered exit=0 Output: '◕‿◕ CB v5.6.1 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6' Refs #1490
Adds the v5.6.2 regression gate to two CI workflows so a broken installer cannot reach canary or release: 1. canary.yml — new ``plugin-hooks-tests`` job (Python pytest only, no node/yarn build needed) blocks ``publish-canary`` via ``needs``. Triggered on master/stag-** push, so master merges are gated before any canary npm publish. 2. e2e-plugin.yml — adds the same pytest invocation + simulation script step to the existing ``e2e-plugin-hooks`` job, and extends ``paths`` to include ``packages/claude-code-plugin/tests/**`` and ``scripts/verify-install-simulation.sh`` so PR-level changes trigger the gate. e2e-plugin runs on non-master branches, so PRs see the gate before merge. Together the two jobs cover both PR review (e2e-plugin) and post-merge canary publish (canary), so the v5.6.0/v5.6.1 failure mode (installer ships without lib sync, all users see the fallback face) cannot recur silently. Test invocations cover all five v5.6.2 hotfix test files: - tests/test_atomic_sync_with_lib.py (9 tests) - tests/test_session_start_hud.py (20 tests) - tests/test_install_hook_with_lib.py (4 tests) - tests/test_health_check.py (35 tests) - scripts/verify-install-simulation.sh (subprocess render gate) actions/setup-python pin reused from existing e2e-plugin.yml entry (SHA a26af69be951a213d495a4c3e4e4022e16d87065 = v5.6.0). Refs #1490
Bump the workspace version from 5.6.1 to 5.6.2 and publish the CHANGELOG entry for the HUD installer hotfix. v5.6.2 closes the v5.6.0 / v5.6.1 HUD statusLine installer regression (#1490). Every user who upgraded to 5.6.0 or 5.6.1 saw only the fallback face '◕‿◕ CodingBuddy' instead of the full Wave 1/2/3 status line, because the installer never copied the hooks/lib/ modules the script depends on. v5.6.2 routes both the HUD installer and the UserPromptSubmit hook installer through a new _atomic_sync_with_lib helper that copies the script and atomically replaces the entire lib/ directory (rmtree + copytree), guaranteeing renamed/removed modules from prior plugin versions cannot linger. Bump surface: - apps/mcp-server/package.json, src/shared/version.ts - packages/rules/package.json - packages/claude-code-plugin/package.json (+ peerDependencies), .claude-plugin/plugin.json, README.md - .claude-plugin/marketplace.json - yarn.lock - CHANGELOG.md (new [5.6.2] section) Refs #1490
…ll-simulation (#1490) Two correctness fixes uncovered while running the script after the v5.6.2 bump: 1. **HOME isolation** — the subprocess that ran the installed HUD inherited the developer's real HOME, so `read_installed_version` tier-1 lookup picked up `~/.claude/plugins/installed_plugins.json` from the developer machine (5.6.1) instead of the version we are about to ship. Output read `CB v5.6.1` even though plugin.json was already 5.6.2. The script now passes an explicit `env=` to subprocess.run with HOME pointing at the tmpdir. 2. **Fake installed_plugins.json** — Claude Code writes `~/.claude/plugins/installed_plugins.json` after every plugin install/update, and `hud_version.get_fresh_version` reads it as tier 1. The simulation now writes a stub manifest in the isolated tmpdir so the installed HUD's version-resolution path matches what a real user will hit on first session-start after `/plugin update`. 3. **Exact version assertion** — the script now reads the version from `plugin.json`, asserts `CB v<expected_version>` appears in the rendered status line, and verifies `~/.claude/hud/.version` contains the same value. This auto-tracks bump-version.sh and removes the previous risk of shipping a release where the version segment renders empty without anyone noticing. Local verification: $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh [verify-install-simulation] stdout='◕‿◕ CB v5.6.2 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6' [verify-install-simulation] PASS: full status line rendered (v5.6.2) exit=0 Refs #1490
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…egment (#1490) Two Critical issues surfaced in CI (e2e-plugin-hooks/3.11) that the local pre-push check missed because the developer/runner HOME leaked into pytest subprocess invocations: 1. **TestHudInstallE2ERegressionGate all 4 scenarios FAILED on CI** with `assert "CB v" in out` because ``hud_version.get_fresh_version``'s tier-1 lookup reads ``~/.claude/plugins/installed_plugins.json`` and CI has none. The render therefore produced ``CB | Ready 🟢 | ...`` — a silent half-broken state where every module imports successfully but the version segment is empty. Locally the same test passed only because the developer's real ``~/.claude/plugins/installed_plugins.json`` (v5.6.1) leaked into the subprocess. Fix: the test now writes a stub ``installed_plugins.json`` into the tmpdir fake home and invokes the subprocess with ``env={"HOME": str(home), ...}`` so all three version-resolution tiers (tier 1 installed_plugins.json, tier 2 plugin.json, tier 3 hud_state) see the isolated environment. Assertion is strengthened to ``CB v<expected_version>`` with ``expected_version`` read from ``plugin.json`` so future bump-version.sh runs auto-gate. A ``.version`` stamp assertion is also added per scenario. The simulation shell script (commit 8) was already hardened this way; this commit brings the pytest E2E suite to parity so CI and local runs agree. 2. **check_hud_installation smoke test did not detect empty version** — it only compared against the literal fallback face, so the ``CB | Ready ...`` half-broken state returned PASS even though the user would see a status line with a missing version segment. Fix: ``check_hud_installation`` now detects ``"CB "`` without ``"CB v"`` in the rendered output and returns FAIL with a clear message ("HUD rendered empty version segment — hud_version fallback chain broken"). The subprocess call also pins ``HOME=self._home_dir`` so the diagnostic honours the HealthChecker's configured home directory rather than leaking the CI runner's real home. A new regression test ``test_fail_when_version_segment_is_empty`` stubs a script that prints the half-broken line and asserts the check FAILs. 3. **test_pass_with_real_plugin_install** (health_check green-path gate) now writes the same fake ``installed_plugins.json`` so the subprocess check_hud_installation invokes can resolve the plugin version inside the isolated environment. Local verification: $ python3 -m pytest tests/test_session_start_hud.py tests/test_health_check.py -v ... 56 passed in 0.90s $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh [verify-install-simulation] PASS: full status line rendered (v5.6.2) Refs #1490
…#1490) Addresses two EVAL findings from the PR #1490 review: **HIGH #1 — race window during lib sync** The previous implementation was rmtree-then-copytree: if target_lib.exists(): shutil.rmtree(target_lib) shutil.copytree(source_lib, target_lib, ...) which left ``target_lib`` missing for the full duration of the copytree (hundreds of milliseconds for a multi-megabyte lib). Any concurrent HUD subprocess that statted the installed script during that window would see ``ImportError`` and render the fallback face — ironic given this PR exists to fix exactly that symptom. Replaced with a staging/rename pattern: 1. ``copytree(source_lib, .lib.staging-<uuid>, ignore=...)`` 2. ``rename(target_lib, .lib.old-<uuid>)`` ← atomic syscall 3. ``rename(.lib.staging-<uuid>, target_lib)`` ← atomic syscall 4. ``rmtree(.lib.old-<uuid>)`` in ``finally`` The window during which ``target_lib`` does not exist now shrinks from ``O(copytree)`` to ``O(rename)`` — effectively atomic on POSIX. Concurrent installers from multiple simultaneous Claude Code sessions are safe because each uses a uuid-scoped staging dir. Rollback: if ``copytree`` raises mid-sync the except block removes the staging dir and restores ``target_lib`` from the archive, so an existing working install is never lost to a partial sync. **HIGH #2 — redundant chmod after rename in _install_hook_with_lib** ``_atomic_sync_with_lib`` already chmod's the synced script to 0o755 before ``_install_hook_with_lib`` renames it to HOOK_FILENAME, and POSIX rename preserves permission bits. The second chmod was defensive but triggered an unnecessary silent-failure path on filesystems that reject mode changes (NFS, read-only mounts). Removed with a code comment explaining why. **New regression tests (+2)**: - ``test_no_staging_leftovers_after_success`` — no stray ``.lib.staging-*`` / ``.lib.old-*`` dirs after a successful sync - ``test_rollback_preserves_old_lib_when_copytree_fails`` — monkeypatches ``shutil.copytree`` to raise OSError, then asserts the pre-existing target lib survives and no staging/archive dirs leak Local verification: 105 pytest suites pass (100 + 5 from prior commit + 2 new race-safety gates). Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
Introduces the canonical install primitive for asset+lib syncs used by both the UserPromptSubmit hook installer and the HUD statusLine installer. The helper guarantees: 1. The script file is copied and chmod 0755. 2. The sibling lib/ directory is **atomically replaced** (rmtree + copytree) so renamed or removed modules from prior plugin versions cannot linger in the target directory. 3. __pycache__, *.pyc, *.pyo, .pytest_cache, test_*.py, and *.egg-info are excluded from the runtime lib so sys.path stays clean. Why rmtree-then-copytree (not dirs_exist_ok=True): copytree's dirs_exist_ok mode only writes; it does not remove files that existed before but are gone now. A renamed module (e.g. hud_old.py → hud_new.py) would remain in the target lib and could be imported first, causing subtle regressions. session-start runs once per Claude Code session, so the rmtree cost is negligible. This commit only introduces the helper and migrates _install_hook_with_lib to use it (no behavior change for the UserPromptSubmit hook beyond stale-safe lib sync). The HUD installer migration follows in the next commit, gated on its own test suite. TDD: 9 RED tests in tests/test_atomic_sync_with_lib.py written first, then GREEN by implementing the helper. Existing TestHookLibCopy/TestRegisterHookInSettings/TestEnsureMcpJson regression suites all still pass (34 prior tests + 9 new = 43). Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
The v5.6.0 refactor extracted 11 hud_*.py modules + tiny_actor_presets
into hooks/lib/, but _install_statusline still ran a single shutil.copy
on codingbuddy-hud.py and never touched the sibling lib/ directory.
End-state on every upgraded user machine:
~/.claude/hud/codingbuddy-hud.py ← updated
~/.claude/hud/lib/ ← MISSING
The script tries `from hud_buddy import BUDDY_FACE` first; the
ImportError trips its outer try/except, which prints the bare
fallback `◕‿◕ CodingBuddy`. Every Wave 1/2/3 status line feature
shipped in v5.6.0 / v5.6.1 was therefore invisible to all users.
Fix:
- Replace shutil.copy(...) with _atomic_sync_with_lib(source, hud_dir)
so the script and the entire sibling lib/ are synced as a single
unit on every session start.
- Write ~/.claude/hud/.version stamp so health_check (next commit)
can detect drift.
- Honour CODINGBUDDY_HUD_DEBUG=1 to surface installer errors on
stderr (default still silent so session start is never blocked).
Test coverage (test_session_start_hud.py):
- TestSyncHudAssets (8 tests): copies all 12 required modules,
excludes __pycache__/*.pyc/.pytest_cache/test_*.py, replaces
stale renamed modules, idempotent re-invocation, writes version
stamp, gracefully skips lib when absent, preserves settings.json
update.
- TestHudInstallE2ERegressionGate (4 parametrized scenarios):
runs the installed script as a real subprocess and asserts the
output is NOT '◕‿◕ CodingBuddy'. Scenarios cover clean install,
partial (current v5.6.1 user state), stale lib (renamed module),
and fresh idempotent re-run. This is the single regression gate
that would have caught the v5.6.0 / v5.6.1 ship.
Total: 6 prior + 14 new = 20 statusline tests. All green.
Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
…nc (#1490) Sister-bug to the HUD installer fix: _install_hook_with_lib used copytree(dirs_exist_ok=True), which writes new files but never removes files that existed before but are gone now. A renamed module (e.g. mode_engine.py → mode_engine_v2.py) would leave the old file in ~/.claude/hooks/lib/ indefinitely, where Python's import system could pick it up first. Commit 1 already migrated _install_hook_with_lib to delegate to _atomic_sync_with_lib (which performs an atomic rmtree + copytree). This commit adds the explicit regression gate so future refactors cannot reintroduce the stale-write pattern unnoticed. Coverage (test_install_hook_with_lib.py, 4 cases): - test_replaces_stale_lib_modules — stale renamed module purged - test_excludes_pycache_pyc_pytest_cache_and_test_files — pollutants from source lib never enter target lib - test_renames_source_to_target_filename — user-prompt-submit.py source lands at codingbuddy-mode-detect.py target, source name does NOT linger - test_idempotent_double_invocation — two consecutive installs yield identical state Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
Adds check #11 to HealthChecker so the v5.6.0/v5.6.1 failure mode is detectable from the user's existing health check workflow without having to read installer source. What it verifies: 1. ~/.claude/hud/codingbuddy-hud.py is present 2. ~/.claude/hud/lib/ exists and contains the seven critical modules: hud_buddy, hud_state, hud_helpers, tiny_actor_presets, hud_version, hud_rate_limits, hud_layout 3. Subprocess render smoke test catches the case where every file looks present but imports still fail at runtime (permission issues, partial copy, .pyc clash). The smoke test fails if the script returns the bare '◕‿◕ CodingBuddy' fallback. 4. ~/.claude/hud/.version stamp is reported in the PASS message so users can confirm which plugin version installed the assets. Coverage (test_health_check.py, +5 cases): - test_fail_when_script_missing - test_fail_when_lib_directory_missing - test_fail_when_required_modules_missing — names the missing module - test_fail_when_smoke_test_returns_fallback — uses a stub script that always prints the fallback face - test_pass_with_real_plugin_install — end-to-end: runs the real _install_statusline against the real codingbuddy-hud.py source in tmpdir, then asserts check_hud_installation returns PASS run_all now returns 11 results (was 10); test renamed accordingly. Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
) Pre-release regression script that simulates a fresh user receiving cache 5.6.x and starting Claude Code for the first time. Runs the real _install_statusline against the in-tree hooks/ source in an isolated tmpdir, then executes the installed script as a subprocess and asserts the output is the full status line — not the bare '◕‿◕ CodingBuddy' fallback face. Why this complements pytest: - The pytest E2E gate runs inside the test runner's process and fixtures. This shell script exercises the exact code path a user hits, including filesystem layout, chmod, and a fresh subprocess. - It can be run by maintainers locally before pushing release branches without needing to set up a Python venv with pytest. - It is invoked from canary CI in the next commit so PRs cannot merge with a broken installer. Exit codes: 0 — full status line rendered, all assertions passed 1 — fallback face detected or required tokens missing 2 — install crashed before render Local verification at commit time: $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh [verify-install-simulation] PASS: full status line rendered exit=0 Output: '◕‿◕ CB v5.6.1 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6' Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
Adds the v5.6.2 regression gate to two CI workflows so a broken installer cannot reach canary or release: 1. canary.yml — new ``plugin-hooks-tests`` job (Python pytest only, no node/yarn build needed) blocks ``publish-canary`` via ``needs``. Triggered on master/stag-** push, so master merges are gated before any canary npm publish. 2. e2e-plugin.yml — adds the same pytest invocation + simulation script step to the existing ``e2e-plugin-hooks`` job, and extends ``paths`` to include ``packages/claude-code-plugin/tests/**`` and ``scripts/verify-install-simulation.sh`` so PR-level changes trigger the gate. e2e-plugin runs on non-master branches, so PRs see the gate before merge. Together the two jobs cover both PR review (e2e-plugin) and post-merge canary publish (canary), so the v5.6.0/v5.6.1 failure mode (installer ships without lib sync, all users see the fallback face) cannot recur silently. Test invocations cover all five v5.6.2 hotfix test files: - tests/test_atomic_sync_with_lib.py (9 tests) - tests/test_session_start_hud.py (20 tests) - tests/test_install_hook_with_lib.py (4 tests) - tests/test_health_check.py (35 tests) - scripts/verify-install-simulation.sh (subprocess render gate) actions/setup-python pin reused from existing e2e-plugin.yml entry (SHA a26af69be951a213d495a4c3e4e4022e16d87065 = v5.6.0). Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
Bump the workspace version from 5.6.1 to 5.6.2 and publish the CHANGELOG entry for the HUD installer hotfix. v5.6.2 closes the v5.6.0 / v5.6.1 HUD statusLine installer regression (#1490). Every user who upgraded to 5.6.0 or 5.6.1 saw only the fallback face '◕‿◕ CodingBuddy' instead of the full Wave 1/2/3 status line, because the installer never copied the hooks/lib/ modules the script depends on. v5.6.2 routes both the HUD installer and the UserPromptSubmit hook installer through a new _atomic_sync_with_lib helper that copies the script and atomically replaces the entire lib/ directory (rmtree + copytree), guaranteeing renamed/removed modules from prior plugin versions cannot linger. Bump surface: - apps/mcp-server/package.json, src/shared/version.ts - packages/rules/package.json - packages/claude-code-plugin/package.json (+ peerDependencies), .claude-plugin/plugin.json, README.md - .claude-plugin/marketplace.json - yarn.lock - CHANGELOG.md (new [5.6.2] section) Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
…ll-simulation (#1490) Two correctness fixes uncovered while running the script after the v5.6.2 bump: 1. **HOME isolation** — the subprocess that ran the installed HUD inherited the developer's real HOME, so `read_installed_version` tier-1 lookup picked up `~/.claude/plugins/installed_plugins.json` from the developer machine (5.6.1) instead of the version we are about to ship. Output read `CB v5.6.1` even though plugin.json was already 5.6.2. The script now passes an explicit `env=` to subprocess.run with HOME pointing at the tmpdir. 2. **Fake installed_plugins.json** — Claude Code writes `~/.claude/plugins/installed_plugins.json` after every plugin install/update, and `hud_version.get_fresh_version` reads it as tier 1. The simulation now writes a stub manifest in the isolated tmpdir so the installed HUD's version-resolution path matches what a real user will hit on first session-start after `/plugin update`. 3. **Exact version assertion** — the script now reads the version from `plugin.json`, asserts `CB v<expected_version>` appears in the rendered status line, and verifies `~/.claude/hud/.version` contains the same value. This auto-tracks bump-version.sh and removes the previous risk of shipping a release where the version segment renders empty without anyone noticing. Local verification: $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh [verify-install-simulation] stdout='◕‿◕ CB v5.6.2 | Ready 🟢 | $0.42🔥$0.21/m | 2m | 0% | Opus 4.6' [verify-install-simulation] PASS: full status line rendered (v5.6.2) exit=0 Refs #1490
JeremyDev87
added a commit
that referenced
this pull request
Apr 11, 2026
…egment (#1490) Two Critical issues surfaced in CI (e2e-plugin-hooks/3.11) that the local pre-push check missed because the developer/runner HOME leaked into pytest subprocess invocations: 1. **TestHudInstallE2ERegressionGate all 4 scenarios FAILED on CI** with `assert "CB v" in out` because ``hud_version.get_fresh_version``'s tier-1 lookup reads ``~/.claude/plugins/installed_plugins.json`` and CI has none. The render therefore produced ``CB | Ready 🟢 | ...`` — a silent half-broken state where every module imports successfully but the version segment is empty. Locally the same test passed only because the developer's real ``~/.claude/plugins/installed_plugins.json`` (v5.6.1) leaked into the subprocess. Fix: the test now writes a stub ``installed_plugins.json`` into the tmpdir fake home and invokes the subprocess with ``env={"HOME": str(home), ...}`` so all three version-resolution tiers (tier 1 installed_plugins.json, tier 2 plugin.json, tier 3 hud_state) see the isolated environment. Assertion is strengthened to ``CB v<expected_version>`` with ``expected_version`` read from ``plugin.json`` so future bump-version.sh runs auto-gate. A ``.version`` stamp assertion is also added per scenario. The simulation shell script (commit 8) was already hardened this way; this commit brings the pytest E2E suite to parity so CI and local runs agree. 2. **check_hud_installation smoke test did not detect empty version** — it only compared against the literal fallback face, so the ``CB | Ready ...`` half-broken state returned PASS even though the user would see a status line with a missing version segment. Fix: ``check_hud_installation`` now detects ``"CB "`` without ``"CB v"`` in the rendered output and returns FAIL with a clear message ("HUD rendered empty version segment — hud_version fallback chain broken"). The subprocess call also pins ``HOME=self._home_dir`` so the diagnostic honours the HealthChecker's configured home directory rather than leaking the CI runner's real home. A new regression test ``test_fail_when_version_segment_is_empty`` stubs a script that prints the half-broken line and asserts the check FAILs. 3. **test_pass_with_real_plugin_install** (health_check green-path gate) now writes the same fake ``installed_plugins.json`` so the subprocess check_hud_installation invokes can resolve the plugin version inside the isolated environment. Local verification: $ python3 -m pytest tests/test_session_start_hud.py tests/test_health_check.py -v ... 56 passed in 0.90s $ bash packages/claude-code-plugin/scripts/verify-install-simulation.sh [verify-install-simulation] PASS: full status line rendered (v5.6.2) Refs #1490
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Critical hotfix for the v5.6.0 / v5.6.1 HUD statusLine regression. Every user who upgraded saw only
◕‿◕ CodingBuddyinstead of the full Wave 1/2/3 status line. The installer copied the script but never thehooks/lib/modules it depends on._install_statuslineranshutil.copyoncodingbuddy-hud.pyand never touched the siblinglib/. The v5.6.0 refactor (b0fb332) extracted 11hud_*.pymodules +tiny_actor_presets.pytohooks/lib/, so every upgraded user ended up with~/.claude/hud/codingbuddy-hud.pypresent and~/.claude/hud/lib/missing. The script'stry: from hud_buddy import BUDDY_FACEfailed and the outertry/exceptprinted only the fallback face._install_hook_with_libusedcopytree(dirs_exist_ok=True)which writes new files but never removes files that existed before but are gone now — renamed/removed modules from prior versions could linger in~/.claude/hooks/lib/._atomic_sync_with_libhelper performsrmtree + copytreeon the lib directory so both installers are stale-safe.Before / After
Commits (TDD, 8 atomic)
feat(hooks): add _atomic_sync_with_lib helper— new install primitive with 9 RED→GREEN unit testsfix(hud): sync hooks/lib in _install_statusline— HUD installer migration + 8 unit + 4 E2E parametrized tests (clean/partial/stale/fresh scenarios)test(hooks): regression gate for _install_hook_with_lib stale-safe sync— sister-bug gate (4 tests)feat(health): add check_hud_installation diagnostic— HealthChecker check Add Backend Developer Agent #11 with subprocess smoke test (5 tests)ci: add HUD installer regression gate to canary + e2e workflows— gate on both PR-level (e2e-plugin.yml) and post-merge (canary.yml)chore(scripts): add verify-install-simulation.sh pre-release gate— shell script exercising the exact user code pathchore(release): prepare v5.6.2— bump-version + CHANGELOGchore(scripts): isolate HOME and assert exact version in verify-install-simulation— simulation hardening uncovered while running v5.6.2Regression gates (why this cannot recur)
test_session_start_hud.py::TestHudInstallE2ERegressionGate(4 parametrized)test_install_hook_with_lib.py(4 tests)test_health_check.py::TestCheckHudInstallation(5 tests)verify-install-simulation.she2e-plugin.yml+canary.ymlgatesTest results
Pre-existing master failures (
test_hud.py::TestIntegration,test_user_prompt_submit.py::TestMcp*) are unrelated flakes — verified by checking out master and reproducing the same 5 failures.Test plan
yarn workspace codingbuddy-claude-plugin lint— passyarn workspace codingbuddy-claude-plugin typecheck— passyarn workspace codingbuddy-claude-plugin build— passpython3 -m pytest tests/test_atomic_sync_with_lib.py tests/test_session_start_hud.py tests/test_install_hook_with_lib.py tests/test_health_check.py hooks/test_session_start.py— 102 passedbash packages/claude-code-plugin/scripts/verify-install-simulation.sh— PASS (v5.6.2)e2e-plugin-hooksjob green (HUD regression gate + simulation)plugin-hooks-testsjob green (post-merge gate)Refs #1490