fix(tests): retarget 20 tests left behind by the wallet bundle-split refactor - #2451
Conversation
…refactor PR #2450 made the app suite actually run in CI, surfacing 65 failures. This fixes 20 of them. They share one cause: the bundle-split refactor moved logic out of the modules these tests still pointed at, so the tests asserted against code that no longer had the behaviour — and the code that DID have it was left with no coverage at all. - useWallet.test.ts: useWalletCompat is now a pure WalletApiContext read; the Privy derivation moved to PrivyWalletApiBridge. Tests now cover the hook for what it is (context read + read-only default + no wallet-SDK import, which is the invariant the refactor exists to protect). - PrivyProviderClient.test.tsx: picks up the Privy -> WalletApi derivation the hook test dropped — connection state, active-wallet selection, signMessage binding, disconnect, and referential stability. This path builds and signs every transaction in the app and had zero tests. Also completes the @privy-io/react-auth/solana mock, which the bridge's new hooks broke. - ConnectButton.test.tsx: retargeted at ConnectButtonPrivyInner. The shell is a dynamic(ssr:false) import that never resolves in jsdom, so all three cases were asserting against the "Loading wallet" placeholder. - useStuckSlabs.test.ts: rewritten against the current lib/inFlightMarket contract (per-slab keys + wallet gate). The old suite drove a `percolator-pending-slab-keypair` key the hook no longer reads. Adds coverage for the wallet gate and the admin-address filter — the guard that stops another wallet's in-flight market surfacing in the recovery banner. - SlabProvider-allowlist.test.tsx: the cases shared one slab address across a module-level cache (lib/slabCache), so each asserted against the previous one's residue. Fresh address per test; mockReset so the WS test's implementation stops leaking forward. One product change, in the phishing guard's reject path: it spread the previous state, so config/engine parsed from the seed cache stayed visible on an account it was actively refusing to trust. Nulling programId alone was not enough — downstream UI reads config/engine too. Now resets to defaults. Local: 95 -> 75 failures, 2702 -> 2714 tests. tsc --noEmit clean. No file touched here is still failing; the remaining 23 files are separate clusters. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe changes expand wallet bridge and compatibility tests, update in-flight slab recovery coverage to the current storage and wallet contracts, reset stale slab provider state for unknown owners, and isolate allowlist test state. ChangesWallet bridge validation
Slab recovery and ownership state
Estimated code review effort: 4 (Complex) | ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/__tests__/hooks/useStuckSlabs.test.ts (1)
254-264: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest does not validate the behavior its name claims.
The title/comment say the hook "keeps the last-good list" on a transient RPC error, but no successful load is seeded first, so
stuckSlabsis vacuously[]on initial mount regardless of the catch behavior. To actually exercise the "don't blank the banner" path, resolvegetAccountInfosuccessfully first, wait for a populated list, then make a subsequentrefresh()reject and assert the list is preserved.🤖 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 `@app/__tests__/hooks/useStuckSlabs.test.ts` around lines 254 - 264, Update the “keeps the last-good list when the RPC lookup fails” test for useStuckSlabs so the initial getAccountInfo call resolves with data and the test waits for a populated stuckSlabs list. Then make the subsequent refresh() call reject with the RPC error and assert that the previously loaded stuckSlabs and stuckSlab values remain unchanged.
🤖 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.
Nitpick comments:
In `@app/__tests__/hooks/useStuckSlabs.test.ts`:
- Around line 254-264: Update the “keeps the last-good list when the RPC lookup
fails” test for useStuckSlabs so the initial getAccountInfo call resolves with
data and the test waits for a populated stuckSlabs list. Then make the
subsequent refresh() call reject with the RPC error and assert that the
previously loaded stuckSlabs and stuckSlab values remain unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7a4130b7-735e-4697-9eaa-6399e27ee295
📒 Files selected for processing (6)
app/__tests__/components/ConnectButton.test.tsxapp/__tests__/components/PrivyProviderClient.test.tsxapp/__tests__/hooks/useStuckSlabs.test.tsapp/__tests__/hooks/useWallet.test.tsapp/__tests__/providers/SlabProvider-allowlist.test.tsxapp/components/providers/SlabProvider.tsx
…path CodeRabbit review on #2451 caught this and it is correct: the test asserted `stuckSlabs` was `[]` right after a failed INITIAL mount, so it was vacuously empty whether the catch preserved state or blanked it. It proved nothing about the behaviour its name claims. Now seeds a successful load, waits for a populated list, then breaks the RPC and forces refresh() before asserting the list survives. Mutation-verified: adding `setStuckSlabs([])` to the hook's catch block fails the new test. The old version passed under that same mutation. This is the fourth instance of the vacuous-pass pattern in this batch of work, and the first one that was mine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch on the The test asserted Fixed in Mutation-verified rather than just "it's green now" — adding Worth noting for anyone reading: this is the fourth vacuous-pass found in this batch of work (see #2452's stake-pool fixtures and #2453's matcher-state negative case) — and the first one that was mine rather than pre-existing. The pattern is consistent enough to be worth watching for repo-wide. |
Fifth slice of the 65 CI failures #2450 exposed. Fixes 8, adds 10. Test files only, no product change. Each suite asserted a contract the route had deliberately moved away from: - funding-global (4): the route is no longer a pass-through proxy. It is a three-tier chain (Railway proxy -> local indexer Postgres -> graceful empty) that intentionally does NOT surface upstream errors, so the funding UI degrades to "no data" rather than an error state when the backend is down — the live situation whenever Railway is dead (#2443). Tests now assert the degradation, and cover the indexer-db fallback tier, which had none: it serves rows, applies the blocklist, and falls through to empty when the query itself fails. - middleware-rate-limit (3): these set BLOCKED_MARKET_ADDRESSES and expected a 404. middleware.ts imports `@/lib/blocklist-edge`, which is edge-pure and intentionally omits env-var overrides — importing the env-reading `@/lib/blocklist` co-bundled Node-only code into the Edge chunk and Vercel's deploy validator rejected it. The tests now pin that boundary explicitly instead of failing on it: env-injected slabs are blocked at the Node API layer only, and blocking at the Edge requires editing blocklist-edge.ts. - markets-supabase-outage-fallback (1): the devnet static directory was migrated to the single v17 wrapper program, so filtering by an old per-slab-tier program id matched 0 rows, not 3. Now asserts the filter's behaviour rather than a hardcoded count, plus the negative case — without it a no-op filter returning everything would still pass. Also adds __tests__/unit/blocklist-edge-sync.test.ts. blocklist-edge.ts carries a "KEEP IN SYNC with HARDCODED_BLOCKED_SLABS" comment and nothing enforced it. The two lists agree today (35 entries each), but drift would leave a slab blocked at one layer and reachable through the other — and entries there include a wrong oracle_authority (price-manipulation risk, GH#837). Includes a size assertion so emptying one set cannot make the diffs pass vacuously. Mutation-verified: removing one address from the edge list fails it. 95 -> 87 failures, 28 -> 25 files. tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed SlabProvider-allowlist test # Conflicts: # app/__tests__/providers/SlabProvider-allowlist.test.tsx
…pper program The 2026-07 fee-split merge re-migrated the static DEVNET_MARKET_DIRECTORY_FALLBACK program from 69VUZ7a2… to the fee-split wrapper DhSkE7uTb8…, so the positive 'filters by program_id' test queried a program no longer in the directory and got 0 rows (expected >0). Updates both program-id constants to the current value; the negative-case test (unknown program → 0 rows) still guards against a no-op filter.
…ator On a staked market the wizard's full create flow (StakeInitPool + BindInsuranceAuthority) rotates marketauth, insurance_authority AND insurance_operator to program PDAs. The panel gated on insurance_operator, so on every real (staked) market it rendered for NOBODY -- no wallet holds a PDA key. Verified on the live staked market 7FBXdrm1…: insurance_operator = PDA 6a3tiSd2…, but asset_admin = the creator's wallet 7JVQvrAf…. asset_admin bootstraps to the creator and the stake flow leaves it alone, so it is the field that reliably tracks the creator through staking. Matches the on-chain re-gate (wrapper hash 4f5df6be…, tag 90 WithdrawCreatorFee now checks asset 0's asset_admin). - lib/v17-creator-fee.ts: read the gate from parseAssetOracleProfileV17(...). assetAdmin (SDK-owned, profile-rel 368), not .insuranceOperator. - useCreatorClaim.ts: isOperator -> isClaimAuthority; error copy "Only this market's admin (the creator) can claim". - CreatorClaimPanel.tsx: badge "You are the operator" -> "You are the creator". - Tests use three distinct keys (asset_admin=creator, insurance_operator=PDA, marketauth=third) mirroring the live market, and assert the panel shows for asset_admin and NOT insurance_operator -- reverting the read to insuranceOperator turns 20 tests red. tsc clean, next build green, 53/53 in the creator-claim suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDXvFwXmYuz5cbJYyczo7u
…d (code-split heavy cards)
Two issues reported on /analytics/[slab]: data lags behind reality, and initial
load is slow.
DATA LAG — SlabProvider used adaptive polling ("30s when wsActive, 3s when not"),
but wsActive latched to true on the FIRST onAccountChange message and was never
reset. A silently-dropped WebSocket (frequent on Vercel/public RPC — there's even
a reconnect-storm note in useWalletCompat) left the page refreshing only every
30s with nothing detecting it. Replaced with a steady 5s backup poll that the WS
rides on top of: WS still delivers ~instant updates when alive, and a dead socket
now costs at most 5s of staleness instead of 30s. bytesEqual in parseSlab dedups
the no-op re-parses, so the steadier poll adds zero extra re-renders. (Also
benefits /trade/[slab], which shares this provider.)
INITIAL LOAD — the analytics page mounted all 11 cards at once, 4 of which scan
every portfolio in the 26KB slab (AccountsCard, AdlLeaderboard,
LiquidationAnalytics, SystemCapitalCard). Code-split those 4 via next/dynamic
(ssr:false) with skeleton fallbacks so they load in their own chunks after the
light top-of-page cards paint, trimming the initial JS bundle and first-render
work for the dashboard route.
tsc clean, next build green (analytics route compiles with the split chunks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDXvFwXmYuz5cbJYyczo7u
…slabs 394f856 ("blocklist the entire devnet-2.0 lineup") added entries to lib/blocklist.ts but not to lib/blocklist-edge.ts. Those are two separate hardcoded sets, deliberately duplicated so the Edge middleware gets an isolated bundle (the file's own header says KEEP IN SYNC), and middleware.ts imports the EDGE one: app/middleware.ts:26 import { BLOCKED_SLAB_ADDRESSES } from "@/lib/blocklist-edge"; So the canonical set had 41 entries while the Edge set had 35, and the 6 newly blocklisted devnet-2.0 slabs were still reachable through the middleware: 7FBXdrm1vQ4ktQJjMwurq4cAHkVB1gKoZ7Hx3CAQv6P4 8SHhSKuY9cun15Y2Q9p9SNEV86zzSWbeP4e59xLAv99h BLAHwD5wZ3Wo6naHD4GTT6zpYFcyLWAviEWR4zT7C36p BPgSUbDsxZ9bkauWgd6eQ8oLHVx6pSsvfAjPGsS2Sso8 CseeeuKKbgNU38VRukG38mTdcPJ4KWci5GmFikEtp1X5 gHey79gB1xGQyXne8yEHoKmGi6jrEVigLwxSXQrYkD3 Adds those 6, leaving both sets at 41 with zero divergence in either direction. Entries re-sorted case-insensitively so future diffs stay readable. Found by the blocklist-edge-sync test in #2456, which exists precisely to catch this class of drift; verified against that test here (it fails if any one of the 6 is removed again, passes with all present).
…ocklist instead 394f856 blocklisted the entire devnet-2.0 lineup. DEVNET_MARKET_DIRECTORY_FALLBACK is now a SINGLE entry and that entry is itself blocklisted, so the outage fallback legitimately returns zero rows on devnet — which broke this test's `markets.length > 0` premise. Dropping the emptiness assertion rather than propping it up: it was never the point, and it would re-break the moment the directory is repopulated. The filter's real contract — everything returned matches the requested program — is kept. To make sure removing it did not leave a vacuous test, added a guard that pins the behaviour that now actually matters: requesting the blocklisted entry's OWN program must still return nothing, proving the blocklist filter runs ahead of the program filter rather than the emptiness being incidental. Mutation-verified: disabling the fallback's BLOCKED_SLAB_ADDRESSES filter fails the new guard; restoring it passes. Note (not fixed here, it is a product call): during a Supabase outage the devnet fallback now serves zero markets. Flagged separately.
The leverage slider computed its max with BigInt division: `Number(10000n / initialMarginBps)`, which discards the remainder. For the standard 1500-bps (15%) market the real engine cap is 10000/1500 = 6.6667x, but the truncation showed 6x — silently denying traders the last ~0.67x the engine actually permits. Meanwhile the position panel's "Risk Lev." computed the honest value and displayed 6.7x, so a max order read as "6" on the slider and "6.7" once open. Same market, two numbers. Fix (client-side only; the on-chain cap is unchanged): - maxLeverageFromOnChain now uses float division, floored to 2 decimals so a max order lands at 1501.5 bps >= the 1500 required (clears the margin check with a hair of buffer rather than sitting exactly on the edge). Displays 6.7x. - Made the size math fractional-safe. The old `BigInt(leverage)` and `BigInt(Math.round(maxLeverage))` throw / overshoot on a non-integer like 6.66 — buyingPower and notionalNative now scale by 100 before the BigInt. - All leverage displays route through formatLeverageValue: integers stay clean (1x/3x/5x/6x), the max shows 6.7x. The market list / wizard still advertise the round floored 6x; only the trade ticket exposes the true achievable ceiling, as intended. Verified: tsc clean; leverage-display + wizard suites (28 tests) green; the exact arithmetic checked directly (1500bps -> "6.7x" -> 1501.5bps -> accepted; BigInt paths no longer throw on 6.66). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
Root cause, verified against the deployed engine (2026-07-27):
`withdraw_not_atomic` (percolator/src/v16.rs:14412) returns V16Error::Stale
(Custom 19) UNCONDITIONALLY whenever the account's active_bitmap is non-empty —
i.e. whenever ANY position is open — regardless of oracle/crank freshness. It is
a by-design refusal to release collateral backing a position, NOT a stale-data
condition. Proven: an account whose four cert epochs all match the header still
reverts Custom(19) while a leg is open, and a maintainer crank never clears it.
The app was doing two wrong things:
- Surfacing "the market's data is too stale; needs a maintainer crank" for
Custom(19) on withdraw — actively misleading; the real remedy is "close first".
- Believing "free margin" (capital − position margin) was withdrawable while a
position stayed open, and prepending a PermissionlessCrank to "un-stale" the
withdraw. Neither works: the engine locks the whole account until flat.
Fix (frontend only; the engine behavior is intended and unchanged):
- Block the doomed withdrawal up front when the account has an open position,
with the true reason ("close your position first"), instead of letting it
revert on-chain.
- Rewrite the Custom(19) withdraw message and the generic errorMessages.ts copy
for 19 and 21 to state the real causes (position-open / transient-lag /
recovery) instead of always asserting "the market needs a re-seed".
- Remove the useless v17 crank-prepend (and its now-unused import + the
always-false CU flag): it could never help a position withdraw.
NOT a keeper change: the earlier "keeper should crank user accounts" plan was
based on a wrong stale-cert diagnosis and is dropped — cranking cannot empty the
bitmap. Closing (a self-certifying trade) then withdrawing is the supported flow
and works today.
Verified: tsc clean; errorMessages + withdraw suites (76 tests) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…uardrails The launch wizard hardcoded two families of parameters that are written ONCE at market creation and can never be changed (the matcher has no update instruction; max_price_move lives in the engine config). Both were verified harmful on devnet. 1. maxPriceMoveBpsPerSlot=1 / maxAccrualDtSlots=500 froze NEW positions for as long as the settlement price lagged the oracle — ~17 min after a 26% move. Proven causally: a converged market opens fine; push -30% and the same trader is refused Custom(21) until the gap closes, with the gap shrinking monotonically under cranking (2461 -> 2205 bps). 2. The LP had NO guardrails: impactKBps 0, maxFillAbs and maxInventoryAbs at i128::MAX, skewSpreadMultBps 0 — a free, unlimited, fixed-price counterparty. That is how Jimothy's LP reached $0 capital / -$2,479 pnl. lib/market-params.ts now derives all of it from the only things a creator picks (leverage + fee split). Trading fee stays fixed; price-move, accrual window and LP caps are never shown or typed in. The solvency envelope makes this a TRADE-OFF, not free headroom. An earlier reading of `<= 10_000` was wrong: that bound applies only to a special-case early return. The binding limit is scaled by maintenance margin — bisected on-chain at `price_move x window <= 500` for 1500-bps margin, with every combination at 750+ REJECTED by InitMarket. The rate limit is a solvency guarantee (it stops a price move outrunning liquidation), so higher leverage buys less of it. MAX_PRICE_MOVE_BY_MARGIN encodes the bisected maxima. Also removes the basis for the 6.67x leverage floor: the July bisection concluded "10x fails", but re-testing shows 10x fails ONLY when paired with the old 1x500 budget. With a compatible budget (4x100) 10x is accepted. Leverage was never the problem, so creators can now be offered 2x-10x. Verified on devnet: - every leverage 2x..10x produces an InitMarket the program ACCEPTS (bisected). - a market launched with the derived config reads back on-chain as max_price_move=6, max_accrual_dt=100, max_fill_abs=600000000, max_inventory_abs=2400000000 (was i128::MAX). - the fill cap BITES: a 300M order lands, a 900M order is refused, so the LP can no longer be loaded without limit in a single trade. - tsc clean; wizard + leverage-display suites (28 tests) green. Not yet wired: the leverage picker UI (creator still supplies initialMarginBps), and skewSpreadMultBps is derived but unverified on-chain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
Two more launch-path defects found by executing the flow on devnet. 1. ZOMBIE MARKETS. The /api/markets POST — the call that makes a market VISIBLE in the app — fired immediately after M1, in parallel with the funding steps. A launch that died at M3a still published a listed, unfunded, untradeable market. That is exactly what happened to ANSEM: slab + LP portfolio created, the deposit never arrived, steps 4-5 never ran, and a broken market appeared in the UI with the creator's funds still in their wallet. Because "create the market" is always step 1 and always succeeds, EVERY failed launch left one. Registration is now deferred behind registerMarketInDb() and called only after M3a (DepositCollateral + backing seed) lands, so a market becomes visible only once it actually holds collateral. A launch that dies earlier leaves an on-chain slab nobody sees instead of a broken listing. keeper-register is deliberately NOT deferred: it must still run before M4 because StakeInitPool rotates marketauth and keeper-register's H1 check requires marketauth to still equal the deployer. The sequential path already registered after Step 3 and needed no change. 2. SHORTS BACKED BY ONE CENT, FOREVER. Both backing domains were seeded with a flat 0.01 test-USDC. That is fine for LONG (DepositToLpVault can top it up) but not for SHORT: CreateLpVault overwrites the asset's backing_bucket_authority to the vault registry PDA, and that field is shared by BOTH domains while a vault serves only domain 0. Afterwards the creator is Unauthorized (Custom 8) on TopUpBackingBucket, WithdrawBackingBucket and SyncBackingDomainLedger for both domains — verified on devnet, and irreversible (UpdateAssetAuthority needs a signature the PDA cannot give, CloseLpVault does not restore the field, UpdateAssetLifecycle returns AssetSlotAlreadyConfigured on an active asset). Whatever SHORT gets at creation is all it will ever have, so the seed is now backingSeedPerDomain() = 10% of the LP seed per domain, floored at the old 0.01 so a tiny LP seed still defuses the freshness deadlock. A $1000 LP seed now backs each side with $100 instead of one cent. The tx4 funding requirement was updated to cover the larger seed. Verified: tsc clean; wizard suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
MIN_SAFE_INITIAL_MARGIN_BPS = 1500n floored the on-chain initial_margin_bps at
all three InitMarket sites, so every market was created at 6.67x regardless of
what the creator chose. It also disagreed with the rest of the launch: `derived`
sized max_price_move_bps_per_slot for the leverage the creator PICKED while
InitMarket wrote the floored margin, so the two describe different markets.
The floor came from a bisection that concluded "10x fails on-chain". That was a
misdiagnosis. 10x fails only when paired with the OLD hardcoded budget
(1 bps/slot x 500 slots = 500), which exceeds what 500-bps maintenance margin
can absorb. Re-simulated against the deployed program, both halves confirmed:
im=1000 pm=1 dt=500 ❌ rejected <- the "proof" that 10x fails
im=1000 pm=4 dt=100 ✅ accepted <- same leverage, compatible budget
Leverage was never the problem; the budget paired with it was.
Margin and maintenance now come from deriveMarketParams — the same call that
produces the price-move budget — so the two are coherent by construction and
cannot drift apart again.
Validation bounds were a second silent-clamp of the same kind: they allowed
100..5000 bps (100x..2x) while deriveMarketParams clamps to 10x..2x, so a
creator asking for 20x or 100x passed validation and was handed a 10x market
with no warning. Bounds are now derived from MIN/MAX_LEVERAGE_X so they cannot
drift from the clamp, and an out-of-range request is rejected with the real
number instead of being quietly changed.
Verified against the deployed program: every leverage the wizard offers
(2, 2.5, 3, 4, 5, 6, 6.67, 7, 8, 9, 10x) simulates ACCEPTED, so the wizard
cannot produce a config that fails InitMarket.
Adds market-params.test.ts (22 tests) pinning the bisected envelope table, the
leverage round-trip, the LP guardrails, and the backing seed — these values are
write-once per market and unfixable after creation. App suite back to its
pre-existing baseline (93 unrelated failures on this branch, unchanged).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
The caption said yield distribution "isn't active on the deployed program yet, so APY is currently 0%". That blames the program, and it is false. Verified on a fresh devnet market: a 500-notional round trip at 30 bps accrued exactly 1_440_000 atoms of LP fee on the slab (the LP's 48% of $3.00), and a single LpVaultCrankFees moved all of it into the vault — lpFeeWithdrawnAtoms 0 -> 1440000, vault feeDistribution 0 -> 1440000. The program distributes fees correctly. Nothing was CALLING the crank, which the keeper now does on its own interval (percolator-oracle-keeper @82dde32). New copy says what is actually true: LPs earn a 48% share of every trading fee, distributed automatically, and 0% means the market is quiet rather than the feature being absent. Left the equivalent line on the stake page alone. Stake may well be the same shape of problem — StakeAccrueFees exists but has no SDK account list and no callers — but I have not proven it end-to-end, and rewriting that copy on an assumption is the same mistake in the opposite direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
…ong number Three related problems in the create wizard's parameters step. 1. Leverage was not selectable. Creators set "Initial Margin" on a raw bps slider ranging 100..5000 (100x..2x), while deriveMarketParams clamps to 10x..2x — so a request outside that range was silently turned into a different market. It also asked for the inverse of the number creators actually think in. Replaced with a LeveragePicker (2/3/4/5/8/10x) that previews margin, maintenance, price-move budget and recovery time, all read straight from deriveMarketParams so the preview cannot drift from what gets written on-chain. 2. The advertised leverage was wrong for every leverage that does not divide evenly. Margin rounds UP (ceil(10000/lev)), so 3x stores 3334 bps — and floor(10000/3334) is 2. A 3x market called itself 2x on the review screen, the success screen, AND in the markets DB's max_leverage column. Added leverageFromMarginBps() (the exact inverse, rounding) and used it at all three sites. 3. Trading fee was creator-settable via a 1..1000 bps slider. Per product decision it is one rate for every market: a creator undercutting on fees does not make their market better, it starves the LP and insurance shares that keep it solvent. The fee is now pinned to FIXED_TRADING_FEE_BPS at the source, so a quick-launch preset cannot override it either, and shown as a read-only row pointing at the fee split — the knob that IS theirs. Net effect: the creator chooses leverage and their fee split, and everything else is derived. That matters here more than usual because these parameters are write-once — there is no instruction to change a market's leverage, matcher caps, or price-move budget after creation. App suite unchanged at its pre-existing baseline (93 unrelated failures on this branch); market-params tests 25/25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
Swept the app for the class of bug behind the 6.67x leverage cap and the missing LP caps: a value typed once into an InitMarket/InitMatcherCtx call, invisible afterwards and permanent for that market. Findings: - `impactKBps: 0` is NOT a missing guardrail. market-params.ts listed it alongside the caps that really were disabled, which would have sent the next reader chasing it. Impact is vAMM-only: compute_passive_execution in percolator-match/src/vamm.rs prices off base_spread + trading_fee + skew and never reads impact_k_bps, and the impact term zeroes out anyway when liquidity_notional_e6 is 0. These markets are kind: 0 (Passive), so 0 is correct. Comment corrected to say so explicitly. - Both I128_MAX constants were dead. The recovery/STEP2 path already derives its matcher caps like the merged path does, so nothing referenced them. Removed — a leftover i128::MAX sitting next to a matcher call is an invitation to reintroduce the bug. - Documented every remaining pinned parameter with why it is safe to pin. Most notable: maxAbsFundingE9PerSlot "0" is deliberate, not an oversight — trades settle at the pushed AuthMark, which IS the reference price, so there is no perp-vs-spot basis for funding to correct. Also closed a real gap in the earlier leverage verification. That matrix ran with minFundingLifetimeSlots=100, but the wizard sends 500 — so it had not actually tested the shipped config. Re-ran with the wizard's exact args: all of 2/2.5/3/4/5/6/6.67/7/8/9/10x still ACCEPTED, and the control still reproduces the original misdiagnosis (im=1000 pm=1 dt=500 rejected). Stake page: copy CHECKED and left accurate, with the on-chain reason recorded so it does not get "corrected" into a lie the way the Earn caption was. The wizard creates stake pools via StakeInitPool, which sets pool_mode = 0; percolator-stake's process_accrue_fees rejects anything but pool_mode == 1 with InvalidPoolMode. Confirmed on a fresh market: pool_mode reads 0 at offset 280. So stake earns nothing by design — it is insurance backing, not a fee-earning pool — and wiring a stake AccrueFees crank would have been work on a call that can never succeed. Reworded to say what staking IS, and to point at Earn for fee yield. App suite unchanged at its pre-existing baseline (93 unrelated failures). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
`computeMaxLeverage` used Math.floor(10000 / initialMarginBps). The launch path derives margin with ceil(10000 / leverage), so 3x stores 3334 bps and floor(10000 / 3334) is 2 — the markets list advertised a 3x market as 2x, and so did every consumer of this route. Same defect fixed on the client in 9b3afe7; this server route was missed because it kept its own copy of the calculation. Now imports the shared leverageFromMarginBps so the two cannot drift again. Also blocklists the 14 harness markets created during this session's verification sweep. They are real, funded, wrapper-owned markets, so /api/markets discovers them on-chain and they were showing on the LIVE playground as untitled "UNKNOWN" rows. They are test fixtures, not products. Nothing depends on them — the keeper prices only its own registry, which they were never added to. Repairs __tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts. It had been failing since the zombie fix and, more importantly, had stopped guarding: the slice used `keeperRegisterPromise` as an end marker, the zombie fix moved markets-registration AFTER it, indexOf returned -1, and every assertion inside became unreachable. Rewritten against the current structure and extended to cover what actually broke in production: - registration happens only AFTER M3a lands (the ANSEM zombie — a market that registered and then never got funded) - registration is idempotent (marketsRegistered guard) - keeper-register still precedes M4, because StakeInitPool rotates marketauth and keeper-register's H1 check needs it to equal the deployer - a bounds test on the source-scan itself, so a moved marker fails loudly instead of silently disabling the guard Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
92 failures -> 31, from two causes that had nothing to do with product code. Web Storage was missing entirely. `window.localStorage` under this jsdom setup is a PLAIN EMPTY OBJECT — no getItem, no setItem, no clear (its prototype is Object.prototype). Every test touching storage died on its first call, which is why the whole useChartDrawings + useChartDrawingTool suites failed as a block (29 tests) on `window.localStorage.clear is not a function`. That is one missing global, not 29 broken behaviours. The polyfill puts methods on a shared PROTOTYPE rather than on the instance, because the suites spy via `vi.spyOn(window.localStorage.__proto__, "setItem")` to simulate quota errors and Safari Private Mode — and because that is how real jsdom behaves (all Storage shares Storage.prototype). An instance-level implementation leaves the prototype empty and spyOn throws. Deleted 10 test files for routes that this branch deliberately removed in 7f5b512 ("cut marketing cruft, lock devnet, decouple Supabase"): the waitlist signup flow, /api/bugs, /api/ideas, /api/applications. They read route files off disk and failed ENOENT. Verified every one still exists on origin/main alongside the code it tests before removing it here — this deletes a duplicate, not the only copy. Leaving them red was not free: 30+ permanently-failing tests are the noise that let a genuinely broken guard (useCreateMarket-fresh-batched-registration, whose assertions had become unreachable) sit unnoticed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
…tracts
Both suites were asserting against superseded storage layouts, so they tested
the empty path while looking like product failures.
useStuckSlabs (8 tests): the mock returned only useConnectionCompat, but the
hook also calls useWalletCompat — and a vi.mock factory REPLACES the module, so
every render threw "No useWalletCompat export is defined". With that fixed the
fixtures still matched nothing: they wrote the legacy single-keypair blob
(`percolator-pending-slab-keypair` = a bare secretKey array) while the hook now
reads wallet-scoped entries via loadAllInFlightMarkets() filtered on
`adminAddress === connectedWallet`. Fixtures now write the real
`percolator:in-flight-market:<slab>` shape against a connected wallet, and the
removal assertions target the per-slab key instead of the retired global one.
useStakePool (2 tests): fixture used the v1 StakePool layout — 352 bytes with
cooldown_slots at offset 162. The struct is now 384 ("prior 352 +
pending_admin[32] = 384" in percolator-stake state.rs) and cooldown_slots sits
at 184. The HOOK was correct the whole time; confirmed against a live devnet
pool, where offset 184 reads exactly the 5 slots the market was created with
and 162 reads garbage. Fixture migrated to the v2 offsets.
Neither was a product bug — but both looked like one, which is the cost of
leaving a suite red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
31 -> ~12. All stale-contract, none product bugs. setup.ts: guard the Storage polyfill behind `typeof window !== "undefined"`. Some suites opt into `@vitest-environment node`, where touching `window` at setup time throws ReferenceError and fails the file before its first test — a regression I introduced in 7817632. Those three suites now pass (49 tests). useStakeDepositByPool / useStakeWithdrawByPool (18 tests): two stale things. The SDK mock omitted V17_MARKET_GROUP_OFF, so the import threw before any test ran; and the pool fixture stamped the RETIRED stake program as the account owner. The hooks derive against getConfig().vaultProgramId — GCHhcgw…, which is what is actually deployed (confirmed on devnet) — so the owner guard correctly rejected the fixture. Fixtures now use the live program and the v2 384-byte pool size. PrivyProviderClient: mock omitted useSignTransaction / useSignAndSendTransaction / useSignMessage. A vi.mock factory replaces the whole module, so every hook the provider imports has to be listed. Removed 2 more orphaned suites — Guide.test.tsx and launch-invalid-json-guard.test.ts import `app/guide/page` and `app/api/launch/route`, both cut from this branch and both still present on origin/main with their tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
The Edge blocklist was missing 14 entries. I added the harness/QA markets to lib/blocklist.ts in 67ac873 but not to lib/blocklist-edge.ts, which middleware imports as a deliberately isolated, zero-import copy — so middleware would not have blocked them. Caught by blocklist-edge-sync.test.ts, which came in with the merged api-route-tests branch and exists for exactly this drift. Synced. Four more fixtures that encoded superseded contracts: v17-matcher-state: fixtures wrote the delegate at CTX_VAMM_OFFSET, but the delegate lives at +16 — the 16-byte VAMM magic occupies +0..+16. That is the precise bug lib/v17-matcher-state.ts was fixed to stop doing (verified against live devnet contexts), so the fixture encoded the old defect and the "initialized" case could never pass. One sibling test had been passing for the wrong reason: a 32-byte write at +0 leaves non-zero bytes at +16, which reads as a mismatched delegate rather than an empty one. priceStore-poll: the wsManager mock only had onMessage. priceStore now subscribes per channel, so the store threw "onMessageForChannel is not a function" on first subscribe. gh1654: asserted MarketInfoBar imports MarketLogo directly. The symbol+logo became a dropdown market switcher, so the assertion now follows the indirection (bar -> MarketSwitcher -> MarketLogo) and tests the GH#1654 requirement rather than the old implementation. Suite: 92 failing -> 14, and 2813 passing (was 2701). tsc clean, production build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162bycxSd52kpWco9npQAXr
…allet/ConnectButton retargets The upstream test-maintenance batch (7817632, 03d3f79, 5d6f715) fixed PrivyProviderClient.test.tsx and useStuckSlabs.test.ts independently, so this branch's older versions of those two files conflicted. Both now pass on playground on their own, so upstream's versions win — this branch adds nothing there any more. What this branch still uniquely fixes is the remainder of the wallet-bundle retarget that upstream did NOT cover: useWallet.test.ts (6 failures) and ConnectButton.test.tsx (1), which still fail on 8bab3ce.
ci: actually run the app test suite, and stop the Merge Gate certifying skips (#2447) Merged AFTER the test-fix PRs (#2451/#2458/#2459) so the newly-enforcing Merge Gate switches on against a green suite rather than the 4 files that were failing before them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to #2450 / #2447. #2450 made the app suite actually run in CI, surfacing 65 real failures. This fixes 20 of them.
The common cause
All five files point at modules whose logic moved during the wallet bundle-split refactor. The tests kept asserting against the old location, so they failed — and the code that actually took over the behaviour was left with no coverage at all. Making them green was the smaller half; restoring the lost coverage was the point.
useWallet.test.tsuseWalletCompat()to derive wallet stateWalletApiContextread — tests the context read, the read-only default, and that it imports no wallet SDK (the invariant the refactor exists for)PrivyProviderClient.test.tsxWalletApiderivation: connection state, active-wallet selection,signMessagebinding, disconnect, referential stabilityConnectButton.test.tsxdynamic(ssr:false)shell, which never resolves in jsdomConnectButtonPrivyInner; all 3 cases were asserting against the "Loading wallet" placeholderuseStuckSlabs.test.tspercolator-pending-slab-keypairkey the hook no longer readslib/inFlightMarket(per-slab keys + wallet gate); adds wallet-gate and admin-address-filter coverageSlabProvider-allowlist.test.tsxmockResetso the WS test's impl stops leaking forwardThe
PrivyWalletApiBridgecoverage matters most here: that path builds and signs every transaction in the app and had zero tests after the logic moved out of the hook.One product change
SlabProvider's phishing-guard reject path spread the previous state:The seed cache (
getSeedSlab) can already have parsed and publishedconfig/engine/accountsfor that address before the owner is known, so parsed state stayed visible on an account the provider was actively refusing to trust. NullingprogramIdalone isn't enough — downstream UI readsconfig/enginetoo. Now resets to defaults, keeping onlyslabAddress.Evidence
npx tsc --noEmitcleanplayground, stake-pool, chart-drawings, priceStore)How to test
cc @dcccrypto — the
SlabProviderchange is in a security guard, so it wants a security look before merge.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests