Repository navigation
fix(tests): isolate notify-slack case i from the host's CLAUDE_CONFIG_DIR - #2204
Conversation
…_DIR case_i redirects HOME to a temp dir, but claude_home_path() prefers $CLAUDE_CONFIG_DIR (set on every claude-sutando install) over ~/.claude — so the redirect was bypassed and the host's REAL channels/slack/.env leaked into the assertions: 3 failures on any slack-enrolled host, with the live bot token printed verbatim in the failure messages. Fix: save/pop CLAUDE_CONFIG_DIR and CLAUDE_HOME alongside HOME (restored in finally), and mask non-fixture values in failure output so a future isolation regression can't print a live token into CI logs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 57%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changesNo lines with coverage information in this diff. |
|
Approval signal for head The change is narrowly scoped to the notify-slack test isolation path. Case i now clears both Focused checks in the isolated
GitHub CI, diff coverage, CLA, and policy lint jobs are green on this head. Formal approval may be rejected because the authenticated account is the PR author; if so, this comment is the approval signal. Reviewed by Qingyun's Codex. |
john-the-dev
left a comment
There was a problem hiding this comment.
Approve — clean, correct, security-conscious test-only fix (closes #2203).
Verified:
- Root cause is real:
claude_home_path()prefers$CLAUDE_CONFIG_DIR, which is set on every claude-sutando install, so case i's HOME redirect was silently bypassed and the host's realchannels/slack/.envresolved into the assertions — 3 deterministic failures on any slack-enrolled host, with the live bot token printed verbatim into test output. Vanilla-env CI never saw it. Good catch. - Fix is right:
CLAUDE_CONFIG_DIR+CLAUDE_HOMEare popped alongside HOME and restored infinallywith the sameif saved is not Noneguard as the existing HOME/SLACK_BOT_TOKEN handling — resolution now falls to the redirected~/.claudeexactly as the fixture intends. - Defense-in-depth: the
masked()allowlist (xoxb-from-channel,xoxb-from-repo,"") prints the three expected fixtures normally and reduces anything else to<non-fixture N-char value, masked>— so a future isolation regression fails loudly without leaking a secret. - Issue-linked per queue policy; supersedes the stale #1903.
merge=BEHIND (needs update-branch) but no code hazards. LGTM.
liususan091219
left a comment
There was a problem hiding this comment.
Justification: [change-requested: missing_before_after]
- missing_before_after: Add before/after test output showing host env isolation and masking.
|
Before/after showing both halves of the ask — host-env isolation and masking — reproduced with a synthetic token so no real secret was ever involved. This host has no Before ( (I redacted the sentinel above; the raw run printed it verbatim, which is the point.) Both failure modes visible at once: the After (this branch) — So the isolation half is doing the work (the fixture values win again), and the masking half means that even if isolation regresses later, the failure message can't print a non-fixture value. Why the masking matters independently of the isolation fix, and I think this is the stronger half: isolation can be re-broken by any future env var that One note on method: I deliberately did not run |
Before / after — plus a defect in this PR and a design question it exposed1. The isolation fix works (before / after, this host)
Case i fails on 2. This PR ships a broken new test —
|
Retraction — my previous comment was wrong. I tested against the wrong tree.Almost everything in my earlier comment is invalid. Correcting it in full. What I did wrong: I ran the "before" and "after" from a local clone sitting on Re-run in a clean worktree checked out at Point by point:
So the only surviving finding is the one this PR actually fixes: I also filed an internal product question ("should a never-resolving failure re-DM daily or never?") on the strength of the bogus finding. Withdrawing it — the behaviour I thought I'd found doesn't exist. The lesson, since it invalidated a whole comment: "before/after" is worthless unless the "before" is the tree the reviewer will merge into. I never checked what branch the clone was on. A |
Retracted: this change-request was auto-posted by a triage sweep that has been stopped per the maintainer. Dismissing so it no longer blocks the PR.
|
@cla-assistant check |
Review blocker on this PR: `load_bridge()` set the process-wide CLAUDE_CONFIG_DIR to a temp dir and never restored it. Why that is not cosmetic: `claude_home_path()` reads $CLAUDE_CONFIG_DIR at CALL time, not at import, so the mutation outlives this file. In the standalone-test loop every sibling fixture that afterwards seeds `Path.home()/".claude"` then reads a different config root than the bridge does, which is the downstream failure reported in the open review thread. The leak is invisible from inside this test — it only breaks the NEXT one. - `temp_claude_config()`: a context manager that saves the prior value, points it at a throwaway dir, and puts it back in a `finally` (so a failing assertion cannot skip the restore) before removing the temp tree. - `load_bridge(config_root)` now takes the dir instead of creating and leaking one; the #2204 host-leakage guard it was added for is unchanged — the caller's real config is still never written to. - `main()` asserts the caller's environment came back, so this cannot silently regress: an unrestored var is otherwise undetectable here. Evidence — the assertion can say NO. With the restore deliberately disabled: $ CLAUDE_CONFIG_DIR=/tmp/fake-ccd python3 tests/discord-bridge-collaborator-tier.test.py FAIL: team-collaborator path has issues: - CLAUDE_CONFIG_DIR not restored: '/tmp/fake-ccd' -> '/var/.../dbct-claude-home-yffcc76n' and with the fix in place, on the clean non-default sequence the review named: $ CLAUDE_CONFIG_DIR=/tmp/fake-ccd python3 tests/discord-bridge-collaborator-tier.test.py PASS: discord-bridge.py team-collaborator engage path is correct. caller env after the run: /tmp/fake-ccd (unchanged) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyfS386nrKn8FFdtRLme6x
…config dir (#2357) * fix(tests): stop seeding a stub Discord token into the caller's real config dir load_bridge() seeded DISCORD_BOT_TOKEN into $CLAUDE_CONFIG_DIR/channels/discord/.env when absent -- the caller's REAL config directory. On a machine with no Discord install that fabricates one: health-check then reports 'discord-bridge: configured but not running' indefinitely, and a stub token is left sitting in the user's config dir. Hit this for real: running the bridge suites during unrelated work created the file, and the false warning appeared in the next health check. Seed into a temp CLAUDE_CONFIG_DIR instead. Same host-leakage class as #2204. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ * fix(test): restore CLAUDE_CONFIG_DIR — the leak outlived this module Review blocker on this PR: `load_bridge()` set the process-wide CLAUDE_CONFIG_DIR to a temp dir and never restored it. Why that is not cosmetic: `claude_home_path()` reads $CLAUDE_CONFIG_DIR at CALL time, not at import, so the mutation outlives this file. In the standalone-test loop every sibling fixture that afterwards seeds `Path.home()/".claude"` then reads a different config root than the bridge does, which is the downstream failure reported in the open review thread. The leak is invisible from inside this test — it only breaks the NEXT one. - `temp_claude_config()`: a context manager that saves the prior value, points it at a throwaway dir, and puts it back in a `finally` (so a failing assertion cannot skip the restore) before removing the temp tree. - `load_bridge(config_root)` now takes the dir instead of creating and leaking one; the #2204 host-leakage guard it was added for is unchanged — the caller's real config is still never written to. - `main()` asserts the caller's environment came back, so this cannot silently regress: an unrestored var is otherwise undetectable here. Evidence — the assertion can say NO. With the restore deliberately disabled: $ CLAUDE_CONFIG_DIR=/tmp/fake-ccd python3 tests/discord-bridge-collaborator-tier.test.py FAIL: team-collaborator path has issues: - CLAUDE_CONFIG_DIR not restored: '/tmp/fake-ccd' -> '/var/.../dbct-claude-home-yffcc76n' and with the fix in place, on the clean non-default sequence the review named: $ CLAUDE_CONFIG_DIR=/tmp/fake-ccd python3 tests/discord-bridge-collaborator-tier.test.py PASS: discord-bridge.py team-collaborator engage path is correct. caller env after the run: /tmp/fake-ccd (unchanged) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyfS386nrKn8FFdtRLme6x * test(discord): seed the token under the CONFIG ROOT, via one shared helper Review blocker (#2357, john-the-dev 2026-07-30T02:24): removing this PR's caller-config pollution exposed a supported split-config sequence that is red. Three fixtures seeded `Path.home() / ".claude" / channels/discord/.env`, which is the WRONG root whenever CLAUDE_CONFIG_DIR is set — i.e. in every real split-config install. All three failed with `DISCORD_BOT_TOKEN not set in $CLAUDE_CONFIG_DIR/channels/discord/.env`. Those three were NOT introduced by the leak fix. They were always wrong about the root; the unrestored CLAUDE_CONFIG_DIR from discord-bridge-collaborator-tier happened to leave a config root containing a token, so later tests in the same standalone run inherited it and passed for a reason unrelated to their own setup. Newly exposed is not newly introduced — so the repair is to align the fixtures with the production contract, not to restore the pollution. - tests/_helpers/discord_env.py: `seed_discord_token()` resolves the root the way the BRIDGE resolves it ($CLAUDE_CONFIG_DIR, else ~/.claude) instead of picking one half. A fixture that hardcodes either half is correct only by coincidence, which is the whole bug. Never overwrites an existing .env, so a developer's real token survives a test run; idempotent for several fixtures in one process. - The three fixtures now call it. - tests/discord-bridge-split-config-root.test.py: the reviewer's reproduction as a test rather than a paragraph. Runs the four fixtures with FRESH, DISTINCT HOME and CLAUDE_CONFIG_DIR, requires exit 0 from each, and then asserts WHERE the token landed. Sequence order is deliberate — collaborator-tier runs first because it is the fixture whose leak used to mask the others, and reversing the order would hide the interaction under test. Both halves of that regression matter: "all four exit 0" alone would also pass if the fixtures stopped seeding entirely, so the location assertions are what pin the contract. Evidence — the reviewer's exact sequence, fresh distinct roots: AFTER BEFORE (control) collaborator-tier exit=0 exit=0 reply-directive exit=0 exit=1 mod-judge-trackers exit=0 exit=1 mod-judge-codex exit=0 exit=1 token under $CLAUDE_CONFIG_DIR: yes no token under HOME/.claude: no yes The new regression FAILS 5 of 6 checks on the pre-fix tree, including both location assertions, so it discriminates rather than merely passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyfS386nrKn8FFdtRLme6x * fix(tests): give each Discord fixture its own config root, not the caller's Addresses john-the-dev's [P1] on b279402: the previous cut fixed WHICH root the bridge reads but not WHOSE. `seed_discord_token()` wrote under the ambient root, so a fixture run with a real CLAUDE_CONFIG_DIR left a 34-byte stub credential in the caller's actual config dir — fabricating a Discord install on a machine that has none. That is the production symptom this PR exists to fix, relocated rather than removed. Reviewer's repro, reproduced verbatim before changing anything: HOME=<fresh> CLAUDE_CONFIG_DIR=<fresh> \ python3 tests/discord-bridge-reply-directive.test.py -> exit 0, and <fresh-ccd>/channels/discord/.env == 34 bytes of stub token Fix: `temp_config_root()` in tests/_helpers/discord_env.py gives each fixture a throwaway root, seeds it there, and restores the caller's environment in a try/finally — restoring ABSENCE with a pop rather than assigning "", since those are different states to any reader that does not .strip(). The bridge resolves its token at exec time, so the temp root only has to be live across the exec. The cross-fixture regression now asserts ISOLATION, which is the half that was missing. Its previous assertion — that the token landed under the caller's config root — verified routing while *requiring* the pollution. Routing is still pinned by the four exit-0 checks: a fixture seeding the wrong root cannot load the bridge. Verification: * all four fixtures, fresh distinct HOME+CLAUDE_CONFIG_DIR: exit 0, caller's config dir left with zero entries * tests/discord-bridge-split-config-root.test.py: PASS (7 checks) * CONTROL — reverted only the helper+fixture change, kept the new assertions: the four exit-0 checks still PASS while both isolation checks FAIL ("unexpected entries: ['channels']"). The old test could not have caught this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com> Co-authored-by: john-the-dev <johnv.the.dev@gmail.com> Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>
closes #2203
Problem
tests/health-check-notify-slack.test.pycase i redirects$HOMEto a temp dir to test token-path resolution — butclaude_home_path()prefers$CLAUDE_CONFIG_DIR, which is set on every claude-sutando install. The HOME redirect is silently bypassed and the host's realchannels/slack/.envresolves into the assertions.Effect on any slack-enrolled host: 3 deterministic failures — and the failure messages print the live bot token verbatim into test output/CI logs/agent transcripts (see #2203; this bit us 2026-07-20 during integration-branch suite verification). Vanilla env passes, which is why CI never saw it.
Fix (test-only, but issue-linked per queue policy)
CLAUDE_CONFIG_DIR+CLAUDE_HOMEalongsideHOMEin case i, restored infinally— resolution falls to the redirected~/.claudeexactly as the fixture intends.Supersedes #1903 (same diagnosis; auto-closed for missing issue linkage, branch since stale).
Verification
CLAUDE_CONFIG_DIR→ real enrolled config dir: was 3 failures → now 10/10 pass.🤖 Generated with Claude Code