test: eliminate remaining cross-file mock.module poisoning in client tests (closes #977) - #1227
Conversation
…tests (closes #977) Converts the 5 remaining active mock.module() poisoners in packages/client (2 targeting routes/__root, 1 each targeting lib/capabilities, hooks/useAppWs, hooks/useCreateWorktree) to DI-seam / spyOn / real-module-plus-injected-value patterns, since bun:test's mock.module() is process-global and readdir-order dependent -- the same hazard class that caused PR #976's CI-only failures. Adds the cross-file-imported-target prohibition to testing.md Anti-Pattern #2 and the conversion how-to (plus the mock.module merge-not-replace semantics discovered while classifying these call sites) to test-standards.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughTesting guidance now restricts cross-file ChangesCross-file mock isolation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Coverage CheckNo production files matching coverage patterns were changed. Rule/Skill Duplication Check✅ No rule paragraphs found verbatim in any skill file. Language Check (public artifacts)✅ All public artifacts use Latin / Greek / Cyrillic scripts only. Source-Comment Blame-Shift Check✅ No new Issue / PR / dated CodeRabbit references in source comments. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
.claude/skills/test-standards/test-standards.md (1)
266-268: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUse explicit process ordering or separate processes instead of ordering-based proof.
The
__directory prefix and filename ordering rely on observed Bun test-file loading behavior, not a guaranteed contract; these documents describe them as normative/deterministic levers, which can change across versions, platforms, randomization, or worker concurrency. Reword this as a version/platform-specific empirical note, or add a driver/preload/process isolation and unique sentinels if the verification needs a stable order.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/skills/test-standards/test-standards.md around lines 266 - 268, The documented verification approach incorrectly presents Bun’s directory and filename ordering as deterministic. Update the guidance in .claude/skills/test-standards/test-standards.md (lines 266-268) and .claude/rules/testing.md (lines 24-29) to label this behavior as version/platform-specific empirical evidence only; recommend explicit process ordering, a driver/preload, or process isolation with unique sentinels for stable proofs, and require verification rather than relying on naming conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/skills/test-standards/test-standards.md:
- Around line 214-218: Update the useAppWsState mock implementation to avoid
returning false as an arbitrary generic cast. Provide a typed fake
AppWebSocketState and invoke the selector with that state, or specialize each
test mock to the concrete return type being tested; preserve the generic
selector contract.
- Around line 196-199: Update the “Pattern 2 — spyOn() on a named export”
guidance to remove the claim that spyOn cannot leak across test files. State
that mockRestore() is still required and that shared module state or concurrent
execution requires test isolation or serial execution for these spies.
---
Nitpick comments:
In @.claude/skills/test-standards/test-standards.md:
- Around line 266-268: The documented verification approach incorrectly presents
Bun’s directory and filename ordering as deterministic. Update the guidance in
.claude/skills/test-standards/test-standards.md (lines 266-268) and
.claude/rules/testing.md (lines 24-29) to label this behavior as
version/platform-specific empirical evidence only; recommend explicit process
ordering, a driver/preload, or process isolation with unique sentinels for
stable proofs, and require verification rather than relying on naming
conventions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 82b6f7b2-b03b-4c0b-91ad-31255dadf12f
📒 Files selected for processing (7)
.claude/rules/testing.md.claude/skills/test-standards/test-standards.mdpackages/client/src/__tests__/routes/WorktreeRow.test.tsxpackages/client/src/components/__tests__/SessionSettings.test.tsxpackages/client/src/components/sessions/hooks/__tests__/useSessionPageState.test.tspackages/client/src/components/worktrees/__tests__/QuickWorktreeDialog.test.tsxpackages/client/src/hooks/__tests__/useCreateWorktree.test.ts
…eneric mock cast Major: the "not other files" claim for spyOn()'s cross-file safety was unconditional as written. State the actual precondition instead -- this repo's client test invocation runs files sequentially (no --concurrent, no concurrentTestGlob in bunfig.toml), which is what makes mockRestore() in afterEach sufficient; name the concurrent-execution case where it would not hold. Minor: replace the useAppWsState mock's arbitrary `false as T` cast with a selector(fakeState) example that actually exercises the generic contract, and note when a documented cast (as the existing routes/__tests__/index.test.tsx example already does) is the acceptable exception. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
Closes #977
What
Converts the 5 remaining active
mock.module()call sites inpackages/client(across 4 target modules) to cross-file-safe patterns, and codifies the prohibition + conversion how-to. Full suite green, both directions of every poisoner/victim pair verified.Phase 1 — classification (all 5 call sites are active poisoners)
hooks/__tests__/useCreateWorktree.test.ts:7routes/__rootuseWorktreeCreationTasksContextroutes/__root(verified directly); transitively,routes/index.tsx'sRepositoryCard(rendered byroutes/__tests__/index.test.tsxfor the >=1-repo case)components/__tests__/SessionSettings.test.tsx:98routes/__rootuseWorktreeDeletionTasksContext,WorktreeDeletionTasksContext__tests__/routes/WorktreeRow.test.tsx(importsWorktreeDeletionTasksContextdirectly)__tests__/routes/WorktreeRow.test.tsx:10lib/capabilitieshasVSCode,getVSCodeOpenMode,getVSCodeRemoteHost(notsetCapabilities)routes/__tests__/index.test.tsx(spies on the real module)components/sessions/hooks/__tests__/useSessionPageState.test.ts:24hooks/useAppWsuseAppWsEvent,useAppWsState(the module's only 2 exports)routes/__tests__/index.test.tsx,__tests__/routes/agents/index.test.tsx(both spy on the real module)components/worktrees/__tests__/QuickWorktreeDialog.test.tsx:11hooks/useCreateWorktreeuseCreateWorktree(the module's only export)hooks/__tests__/useCreateWorktree.test.ts(its own unit-under-test)Zero benign/self-consumed cases this round; scope matched the
routes/__root-centric shape the Issue anticipated (2 of 5 call sites target__root), so no PR-split was needed.A note on classification methodology (worth flagging)
Bun's
mock.module(specifier, factory)mergesfactory()'s return onto the real module rather than fully replacing it — an export the factory doesn't declare falls through to the real implementation for every importer. This means "does the victim's test suite still pass" is not a reliable signal for classification: a victim can receive a genuinely poisoned export and still pass every assertion if it happens to be structurally tolerant (e.g. it re-derives both a Provider and consumer from the same poisoned reference, so both sides stay mutually consistent without ever touching the real module).Classification here was done by object-identity checks (does the victim's imported Context
===the poisoner's locally-created mock Context?) and source-string checks (does the victim's resolved function's.toString()match the poisoner's factory body?), forcing deterministic poisoner-then-victim load order via a lexicographically-earlier-sorting temp file copy (Bun's scheduler does not respect CLI argument order or simple directory position — verified empirically, not assumed). Documented intest-standards.md's new section for future classification work.Phase 2 — conversions
routes/__root(both mockers): replaced with the realWorktreeCreationTasksContext/WorktreeDeletionTasksContextProviders fromcontexts/root-contexts.ts(the moduleroutes/__root.tsxre-exports from, without pulling in the router-registration barrel) wrapping an injected fake value. Zero production changes.lib/capabilities: replaced with the module's own realsetCapabilities()setter (already existed for exactly this purpose). Zero production changes.hooks/useAppWs: replaced withspyOn()+.mockRestore()inafterEach, matching the pattern already used byroutes/__tests__/index.test.tsx/__tests__/routes/agents/index.test.tsxfor the same module.hooks/useCreateWorktree: replaced withspyOn()+.mockRestore()inafterEach.Regression verification — polarity + order-independence
A plain "full suite is green" only shows the current readdir order happens not to expose the poisoning (that's exactly how #970/PR #976's bug hid locally and only surfaced in CI). Explicit polarity evidence per converted poisoner, all pairs run in the same bun:test process with deterministically forced load order:
HEADfor just the poisoner+victim pair, forced poisoner-first): every pair demonstrably leaks.useCreateWorktree.test.ts→routes/__root: a real re-import'suseWorktreeCreationTasksContextresolves to the poisoner's canned closure body.SessionSettings.test.tsx→routes/__root:WorktreeRow.test.tsx's importedWorktreeDeletionTasksContextisObject.is-identical to the poisoner's locally-created mock Context.WorktreeRow.test.tsx→lib/capabilities: a real re-import'shasVSCoderesolves to the poisoner's() => false, not the real cached-value reader (setCapabilities, not overridden, stays real — confirming the merge, not full-replace, semantics).useSessionPageState.test.ts→hooks/useAppWs: a real re-import'suseAppWsEventresolves to the poisoner's callback-capturing closure.QuickWorktreeDialog.test.tsx→hooks/useCreateWorktree: forcing this pair againstuseCreateWorktree.test.ts(its own unit test) drops 4 of 13 tests to failing, because the victim's own hook-under-test is silently replaced by the poisoner's canned mock.bun testarguments) is green with the full expected test count, for all 5 pairs.Phase 3 — rule / skill updates
.claude/rules/testing.mdAnti-Pattern docs: add development workflow and project guidelines to CLAUDE.md #2: added the cross-file-imported-target prohibition, a prohibited example (routes/__root) and a permitted example (self-consumed target), plus a pre-implementation-checklist bullet..claude/skills/test-standards/test-standards.md: added the 3-pattern conversion how-to (DI seam /spyOn/ real-module-plus-injected-fake) with worked code examples referencing this PR and PR feat(client): scroll-back history paging over archived output (closes #959) #976, plus the merge-semantics + load-order-control methodology note above.Out of scope (per Issue AC)
A mechanical lint gating new cross-file-imported
mock.module()targets requires import-graph analysis and is filed as a follow-up: #1226.Test plan
bun run test(full workspace: client 2075, shared 596, integration 73, server 3653, embedded-agent 287, scripts/hooks 483) — all green,TEST_EXIT: 0bun run typecheck— clean (part ofbun run test)bun run check:lang— clean (113 files scanned)node .claude/skills/orchestrator/preflight-check.js— clean (no coverage gaps, no rule/skill duplication, no language violations, no blame-shift comments)coderabbit review --agent --base main🤖 Generated with Claude Code
CodeRabbit status (2026-07-27, final) — CLEAN
main's red state (#1225) resolved via PR #1229; head rebased onto it (5cfe359f). First genuine CodeRabbit walkthrough on5cfe359f(after two rate-limit rounds -- local CLI: headless-worktreeautomatic_login_failed, structural; GitHub bot: rate-limited at PR open, then the "Review finished... does not re-review" quirk on a same-commit retrigger, then a fresh rate-limit on auto-review-on-push) returned 2 actionable findings, both intest-standards.md:spyOn()cross-file-safety claim was stated as an unconditional guarantee; it actually depends on this repo's test files running sequentially (no--concurrent, noconcurrentTestGlob). Fixed: named the precondition explicitly and the concurrent-execution case where it would not hold.useAppWsStatemock example used an arbitraryfalse as Tcast instead of exercising the real selector contract. Fixed: replaced with aselector(fakeState)example, kept the cast form only as a documented exception (matching the existingroutes/__tests__/index.test.tsxprecedent).Fix commit
abc6efb7pushed; verified against actual repo config before writing (no--concurrentflag inpackages/client/package.json's test script, noconcurrentTestGlobinbunfig.toml). Re-review onabc6efb7(after a third rate-limit round, cleared and manually retriggered) returned a genuine walkthrough: "No actionable comments were generated in the recent review." Zero new findings; both prior findings confirmed resolved, not carried forward.3-layer verdict on
abc6efb7: pre-merge checks clean, CodeRabbit commit statusstate: success/description: "Review completed"(verified via the walkthrough body, not just the status line, per the state-vs-description trap), inline actionable comments = 0,reviewDecisionempty (walkthrough-exists clean-equivalent case percoderabbit-opstroubleshooting -- a genuine review ran, found nothing to flag, and CodeRabbit does not always submit a formalAPPROVEDevent for zero-finding passes). Independent Architect code-appropriateness audit (session56b4d446, gen-2): CLEAN -- confirmed zero production changes, verified the context-identity chain betweenroutes/__root.tsxandcontexts/root-contexts.ts, verified no hidden dependency on the removedSessionSettingsmock's no-Provider fallback branch, and independently confirmed the CR-Major fix's config claims and the documented-cast exception's premise.Note on CI
The
testjob's failure is a confirmed pre-existingmain-branch flake (this.#handle.unref is not a functioninpackages/client/src/lib/api.ts'sopenPath), already tracked in #1225 and reproduced identically onmain's own most recent CI run (2026-07-23). This PR's diff never touchespackages/client/src/lib/api.ts; locallybun run test(full workspace) is green withTEST_EXIT: 0. All other CI jobs (preflight, language-lint, comment-blame-shift-lint, check-structure, CodeQL/Analyze) pass.Summary by CodeRabbit
Tests
Documentation