-
Notifications
You must be signed in to change notification settings - Fork 35
fix(launch): a11y, bounded caches, volume finiteness, timer cleanup, network-scoped indexer reads #2526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(launch): a11y, bounded caches, volume finiteness, timer cleanup, network-scoped indexer reads #2526
Changes from all commits
580194e
1c2a222
50321be
fbe753b
9f10112
9d06c38
7268adb
5d6cbf5
847790d
1f1f072
159083e
9a63d67
c524010
06d7084
4fb3210
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // #2509 — omitting `slabAddress` deliberately means "apply to ALL admin-oracle | ||
| // markets". The old handler caught a JSON parse failure and left `body = {}`, | ||
| // so a truncated or corrupt payload silently widened a single-market change | ||
| // into a fleet-wide one. Empty and malformed are different requests. | ||
| describe("#2509 set-price-cap distinguishes empty from malformed body", () => { | ||
| const src = fs.readFileSync( | ||
| path.join(__dirname, "../../app/api/oracle/set-price-cap/route.ts"), | ||
| "utf8", | ||
| ); | ||
|
|
||
| it("reads the raw body and only parses when non-empty", () => { | ||
| expect(src).toMatch(/await req\.text\(\)/); | ||
| expect(src).toMatch(/rawBody\.trim\(\)\s*!==\s*""/); | ||
| }); | ||
|
|
||
| it("returns 400 on malformed JSON rather than falling through", () => { | ||
| // the parse failure path must produce a 400, not an empty-object default | ||
| const parseCatch = src.slice(src.indexOf("JSON.parse(rawBody)")); | ||
| expect(parseCatch).toMatch(/status:\s*400/); | ||
| expect(parseCatch).toMatch(/Malformed JSON body/); | ||
| }); | ||
|
|
||
| it("no longer treats a parse failure as the all-markets default", () => { | ||
| // the old shape: `try { body = await req.json() } catch { /* empty ok */ }` | ||
| expect(src).not.toMatch(/body\s*=\s*await req\.json\(\)/); | ||
| }); | ||
|
|
||
| it("rejects a non-object JSON body (array / null) too", () => { | ||
| expect(src).toMatch(/Array\.isArray\(body\)/); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // #2510 — `totalTrades: rows.length` reported the CAPPED row count with nothing | ||
| // marking it as a floor, so a wallet above the cap silently under-reported every | ||
| // aggregate (volume, fees, unique markets) with no signal to the caller. | ||
| describe("#2510 trader stats surface truncation", () => { | ||
| const src = fs.readFileSync( | ||
| path.join(__dirname, "../../app/api/trader/[wallet]/stats/route.ts"), | ||
| "utf8", | ||
| ); | ||
|
|
||
| it("names the cap once instead of repeating a literal", () => { | ||
| expect(src).toMatch(/export const TRADER_STATS_MAX_ROWS = 10_000;/); | ||
| // the raw literal must not survive in the queries, or cap and flag can drift | ||
| expect(src).not.toMatch(/\.limit\(10_000\)/); | ||
| }); | ||
|
|
||
| it("applies the named cap to BOTH query paths (primary and fallback)", () => { | ||
| const uses = src.match(/\.limit\(TRADER_STATS_MAX_ROWS\)/g) ?? []; | ||
| expect(uses.length).toBeGreaterThanOrEqual(2); | ||
| }); | ||
|
|
||
| it("exposes a truncated flag derived from the cap", () => { | ||
| expect(src).toMatch(/truncated:\s*rows\.length >= TRADER_STATS_MAX_ROWS/); | ||
| }); | ||
|
|
||
| it("declares truncated on the response type", () => { | ||
| expect(src).toMatch(/truncated:\s*boolean;/); | ||
| }); | ||
|
|
||
| it("sets truncated:false on the empty-result path", () => { | ||
| expect(src).toMatch(/totalTrades:\s*0,\s*\n\s*truncated:\s*false,/); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // #2520 — every pre-existing check on this route was SHAPE-only (valid pubkey, | ||
| // printable name, ticker charset, decimals range). A syntactically valid pubkey | ||
| // for an account that was never created still landed in `devnet_mints`. | ||
| describe("#2520 devnet-register-mint verifies the mint on-chain", () => { | ||
| const src = fs.readFileSync( | ||
| path.join(__dirname, "../../app/api/devnet-register-mint/route.ts"), | ||
| "utf8", | ||
| ); | ||
|
|
||
| it("fetches the account before upserting", () => { | ||
| expect(src).toMatch(/getAccountInfo\(new PublicKey\(mintAddress\)\)/); | ||
| }); | ||
|
|
||
| it("rejects a nonexistent account", () => { | ||
| expect(src).toMatch(/does not exist on devnet/); | ||
| }); | ||
|
|
||
| it("requires the token program as owner AND the exact SPL mint length", () => { | ||
| expect(src).toMatch(/owner\.equals\(TOKEN_PROGRAM_ID\)/); | ||
| expect(src).toMatch(/SPL_MINT_LEN\s*=\s*82/); | ||
| expect(src).toMatch(/data\.length\s*!==\s*SPL_MINT_LEN/); | ||
| }); | ||
|
|
||
| it("fails CLOSED on an RPC error rather than falling through to the upsert", () => { | ||
| // an advisory check that skips on error would leave the issue half-closed | ||
| const catchBlock = src.slice(src.indexOf("mint existence check failed")); | ||
| expect(catchBlock).toMatch(/status:\s*503/); | ||
| expect(catchBlock).toMatch(/return NextResponse\.json/); | ||
| }); | ||
|
|
||
| it("runs BEFORE the DB upsert", () => { | ||
| expect(src.indexOf("getAccountInfo")).toBeLessThan(src.indexOf('from("devnet_mints")')); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // #2243 — the footer social icons carried `title` only. `title` is not a reliable | ||
| // accessible name (screen readers vary, and it is invisible on keyboard focus), so | ||
| // each link needs an explicit aria-label and each decorative glyph aria-hidden. | ||
| describe("#2243 footer social links have accessible names", () => { | ||
| const src = fs.readFileSync( | ||
| path.join(__dirname, "../../components/layout/Footer.tsx"), | ||
| "utf8", | ||
| ); | ||
|
|
||
| const socials = ["GitHub", "X (Twitter)", "Discord", "Telegram"]; | ||
|
|
||
| it.each(socials)("labels the %s link", (name) => { | ||
| expect(src).toContain(`aria-label="Percolator on ${name}"`); | ||
| }); | ||
|
|
||
| it("hides every decorative social glyph from the a11y tree", () => { | ||
| // Count the social anchors by their aria-labels, then require at least as many | ||
| // aria-hidden svgs — so adding a link without hiding its glyph fails here. | ||
| const labelled = (src.match(/aria-label="Percolator on /g) ?? []).length; | ||
| const hidden = (src.match(/<svg aria-hidden="true"/g) ?? []).length; | ||
| expect(labelled).toBe(socials.length); | ||
| expect(hidden).toBeGreaterThanOrEqual(labelled); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { getLiquidationSeverity } from "../../hooks/usePortfolio"; | ||
|
|
||
| // #2412 — both comparisons are FALSE for NaN, so an unguarded non-finite | ||
| // distance fell through to "safe": a position at liquidation risk rendering as | ||
| // fine. That is the one direction a risk indicator must never fail in. | ||
| describe("#2412 getLiquidationSeverity never reports safe on bad data", () => { | ||
| it("does not report NaN as safe", () => { | ||
| expect(getLiquidationSeverity(NaN)).not.toBe("safe"); | ||
| }); | ||
|
|
||
| it("does not report Infinity as safe", () => { | ||
| expect(getLiquidationSeverity(Infinity)).not.toBe("safe"); | ||
| expect(getLiquidationSeverity(-Infinity)).not.toBe("safe"); | ||
| }); | ||
|
|
||
| it("fails toward danger, not merely warning", () => { | ||
| // A suppressed warning is a liquidation the user never saw coming; a | ||
| // spurious one is noise. The asymmetry justifies the loudest bucket. | ||
| expect(getLiquidationSeverity(NaN)).toBe("danger"); | ||
| }); | ||
|
|
||
| it("still classifies real values correctly — the guard did not flatten it", () => { | ||
| expect(getLiquidationSeverity(5)).toBe("danger"); | ||
| expect(getLiquidationSeverity(10)).toBe("danger"); | ||
| expect(getLiquidationSeverity(20)).toBe("warning"); | ||
| expect(getLiquidationSeverity(30)).toBe("warning"); | ||
| expect(getLiquidationSeverity(31)).toBe("safe"); | ||
| expect(getLiquidationSeverity(100)).toBe("safe"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // #2513 — every read against the indexer DB must be scoped to the deployment's | ||
| // network. The `trades` and `funding_history` tables both carry a `network` | ||
| // column, but no query used it as a predicate, so a devnet deployment could | ||
| // serve mainnet rows and vice versa. | ||
| // | ||
| // Asserted structurally rather than by running SQL: the failure mode is a NEW | ||
| // query being added without the filter, and only a source-level invariant | ||
| // catches that. A value test over the existing queries would pass forever while | ||
| // query #13 silently leaks. | ||
| describe("#2513 indexer-db reads are network-scoped", () => { | ||
| const src = fs.readFileSync( | ||
| path.join(__dirname, "../../lib/indexer-db.ts"), | ||
| "utf8", | ||
| ); | ||
| const lines = src.split("\n"); | ||
|
|
||
| it("every FROM trades / funding_history is followed by a network predicate", () => { | ||
| const offenders: string[] = []; | ||
| lines.forEach((line, i) => { | ||
| if (!/FROM\s+(trades|funding_history)\b/.test(line)) return; | ||
| // the predicate may be on this line (single-line form) or within the | ||
| // next few lines of the same statement | ||
| const window = [line, ...lines.slice(i + 1, i + 5)].join("\n"); | ||
| if (!/network\s*=\s*\$\{getServerNetwork\(\)\}/.test(window)) { | ||
|
Comment on lines
+24
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Make the network-scope scan statement-aware. The test only matches Inspect each Also applies to: 35-37 🤖 Prompt for AI Agents |
||
| offenders.push(`line ${i + 1}: ${line.trim()}`); | ||
| } | ||
| }); | ||
| expect(offenders).toEqual([]); | ||
| }); | ||
|
|
||
| it("actually found query sites — the scan cannot pass vacuously", () => { | ||
| const sites = lines.filter((l) => /FROM\s+(trades|funding_history)\b/.test(l)); | ||
| expect(sites.length).toBeGreaterThanOrEqual(12); | ||
| }); | ||
|
|
||
| it("imports the server-side resolver, not the localStorage one", () => { | ||
| expect(src).toMatch(/import \{ getServerNetwork \} from "\.\/supabase"/); | ||
| expect(src).not.toMatch(/from "\.\/config"/); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| // #2321 — hasVolumeData gated the volume pane on `(c.volume ?? 0) > 0`. | ||
| // NaN > 0 is false so NaN was already excluded, but Infinity > 0 is TRUE, so a | ||
| // single corrupt candle enabled the pane and handed Infinity to the histogram | ||
| // series, which then scales the whole pane off that value. | ||
| // | ||
| // Mirrors the predicate rather than importing the component (TradingChart pulls | ||
| // in lightweight-charts + a canvas). The assertion that matters is the shape of | ||
| // the guard: finiteness first, then positivity. | ||
| const hasVolumeData = (candles: { volume?: number }[]) => | ||
| candles.some((c) => Number.isFinite(c.volume) && (c.volume ?? 0) > 0); | ||
|
|
||
| describe("#2321 volume pane requires FINITE volume", () => { | ||
| it("rejects Infinity — the case `> 0` alone let through", () => { | ||
| expect(hasVolumeData([{ volume: Infinity }])).toBe(false); | ||
| expect(hasVolumeData([{ volume: -Infinity }])).toBe(false); | ||
| }); | ||
|
|
||
| it("rejects NaN and missing volume", () => { | ||
| expect(hasVolumeData([{ volume: NaN }])).toBe(false); | ||
| expect(hasVolumeData([{}])).toBe(false); | ||
| }); | ||
|
|
||
| it("still accepts a real positive volume", () => { | ||
| expect(hasVolumeData([{ volume: 0 }, { volume: 1234 }])).toBe(true); | ||
| }); | ||
|
|
||
| it("one corrupt candle does not enable the pane on its own", () => { | ||
| expect(hasVolumeData([{ volume: 0 }, { volume: Infinity }])).toBe(false); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| // #2523 — the canonicaliser's last-resort throw said only "Unsupported market | ||
| // registration payload value". Because it is recursive, that one sentence was | ||
| // the entire signal for a bad value buried anywhere in the payload. | ||
| describe("#2523 payload canonicaliser error is diagnostic", () => { | ||
| const src = fs.readFileSync( | ||
| path.join(__dirname, "../../lib/market-registration-auth.ts"), | ||
| "utf8", | ||
| ); | ||
|
|
||
| it("names the offending type", () => { | ||
| expect(src).toMatch(/type "\$\{typeof value\}"/); | ||
| }); | ||
|
|
||
| it("names the likely causes so the message is actionable", () => { | ||
| expect(src).toMatch(/bigint and function values are the usual causes/); | ||
| }); | ||
|
|
||
| it("does NOT interpolate the value itself — that would leak payload contents", () => { | ||
| // `${typeof value}` is fine; a bare `${value}` or JSON.stringify(value) in the | ||
| // throw would put user payload into a client-visible error and the logs. | ||
| const thrown = src.slice(src.indexOf("Unsupported market registration payload value")); | ||
| const firstThrowBlock = thrown.slice(0, 400); | ||
| expect(firstThrowBlock).not.toMatch(/\$\{value\}/); | ||
| expect(firstThrowBlock).not.toMatch(/JSON\.stringify\(value\)/); | ||
| }); | ||
|
|
||
| it("keeps the sibling non-plain-object error intact", () => { | ||
| expect(src).toMatch(/must contain plain JSON objects/); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the complete accessibility contract for each link.
This count-based source test does not check
focusable="false"or associate each hidden SVG with its own social link. An unrelated hidden SVG can satisfy the count after a social icon regresses. Assert both attributes for each rendered social link, or validate each anchor block directly.🤖 Prompt for AI Agents