Priority: High · Area: Test coverage / core hook · Est. effort: 7–11 h
📌 Problem
src/hooks/ contains nine hooks. Eight have a matching .test.ts:
useDebouncedValue ✓ useFocusShortcut ✓ useInterval ✓ useQueryState ✓
useSortableData ✓ useTheme ✓ useToast ✓ useWallet ✓
useAsync ✗ ← no test file
useAsync is the one that matters most. Its documented contract (src/hooks/useAsync.ts:12-27) is "Runs an abortable async loader on mount and exposes a reload trigger", returning four members — state, reload, refresh and mutate — over an AsyncState<T> union of loading/ready/error. Every component that loads data goes through it.
Its behaviour is exactly the kind that breaks silently:
- Abort on unmount — does an in-flight request get cancelled, or does it resolve into an unmounted component?
- Race handling — if
reload is called while a request is in flight, does the stale response win?
refresh vs reload — the doc says reload "also surfaces a loading state", implying refresh does not. Untested, that distinction drifts.
mutate — optimistic updates followed by a real response.
- Error transitions — does a failed reload after a successful load clear the data or keep it?
There is corroborating evidence this is already a problem: MetricsBar.test.tsx mocks useAsync with only { state, refresh }, omitting reload and mutate — a mismatch that produces type errors and is tracked separately. Components are mocking a hook nobody has pinned down.
🎯 Design decision required
State and defend:
- Behaviour inventory. Read
useAsync.ts and enumerate every behaviour worth asserting before writing tests. Post it in the PR — the inventory is the deliverable that makes the tests reviewable.
- Race and abort strategy. These need deterministic control over promise resolution order. Describe your approach — deferred promises, fake timers, or a controllable mock loader — and why it is not timing-dependent.
- Behaviour that turns out to be wrong. If a test reveals the hook mishandles a race or leaks an update after unmount, that is a bug report, not a reason to adjust the test. Say so explicitly if it happens.
🧩 Requirements and context
- Cover every member of the returned contract:
state, reload, refresh, mutate.
- Cover all three
AsyncState variants and the transitions between them.
- Abort-on-unmount must be tested. A React state update after unmount is a real defect class.
- Tests must be deterministic — no reliance on real elapsed time. Prove it with three consecutive runs.
- Do not change
useAsync's behaviour unless a test exposes a genuine bug; if it does, report it prominently and fix minimally.
- The repo configures 95% coverage thresholds (
vitest.config.ts), though CI does not currently enforce them (separate issue). Aim to meet them for this file.
🛠️ Suggested execution
- Post the behaviour inventory.
- Write tests for the happy path and all state transitions.
- Add abort-on-unmount and concurrent-reload race tests.
- Add
mutate and refresh-vs-reload tests.
- Report any defect found.
✅ Acceptance criteria
🚫 Out of scope
- Rewriting
useAsync, unless a test exposes a real bug.
MetricsBar.test.tsx's mock mismatch — separate issue, though your tests define the contract it should mock.
- Enforcing coverage in CI — separate issue.
🧪 Verification
npm ci
npm test src/hooks/useAsync.test.ts
npm test && npm test && npm test
npx tsc --noEmit
📤 What your PR must include
- The behaviour inventory.
- Your race/abort control strategy and why it is deterministic.
- Any defect the tests exposed.
- Determinism evidence.
Closes #<n>.
🔒 Security notes
useAsync mediates every network response the application renders. An unhandled race means a stale response can overwrite fresh data — so a user can be shown balances or settlement statuses that have since changed, with no indication the view is out of date. A missing abort means responses continue to be processed after the component that requested them is gone. Both are correctness failures with user-visible consequences on a financial interface.
📋 Guidelines
- Minimum 95% test coverage on changed lines
- Clear documentation
- Timeframe: 96 hours from assignment
- One logical change per commit; no merge commits
💬 Join our community
Working on this, or want to sanity-check your approach before you start? Come and ask — the maintainers are there and happy to help.
Telegram: https://t.me/Grainlify
Priority: High · Area: Test coverage / core hook · Est. effort: 7–11 h
📌 Problem
src/hooks/contains nine hooks. Eight have a matching.test.ts:useAsyncis the one that matters most. Its documented contract (src/hooks/useAsync.ts:12-27) is "Runs an abortable async loader on mount and exposes areloadtrigger", returning four members —state,reload,refreshandmutate— over anAsyncState<T>union of loading/ready/error. Every component that loads data goes through it.Its behaviour is exactly the kind that breaks silently:
reloadis called while a request is in flight, does the stale response win?refreshvsreload— the doc saysreload"also surfaces a loading state", implyingrefreshdoes not. Untested, that distinction drifts.mutate— optimistic updates followed by a real response.There is corroborating evidence this is already a problem:
MetricsBar.test.tsxmocksuseAsyncwith only{ state, refresh }, omittingreloadandmutate— a mismatch that produces type errors and is tracked separately. Components are mocking a hook nobody has pinned down.🎯 Design decision required
State and defend:
useAsync.tsand enumerate every behaviour worth asserting before writing tests. Post it in the PR — the inventory is the deliverable that makes the tests reviewable.🧩 Requirements and context
state,reload,refresh,mutate.AsyncStatevariants and the transitions between them.useAsync's behaviour unless a test exposes a genuine bug; if it does, report it prominently and fix minimally.vitest.config.ts), though CI does not currently enforce them (separate issue). Aim to meet them for this file.🛠️ Suggested execution
mutateandrefresh-vs-reloadtests.✅ Acceptance criteria
useAsync.src/hooks/useAsync.test.tsexists and coversstate,reload,refreshandmutate.AsyncStatevariants and their transitions are covered.reloadissued while a request is in flight, asserting the stale response does not win.refreshvsreloadloading-state distinction is asserted.🚫 Out of scope
useAsync, unless a test exposes a real bug.MetricsBar.test.tsx's mock mismatch — separate issue, though your tests define the contract it should mock.🧪 Verification
📤 What your PR must include
Closes #<n>.🔒 Security notes
useAsyncmediates every network response the application renders. An unhandled race means a stale response can overwrite fresh data — so a user can be shown balances or settlement statuses that have since changed, with no indication the view is out of date. A missing abort means responses continue to be processed after the component that requested them is gone. Both are correctness failures with user-visible consequences on a financial interface.📋 Guidelines
💬 Join our community
Working on this, or want to sanity-check your approach before you start? Come and ask — the maintainers are there and happy to help.
Telegram: https://t.me/Grainlify