Skip to content

✨ Wave 2: deep branch coverage for complex hooks — coverage sprint#4124

Merged
clubanderson merged 1 commit intomainfrom
cov/wave2-hooks-deep
Apr 1, 2026
Merged

✨ Wave 2: deep branch coverage for complex hooks — coverage sprint#4124
clubanderson merged 1 commit intomainfrom
cov/wave2-hooks-deep

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Deep branch-coverage tests for the 6 largest, most complex hooks in the console codebase. These hooks already had basic tests (1-3 tests each); this PR adds 152 new tests to exercise conditional branches, error paths, and edge cases that were previously untested.

Hooks covered (by line count):

Hook Lines Tests Added Key Branches Covered
useCachedData.ts 3,156 37 (new file) All 20+ useCached* hooks, fetchAPI error paths (no token, non-JSON, HTTP error), sort/limit, progressive fetcher presence, key formatting
useSearchIndex.ts 553 +18 localStorage card scanning (malformed JSON, non-array, missing card_type), stat scanning fallbacks, catalog de-duplication, node role defaults, null hook data
useRewards.tsx 288 +25 RewardsProvider lifecycle, one-time vs repeatable awards, unknown actions, achievements (coin-based, action-based, count-based), event cap, GitHub dedup, merged total never-negative
useExecSession.ts 421 +25 WebSocket connect/auth/init flow, stdout/stderr/exit/error messages, no-token error, constructor throw, reconnection scheduling, sendInput/resize, disconnect, unmount cleanup
useSidebarConfig.ts 511 +22 addItem/addItems (all targets), removeItem, updateItem, reorderItems, collapse/mobile toggles, restoreDashboard, resetToDefault, generateFromBehavior, persistence
useProviderHealth.ts 306 +7 AI/cloud provider split, isDemoFallback suppression during loading, failure state passthrough

Approach

  • Read each hook and identified all if/else branches, switch cases, ternaries, early returns
  • Tests exercise BOTH sides of every branch where possible
  • Mocked return values to force error paths
  • Tested with empty data, null data, malformed localStorage data
  • WebSocket mock with full event lifecycle for useExecSession

Test plan

  • All 187 tests pass: npm test -- --run src/hooks/__tests__/{useCachedData,useSearchIndex,useRewards,useExecSession,useSidebarConfig,useProviderHealth}*
  • Build passes: npm run build
  • No changes to production code -- test files only

Add comprehensive branch-coverage tests for the 6 largest/most complex
hooks in the console codebase:

- useCachedData (3,156 lines): New test file covering all 20+ useCached*
  hooks, fetcher callbacks (no-token, non-JSON, HTTP error, sort/limit),
  progressive fetcher presence, key formatting, and category defaults.

- useSearchIndex (553 lines): +18 tests covering localStorage card
  scanning (malformed JSON, non-array, missing card_type), stat scanning
  (fallback to defaults, invisible stats), catalog card de-duplication,
  node role defaults, custom dashboard href, null/undefined hook data.

- useRewards (288 lines): +25 tests covering RewardsProvider lifecycle,
  awardCoins (repeatable vs one-time, unknown action, no user), coin-
  based and action-based achievements, event cap at 100, GitHub rewards
  dedup logic, merged total never-negative, metadata passthrough.

- useExecSession: +25 tests covering WebSocket connect/open/auth flow,
  exec_init message fields, stdout/stderr/exit/error message handling,
  no-token error, constructor throw, onerror before connection, onclose
  with code, reconnection scheduling, sendInput/resize JSON messages,
  disconnect preventing reconnect, unmount cleanup, multi-connect.

- useSidebarConfig: +22 tests covering addItem/addItems (primary,
  secondary, sections), removeItem, updateItem, reorderItems (all
  targets), toggle/set collapsed, mobile sidebar open/close/toggle,
  restoreDashboard (add + no-op), resetToDefault, generateFromBehavior,
  localStorage persistence, exported constants.

- useProviderHealth: +7 tests covering AI/cloud provider separation,
  isDemoFallback suppression while loading, failed state passthrough.

Total: 187 tests across 6 files, all passing.
Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Copilot AI review requested due to automatic review settings April 1, 2026 12:50
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 1, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Apr 1, 2026
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit 959c5cc
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cd14b39c54530008c9c3e4
😎 Deploy Preview https://deploy-preview-4124.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@clubanderson clubanderson merged commit 69759b9 into main Apr 1, 2026
22 of 24 checks passed
@kubestellar-prow kubestellar-prow bot deleted the cov/wave2-hooks-deep branch April 1, 2026 12:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds deep branch-coverage tests for several complex console hooks, focusing on conditional branches, edge cases, and error paths—without changing production code.

