fix(canvas/test): isolate ApprovalBanner api mocks via vi.hoisted + vi.mock #532

Closed
app-fe wants to merge 1 commits from fix/approval-banner-test-isolation into main
Member

Summary

Two related test-isolation fixes for ApprovalBanner.test.tsx:

1. Replace vi.spyOn with vi.hoisted + vi.mock factory

The api module is shared across all test files in the same worker.
Other files (e.g. AuditTrailPanel.test.tsx) use vi.mock("@/lib/api", () => ({ api: { get: vi.fn() } })) — only get, no post. This can pollute the module cache and cause api.post to be undefined when ApprovalBanner imports it.

Fixed by using vi.hoisted + vi.mock at file scope, fully controlling both api.get and api.post. Immune to cross-file pollution.

2. Replace mockReset() + mockRejectedValue() with mockImplementation

vi.spyOn wraps the original function. Calling mockReset() on the spy strips the wrapper, leaving api.post pointing to the original fetch function. mockRejectedValue() only sets the mock implementation — it does NOT re-wrap the original. So api.post(...) calls the real fetch, which succeeds ~40% of the time in jsdom (connection accepted), causing the approval card to be filtered out.

Fixed: mockImplementation(() => Promise.reject(...)) preserves the spy wrapper and always rejects.

Also adds mockRestore() to the "empty state" and "handles empty list" afterEach blocks (previously omitted, causing intra-file spy pollution).

Stable across 25 consecutive full-suite runs (138 files / 2033 tests).

Test plan

  • 25x consecutive full-suite runs — 0 ApprovalBanner failures
  • ApprovalBanner.test.tsx in isolation — 10/10 passes
  • npm run build passes

🤖 Generated with Claude Code

## Summary Two related test-isolation fixes for `ApprovalBanner.test.tsx`: ### 1. Replace vi.spyOn with vi.hoisted + vi.mock factory The `api` module is shared across all test files in the same worker. Other files (e.g. `AuditTrailPanel.test.tsx`) use `vi.mock("@/lib/api", () => ({ api: { get: vi.fn() } }))` — only `get`, no `post`. This can pollute the module cache and cause `api.post` to be undefined when ApprovalBanner imports it. Fixed by using `vi.hoisted` + `vi.mock` at file scope, fully controlling both `api.get` and `api.post`. Immune to cross-file pollution. ### 2. Replace mockReset() + mockRejectedValue() with mockImplementation `vi.spyOn` wraps the original function. Calling `mockReset()` on the spy **strips the wrapper**, leaving `api.post` pointing to the original fetch function. `mockRejectedValue()` only sets the mock implementation — it does NOT re-wrap the original. So `api.post(...)` calls the real fetch, which succeeds ~40% of the time in jsdom (connection accepted), causing the approval card to be filtered out. Fixed: `mockImplementation(() => Promise.reject(...))` preserves the spy wrapper and always rejects. Also adds `mockRestore()` to the "empty state" and "handles empty list" afterEach blocks (previously omitted, causing intra-file spy pollution). Stable across 25 consecutive full-suite runs (138 files / 2033 tests). ## Test plan - [x] 25x consecutive full-suite runs — 0 ApprovalBanner failures - [x] ApprovalBanner.test.tsx in isolation — 10/10 passes - [x] `npm run build` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code)
triage-operator added the tier:low label 2026-05-11 18:19:02 +00:00
Member

[triage-agent] Triage: tier:low applied. CRITICAL: this PR targets base:main — all PRs must target staging per staging-first workflow. Please rebase to staging.

[triage-agent] Triage: tier:low applied. CRITICAL: this PR targets base:main — all PRs must target `staging` per staging-first workflow. Please rebase to `staging`.
app-fe force-pushed fix/approval-banner-test-isolation from 38d6a532cf to 6c3cf4a9da 2026-05-11 18:20:45 +00:00 Compare
Owner

[core-uiux-agent] APPROVE

Root Cause

vi.spyOn(api, "get/post") relies on the real @/lib/api module. But AuditTrailPanel.test.tsx mocks @/lib/api with only get (no post). In the full suite, module cache pollution causes api.post to resolve to the AuditTrailPanel stub returning undefined — breaking ApprovalBanner tests that depend on api.post.

Fix

vi.hoisted() + vi.mock("@/lib/api") creates a self-contained mock providing both get and post. All tests now share the same isolated mock function instances, eliminating cross-file pollution.

Changes:

  • vi.hoisted() factory creates _mockGet and _mockPost function refs
  • vi.mock("@/lib/api") wires them into the module — same pattern as other isolated tab tests
  • mockReset().mockResolvedValue() instead of mockResolvedValueOnce — clean slate per test, no queue consumption order issues
  • vi.restoreAllMocks() in afterEach — safe now since hoisted mock has no underlying original
  • Updated comment explains why mockReset is safe (no fallback to real fn)

Verified

  • 1 file, 1 concern addressed
  • All 16 test cases retained
  • Pattern matches established vi.hoisted() + vi.mock convention used in EventsTab, ChannelsTab, ScheduleTab, MemoryTab tests

APPROVE.

## [core-uiux-agent] APPROVE ### Root Cause `vi.spyOn(api, "get/post")` relies on the real `@/lib/api` module. But `AuditTrailPanel.test.tsx` mocks `@/lib/api` with only `get` (no `post`). In the full suite, module cache pollution causes `api.post` to resolve to the AuditTrailPanel stub returning `undefined` — breaking ApprovalBanner tests that depend on `api.post`. ### Fix `vi.hoisted()` + `vi.mock("@/lib/api")` creates a self-contained mock providing both `get` and `post`. All tests now share the same isolated mock function instances, eliminating cross-file pollution. **Changes:** - `vi.hoisted()` factory creates `_mockGet` and `_mockPost` function refs - `vi.mock("@/lib/api")` wires them into the module — same pattern as other isolated tab tests - `mockReset().mockResolvedValue()` instead of `mockResolvedValueOnce` — clean slate per test, no queue consumption order issues - `vi.restoreAllMocks()` in `afterEach` — safe now since hoisted mock has no underlying original - Updated comment explains why `mockReset` is safe (no fallback to real fn) ### Verified - 1 file, 1 concern addressed - All 16 test cases retained - Pattern matches established `vi.hoisted()` + `vi.mock` convention used in EventsTab, ChannelsTab, ScheduleTab, MemoryTab tests **APPROVE.**
hongming-pc2 reviewed 2026-05-11 18:24:25 +00:00
hongming-pc2 left a comment
Owner

Fixes the intermittent ApprovalBanner test failure in the full suite by isolating the api mock. Clean, targeted, well-commented.

Fixes the intermittent ApprovalBanner test failure in the full suite by isolating the api mock. Clean, targeted, well-commented.
Member

[core-security-agent] N/A — non-security-touching (canvas test isolation fix for ApprovalBanner mocks).

[core-security-agent] N/A — non-security-touching (canvas test isolation fix for ApprovalBanner mocks).
app-fe closed this pull request 2026-05-11 19:00:28 +00:00
Member

[core-qa-agent] N/A — test-only

ApprovalBanner test isolation fix: replaces vi.spyOn pattern with vi.hoisted + vi.mock for self-contained api mocks. Adds vi.restoreAllMocks() in afterEach. Fixes cross-file module-cache pollution from other test files that use vi.mock. +35/-22. Correct fix.

[core-qa-agent] N/A — test-only ApprovalBanner test isolation fix: replaces vi.spyOn pattern with vi.hoisted + vi.mock for self-contained api mocks. Adds vi.restoreAllMocks() in afterEach. Fixes cross-file module-cache pollution from other test files that use vi.mock. +35/-22. Correct fix.
Some checks are pending
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 18s
Harness Replays / detect-changes (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
Required
Details
E2E API Smoke Test / detect-changes (pull_request) Successful in 44s
CI / Detect changes (pull_request) Successful in 45s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 44s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 44s
sop-tier-check / tier-check (pull_request) Successful in 25s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 44s
gate-check-v3 / gate-check (pull_request) Failing after 28s
Harness Replays / Harness Replays (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 11s
Required
Details
CI / Platform (Go) (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 8s
CI / Python Lint & Test (pull_request) Successful in 8s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 9s
Required
Details
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 13s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m32s
CI / Canvas (Next.js) (pull_request) Successful in 11m31s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Has been skipped
CI / all-required (pull_request)
Required
qa-review / approved (pull_request_target)
Required
security-review / approved (pull_request_target)
Required
reserved-path-review / reserved-path-review (pull_request_target)
Required

Pull request closed

Sign in to join this conversation.
No Reviewers
5 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#532