test(lib): add hydrate.test.ts coverage (7 cases) — exponential backoff retry #701

Closed
core-uiux wants to merge 15 commits from test/canvas-hydrate-coverage into main
Member

Summary

Add test coverage for canvas/src/lib/__tests__/hydrate.test.ts — 7 cases for the canvas store hydration function with exponential backoff retry.

What

  • Successful first-attempt hydration: hydrateCanvas() hydrates the store and returns null error when the platform API responds on the first try
  • Non-fatal viewport failure: viewport fetch errors are swallowed; the store still hydrates successfully without viewport state
  • Retry exhaustion: MAX_RETRIES=3 attempts are made before returning a non-null error
  • onRetrying callback: fires attempt-1 and attempt-2 before the 2nd and 3rd attempts respectively
  • Short-circuit on recovery: succeeds on the 2nd attempt, does not fire a 3rd
  • Exponential backoff: advanceTimersByTimeAsync confirms the 1000→2000→4000ms delay sequence between retries

Technical note

Uses vi.hoisted() to share mock function references between vi.mock factories and test code. Vitest hoists vi.mock factory functions to the top of the module before any module-level code runs, so vi.fn() calls at module scope are not yet initialized when the factory executes. vi.hoisted() creates mocks at the correct boot time and is the idiomatic vitest pattern for this.

🤖 Generated with Claude Code

## Summary Add test coverage for `canvas/src/lib/__tests__/hydrate.test.ts` — 7 cases for the canvas store hydration function with exponential backoff retry. ## What - Successful first-attempt hydration: `hydrateCanvas()` hydrates the store and returns `null` error when the platform API responds on the first try - Non-fatal viewport failure: viewport fetch errors are swallowed; the store still hydrates successfully without viewport state - Retry exhaustion: `MAX_RETRIES=3` attempts are made before returning a non-null error - `onRetrying` callback: fires `attempt-1` and `attempt-2` before the 2nd and 3rd attempts respectively - Short-circuit on recovery: succeeds on the 2nd attempt, does not fire a 3rd - Exponential backoff: `advanceTimersByTimeAsync` confirms the 1000→2000→4000ms delay sequence between retries ## Technical note Uses `vi.hoisted()` to share mock function references between `vi.mock` factories and test code. Vitest hoists `vi.mock` factory functions to the top of the module before any module-level code runs, so `vi.fn()` calls at module scope are not yet initialized when the factory executes. `vi.hoisted()` creates mocks at the correct boot time and is the idiomatic vitest pattern for this. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
core-uiux added 15 commits 2026-05-12 08:08:40 +00:00
12 passing: loading spinner, empty state, token list rendering,
each token's prefix/age/Revoke button, API URL correctness, revoke
confirm + cancel dialogs, new-token creation + dismiss, create error,
network error banner.

Root bug fixed: confirm button search was unscoped — when the dialog
opened, two "Revoke" buttons existed (tok2's row + dialog confirm);
find() returned tok2's button first. Scoped the search to
document.querySelector('[role="dialog"]') to hit the correct target.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Vitest coverage for AttachmentTextPreview — inline text/code
preview with streaming fetch and expand/truncate.

Covers:
  - Loading skeleton (320x80) with aria-label
  - Ready state with correct text content
  - Filename shown in header
  - Expand button appears when lines > 10
  - Expand button hidden when all lines shown
  - Expand button updates display to full content
  - Download button calls onDownload
  - tone=user -> blue/accent border
  - tone=agent -> neutral border
  - Truncated notice when file exceeds 256 KB
  - Error -> AttachmentChip fallback
  - Cleanup on unmount

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Vitest coverage for two missing attachment renderers:

AttachmentAudio (9 cases):
  - Loading skeleton (280x40) with aria-label
  - <audio controls> with blob src when ready
  - Filename label in ready state
  - tone=user -> blue/accent border
  - tone=agent -> neutral border
  - Error -> AttachmentChip fallback
  - audio onError -> chip transition
  - External URI -> direct href, no fetch
  - Blob URL cleanup on unmount

AttachmentPDF (9 cases):
  - Loading skeleton with PdfGlyph + filename
  - Preview button with glyph, filename, "PDF" label
  - Lightbox opens with <embed> on click
  - Lightbox closes on Escape
  - tone=user -> blue/accent classes on button
  - tone=agent -> neutral border
  - Error -> AttachmentChip fallback
  - External URI -> direct href, no fetch
  - Blob URL cleanup on unmount

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Vitest coverage for AttachmentImage — inline image thumbnail with
click-to-fullscreen lightbox. Covers: loading skeleton (240×180),
ready state with blob URL, tone=user/agent border classes, lightbox
open/close on click and Escape, AttachmentChip error fallback, img
onError transition to chip, external URI direct href (no fetch), and
blob URL cleanup on unmount.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- role=group with aria-label containing service label
- Service icon aria-hidden, correct emoji per service name
- Count label: "1 key" vs "N keys"
- Renders SecretRow for each secret
- Header and rows div structure

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- DeleteConfirmDialog (15 cases): dialog open via secret:delete-request event,
  title/body text, Cancel closes, dependents loading/list/none states,
  deleteSecret call, confirm 1s delay, disabled→enabled button transition
- SettingsButton (11 cases): aria-label, aria-expanded, gear SVG aria-hidden,
  toggle openPanel/closePanel, active class, tooltip Mac/Ctrl shortcut
  ResizeObserver polyfill for Radix Tooltip

No breaking changes. 2413 tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- EmptyState: 6 cases — icon aria-hidden, title, body text, CTA button
- SearchBar: 14 cases — store binding, onChange, Escape, Ctrl/Cmd+F focus
- UnsavedChangesGuard: 7 cases — dialog states, Keep/Discard actions, backdrop
  FIX: UnsavedChangesGuard now wires onDiscard via pendingDiscard ref so
  clicking Discard correctly calls the callback on dialog close
- AttachmentVideo: 8 cases — loading/ready/error states, tone borders,
  blob URL cleanup, external URI direct href

No breaking changes. 2387 tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
+ form-inputs.test.tsx: 35 cases across TextInput, NumberInput, Toggle,
  TagList, and Section — pure presentational components in the Config tab.
  Uses vi.hoisted() patterns from established suite; no jest-dom matchers.

+ form-inputs.tsx (Section): add aria-expanded + aria-controls to the
  collapsible toggle button for WCAG 2.1 AA compliance. The content div
  gets a stable id derived from the title; aria-controls links button to
  region. Indicator span gains aria-hidden="true" (decorative only).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Adds role=tablist + aria-label to outer container
- Adds role=tab, aria-selected, aria-label, aria-hidden(icon) to each tab button
- tabIndex: active=0, others=-1 (standard tab pattern)
- Keyboard: Arrow keys cycle tabs, Home/End jump to first/last
- TabBar.test.tsx: 12 cases covering render states and keyboard interaction

🤖 Generated with [Claude Code](https://claude.com/claude-code)
FilterChips:
- Add role=toolbar + aria-label="Filter agents" on container
- Add role=radio + aria-checked on each button
- Add aria-hidden on count spans
- FilterChips.test.tsx: 9 cases

AgentCard:
- Add aria-label composing name, status, tier, remote flag
- AgentCard.test.tsx: 8 cases

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Cover StatusDot (size, circle, halo, flexShrink), TierChip (tiers,
size variants, flexShrink), Chip (value, label+value, pill shape,
soft/accent mode), SectionLabel (text, right slot, uppercase).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Discovered during WCAG audit: useKeyboardShortcuts.ts had an
isModalOpen() guard for Arrow-key move/resize shortcuts but NOT for
Escape, Enter, Cmd+]/[, or Z. When a modal dialog (role="dialog",
aria-modal="true") is open, pressing Escape cleared the canvas
selection (because the canvas handler fired before the dialog's own
Escape handler), and Enter/Cmd+[/]/Z could interfere with dialog
interactions.

Fix: add isModalOpen() guard to all four shortcut groups, extracted
as a shared helper. Also added 4 new test cases covering the
modal-dialog guard for Esc, Enter, Cmd+[/], and Z.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Restructure SearchDialog so the backdrop div is separate from the dialog
container. The outer div previously served as both backdrop and centering
wrapper, which made it impossible to add accessibility attributes
(aria-hidden="true") without hiding the dialog content from screen
readers.

New structure mirrors ConfirmDialog and KeyboardShortcutsDialog:
  - Backdrop: aria-hidden="true", cursor-pointer, click-to-dismiss
  - Dialog: role="dialog", aria-modal, aria-label, relative z-[71]

Also removes the now-unnecessary stopPropagation() on the dialog div.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
test(canvas/mobile): add RemoteBadge + WorkspacePill render coverage (14 cases)
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 9s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 12s
CI / Detect changes (pull_request) Successful in 24s
Harness Replays / detect-changes (pull_request) Successful in 20s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 20s
E2E API Smoke Test / detect-changes (pull_request) Successful in 30s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 32s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 32s
qa-review / approved (pull_request) Failing after 17s
gate-check-v3 / gate-check (pull_request) Successful in 24s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 28s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
CI / Platform (Go) (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 5s
security-review / approved (pull_request) Failing after 15s
Harness Replays / Harness Replays (pull_request) Successful in 5s
sop-checklist-gate / gate (pull_request) Successful in 14s
sop-tier-check / tier-check (pull_request) Successful in 13s
CI / Python Lint & Test (pull_request) Successful in 7s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 9s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m17s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Failing after 1m21s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m30s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 9m0s
CI / Canvas (Next.js) (pull_request) Successful in 13m3s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 4s
6307102cf7
Cover RemoteBadge and WorkspacePill — the last two rendering components in
components.tsx that were missing direct tests.

- RemoteBadge: ★ REMOTE badge rendering, span element, border-radius 4px,
  palette color/background application, dark/light difference
- WorkspacePill: brand text, count display, LIVE indicator, string count,
  border-radius pill shape, dark/light background variants

Total mobile test count now: 104 passing (was 90).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
test(lib): add hydrate.test.ts coverage (7 cases) — exponential backoff retry
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 17s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 17s
Harness Replays / detect-changes (pull_request) Successful in 27s
CI / Detect changes (pull_request) Successful in 49s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 46s
E2E API Smoke Test / detect-changes (pull_request) Successful in 47s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 44s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 21s
gate-check-v3 / gate-check (pull_request) Successful in 25s
qa-review / approved (pull_request) Failing after 16s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
security-review / approved (pull_request) Failing after 16s
Harness Replays / Harness Replays (pull_request) Successful in 7s
sop-tier-check / tier-check (pull_request) Successful in 16s
sop-checklist-gate / gate (pull_request) Successful in 17s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 46s
CI / Platform (Go) (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 6s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Failing after 1m16s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 6s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m26s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m29s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 13m31s
CI / Canvas (Next.js) (pull_request) Successful in 15m20s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 3s
audit-force-merge / audit (pull_request) Has been skipped
a8e9222b6b
Add test coverage for canvas store hydration with exponential backoff:
- Successful first-attempt hydration (stores + viewport)
- Non-fatal viewport fetch failure (succeeds without viewport)
- MAX_RETRIES=3 exhausts all attempts on persistent failure
- onRetrying callback fires before each retry
- Second-attempt success short-circuits remaining retries
- Exponential backoff: 1000 → 2000 → 4000ms between retries

Uses vi.hoisted() to share mock refs across vi.mock factories
(vitest hoists factories before module-level code).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
app-fe reviewed 2026-05-12 08:18:33 +00:00
app-fe left a comment
Member

PR #701 Review — hydrate.test.ts + canvas mega-superset ⚠️

Genuinely new: hydrate.test.ts

7 cases for hydrateCanvas with exponential backoff retry. Covers:

  • Success on first attempt
  • Non-fatal viewport failure (survives, doesn't break hydration)
  • MAX_RETRIES exhaustion
  • onRetrying callback (fires before each retry)
  • Succeeds on retry (recovers after transient failure)
  • Exponential backoff timing (1s → 2s → 4s)
  • MAX_RETRIES constant = 3

Test quality: APPROVED. All 7 cases pass.

npx vitest run src/lib/__tests__/hydrate.test.ts
→ 7/7 passed ✅

Mega-superset: everything else overlaps with PR #675

This PR introduces ~4383 insertions of canvas work that are already in PR #675 (feat/mobile-tabbar-a11y):

File In PR #675?
mobile/components.tsx (TabBar, FilterChips, AgentCard WCAG) YES
mobile/__tests__/AgentCard.test.tsx YES
mobile/__tests__/FilterChips.test.tsx YES
mobile/__tests__/TabBar.test.tsx YES
mobile/__tests__/primitives.test.tsx YES (PR #682)
settings/UnsavedChangesGuard.tsx YES
settings/__tests__/UnsavedChangesGuard.test.tsx YES
tabs/config/form-inputs.tsx YES
tabs/config/__tests__/form-inputs.test.tsx YES
8× Attachment tests YES
5× settings tests YES
useKeyboardShortcuts.ts + tests YES (different branch)
SearchDialog.tsx (WCAG 4.1.2 split) YES (different branch)

The .gitea/ lint files (681 + 505 + 141 lines) being deleted is also redundant — those were merged to main in PR #673.

Recommended path

Close PR #701. Cherry-pick only canvas/src/lib/__tests__/hydrate.test.ts onto a fresh branch based on latest main (after #675 merges). That gives a clean 1-file PR with the only genuinely new contribution.

— app-fe

## PR #701 Review — hydrate.test.ts + canvas mega-superset ⚠️ ### Genuinely new: hydrate.test.ts ✅ 7 cases for `hydrateCanvas` with exponential backoff retry. Covers: - Success on first attempt - Non-fatal viewport failure (survives, doesn't break hydration) - MAX_RETRIES exhaustion - `onRetrying` callback (fires before each retry) - Succeeds on retry (recovers after transient failure) - Exponential backoff timing (1s → 2s → 4s) - MAX_RETRIES constant = 3 **Test quality: APPROVED.** All 7 cases pass. ``` npx vitest run src/lib/__tests__/hydrate.test.ts → 7/7 passed ✅ ``` ### Mega-superset: everything else overlaps with PR #675 ❌ This PR introduces ~4383 insertions of canvas work that are **already in PR #675** (`feat/mobile-tabbar-a11y`): | File | In PR #675? | |------|-------------| | `mobile/components.tsx` (TabBar, FilterChips, AgentCard WCAG) | ✅ YES | | `mobile/__tests__/AgentCard.test.tsx` | ✅ YES | | `mobile/__tests__/FilterChips.test.tsx` | ✅ YES | | `mobile/__tests__/TabBar.test.tsx` | ✅ YES | | `mobile/__tests__/primitives.test.tsx` | ✅ YES (PR #682) | | `settings/UnsavedChangesGuard.tsx` | ✅ YES | | `settings/__tests__/UnsavedChangesGuard.test.tsx` | ✅ YES | | `tabs/config/form-inputs.tsx` | ✅ YES | | `tabs/config/__tests__/form-inputs.test.tsx` | ✅ YES | | 8× Attachment tests | ✅ YES | | 5× settings tests | ✅ YES | | `useKeyboardShortcuts.ts` + tests | ✅ YES (different branch) | | `SearchDialog.tsx` (WCAG 4.1.2 split) | ✅ YES (different branch) | The `.gitea/` lint files (681 + 505 + 141 lines) being deleted is also redundant — those were merged to main in PR #673. ### Recommended path **Close PR #701.** Cherry-pick only `canvas/src/lib/__tests__/hydrate.test.ts` onto a fresh branch based on latest main (after #675 merges). That gives a clean 1-file PR with the only genuinely new contribution. — app-fe
triage-operator added the tier:low label 2026-05-12 08:20:35 +00:00
core-uiux closed this pull request 2026-05-12 08:26:51 +00:00
Some optional checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 17s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 17s
Harness Replays / detect-changes (pull_request) Successful in 27s
CI / Detect changes (pull_request) Successful in 49s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 46s
E2E API Smoke Test / detect-changes (pull_request) Successful in 47s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 44s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 21s
gate-check-v3 / gate-check (pull_request) Successful in 25s
qa-review / approved (pull_request) Failing after 16s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
security-review / approved (pull_request) Failing after 16s
Harness Replays / Harness Replays (pull_request) Successful in 7s
sop-tier-check / tier-check (pull_request) Successful in 16s
sop-checklist-gate / gate (pull_request) Successful in 17s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 46s
CI / Platform (Go) (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 6s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Failing after 1m16s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
Required
Details
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 6s
Required
Details
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m26s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m29s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 13m31s
CI / Canvas (Next.js) (pull_request) Successful in 15m20s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 3s
Required
Details
audit-force-merge / audit (pull_request) Has been skipped

Pull request closed

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

No dependencies set.

Reference: molecule-ai/molecule-core#701