Changes:

  • Expanded test suites for useSidebarConfig, useSearchIndex, useRewards, useExecSession, and useProviderHealth
  • Added a new comprehensive test file for useCachedData covering many exported hooks and internal fetcher branches
  • Added additional mocks/stubs to exercise error paths and persistence behavior

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
web/src/hooks/tests/useSidebarConfig.test.ts Adds extensive action + persistence tests for sidebar config hook
web/src/hooks/tests/useSearchIndex.test.ts Adds localStorage parsing edge-case and de-duplication tests
web/src/hooks/tests/useRewards.test.tsx Adds provider-backed tests for awarding, achievements, persistence, and GitHub merge logic
web/src/hooks/tests/useProviderHealth.test.ts Adds split/shape/state passthrough tests with cache + provider mocks
web/src/hooks/tests/useExecSession.test.ts Adds WebSocket lifecycle, error handling, reconnect, and command/resize tests
web/src/hooks/tests/useCachedData.test.ts New deep branch-coverage suite for useCachedData hooks + fetcher behavior

Comment on lines +28 to +39
import { useSidebarConfig, DISCOVERABLE_DASHBOARDS, PROTECTED_SIDEBAR_IDS } from '../useSidebarConfig'
import type { SidebarItem } from '../useSidebarConfig'

const STORAGE_KEY = 'kubestellar-sidebar-config-v11'
const OLD_STORAGE_KEY = 'kubestellar-sidebar-config-v10'

describe('useSidebarConfig', () => {
beforeEach(() => {
localStorage.clear()
vi.clearAllMocks()
// Reset module-level shared state by clearing storage
// (the module uses sharedConfig singleton that persists across tests)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates the hook uses a module-level singleton that persists across tests, but the setup only clears localStorage and mocks. If useSidebarConfig reads/writes a shared in-memory singleton, tests can become order-dependent (e.g., addItem mutations leaking into later tests). Consider resetting module state between tests (e.g., vi.resetModules() + dynamic import per test) or calling a dedicated reset exported by the module (if available) so each test starts from a known default config.

Suggested change
import { useSidebarConfig, DISCOVERABLE_DASHBOARDS, PROTECTED_SIDEBAR_IDS } from '../useSidebarConfig'
import type { SidebarItem } from '../useSidebarConfig'
const STORAGE_KEY = 'kubestellar-sidebar-config-v11'
const OLD_STORAGE_KEY = 'kubestellar-sidebar-config-v10'
describe('useSidebarConfig', () => {
beforeEach(() => {
localStorage.clear()
vi.clearAllMocks()
// Reset module-level shared state by clearing storage
// (the module uses sharedConfig singleton that persists across tests)
import type { SidebarItem } from '../useSidebarConfig'
const STORAGE_KEY = 'kubestellar-sidebar-config-v11'
const OLD_STORAGE_KEY = 'kubestellar-sidebar-config-v10'
let useSidebarConfig: typeof import('../useSidebarConfig')['useSidebarConfig']
let DISCOVERABLE_DASHBOARDS: typeof import('../useSidebarConfig')['DISCOVERABLE_DASHBOARDS']
let PROTECTED_SIDEBAR_IDS: typeof import('../useSidebarConfig')['PROTECTED_SIDEBAR_IDS']
describe('useSidebarConfig', () => {
beforeEach(async () => {
localStorage.clear()
vi.clearAllMocks()
await vi.resetModules()
const mod = await import('../useSidebarConfig')
useSidebarConfig = mod.useSidebarConfig
DISCOVERABLE_DASHBOARDS = mod.DISCOVERABLE_DASHBOARDS
PROTECTED_SIDEBAR_IDS = mod.PROTECTED_SIDEBAR_IDS

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +226
const compute = DISCOVERABLE_DASHBOARDS.find(d => d.id === 'compute')!
const before = result.current.config.primaryNav.some(i => i.id === 'compute')
if (!before) {
act(() => { result.current.restoreDashboard(compute) })
expect(result.current.config.primaryNav.some(i => i.id === 'compute')).toBe(true)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can become a no-op when compute is already present (it performs no assertions in that case), which weakens its value and can mask regressions. Make the precondition deterministic (e.g., pick a dashboard guaranteed not to exist initially, or remove it first) and always assert the expected outcome.

Suggested change
const compute = DISCOVERABLE_DASHBOARDS.find(d => d.id === 'compute')!
const before = result.current.config.primaryNav.some(i => i.id === 'compute')
if (!before) {
act(() => { result.current.restoreDashboard(compute) })
expect(result.current.config.primaryNav.some(i => i.id === 'compute')).toBe(true)
}
const dashboard = DISCOVERABLE_DASHBOARDS.find(
d => !result.current.config.primaryNav.some(i => i.id === d.id),
)
expect(dashboard).toBeDefined()
const before = result.current.config.primaryNav.some(i => i.id === dashboard!.id)
expect(before).toBe(false)
act(() => { result.current.restoreDashboard(dashboard!) })
expect(result.current.config.primaryNav.some(i => i.id === dashboard!.id)).toBe(true)

Copilot uses AI. Check for mistakes.
Comment on lines +765 to +767
if (catalogCards.length > 0) {
expect(catalogCards[0].href).toContain('addCard=true')
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test conditionally asserts and will pass without verifying anything if catalogCards is empty. To keep the test meaningful, assert that at least one catalog card exists for the query (or set up mocks/fixtures so the catalog list is deterministic), then check the href shape.

Suggested change
if (catalogCards.length > 0) {
expect(catalogCards[0].href).toContain('addCard=true')
}
expect(catalogCards.length).toBeGreaterThan(0)
catalogCards.forEach(card => {
expect(card.href).toContain('addCard=true')
})

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +16
const mockUseCacheResult = {
data: [],
isLoading: false,
isRefreshing: false,
isDemoFallback: false,
isFailed: false,
consecutiveFailures: 0,
refetch: vi.fn(),
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In TypeScript, data: [] is inferred as never[] in many configurations. Later assignments like mockUseCacheResult.data = [...] as ProviderHealthInfo[] can cause a type error at compile time. Give mockUseCacheResult (or at least data) an explicit type such as ProviderHealthInfo[] or unknown[] to avoid never[] inference.

Copilot uses AI. Check for mistakes.
ok: true,
text: vi.fn().mockResolvedValue(JSON.stringify({ pods: [{ name: 'p1' }] })),
}
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockFetchResponse))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global stubs are being torn down inline within individual tests. If an assertion fails before vi.unstubAllGlobals() runs, the stubbed fetch can leak into subsequent tests and create cascading failures. Prefer centralizing global cleanup in afterEach (e.g., always calling vi.unstubAllGlobals() there) so teardown happens even when a test fails early.

Copilot uses AI. Check for mistakes.
const pods = await fetcher()
expect(Array.isArray(pods)).toBe(true)

vi.unstubAllGlobals()
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global stubs are being torn down inline within individual tests. If an assertion fails before vi.unstubAllGlobals() runs, the stubbed fetch can leak into subsequent tests and create cascading failures. Prefer centralizing global cleanup in afterEach (e.g., always calling vi.unstubAllGlobals() there) so teardown happens even when a test fails early.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

@clubanderson
Copy link
Copy Markdown
Collaborator Author

🔄 Auto-Applying Copilot Code Review

Copilot code review found 3 code suggestion(s) and 3 general comment(s).

@copilot Please apply all of the following code review suggestions:

  • web/src/hooks/__tests__/useSidebarConfig.test.ts (line 39): `import type { SidebarItem } from '../useSidebarConfig'

const STORAGE_KEY = 'kub...`

  • web/src/hooks/__tests__/useSidebarConfig.test.ts (line 226): const dashboard = DISCOVERABLE_DASHBOARDS.find( d => !result.current.confi...
  • web/src/hooks/__tests__/useSearchIndex.test.ts (line 767): expect(catalogCards.length).toBeGreaterThan(0) catalogCards.forEach(card => ...

Also address these general comments:

  • web/src/hooks/__tests__/useProviderHealth.test.ts (line 16): In TypeScript, data: [] is inferred as never[] in many configurations. Later assignments like `mockUseCacheResult.da
  • web/src/hooks/__tests__/useCachedData.test.ts (line 606): Global stubs are being torn down inline within individual tests. If an assertion fails before vi.unstubAllGlobals() ru
  • web/src/hooks/__tests__/useCachedData.test.ts (line 616): Global stubs are being torn down inline within individual tests. If an assertion fails before vi.unstubAllGlobals() ru

Push all fixes in a single commit. Run cd web && npm run build && npm run lint before committing.


Auto-generated by copilot-review-apply workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants