The suite was green while mainnet had not produced a block in 59 hours.
Nothing in it asked whether the chain was alive. `00-health` has nine tests
that all reduce to `eth_blockNumber > 0` — "node reports healthy", "C-Chain is
bootstrapped", "all primary chains bootstrapped" — and a frozen chain answers
every one of them. Readiness agreed: 15 pods at 5/5 Ready, every public API
200, on all three networks, one of which was stopped.
mainnet 96369 tip 1,098,191 age 58.9h testnet 96368 tip 1,354 age 3.6m
devnet 96367 tip 528 age 3.3m
e2e/00-liveness.spec.ts gates on the TIP. A fixed "block every N seconds" is
wrong for demand-driven chains, so the gate CALIBRATES ITSELF: the tip may be
no older than 4x the chain's own median gap over its last 8 blocks, floored at
600s. Measured cadences are 7s on mainnet and ~300s on testnet/devnet (the
heartbeat CronJob) — no constant spans both. Median, not mean: one 27,375s gap
IS the freeze, and a mean would let a stall widen its own budget.
Four orthogonal tests: fresh, monotonic (the tip never goes backwards and a
served block keeps its hash — the reorg shape devnet had at height 512),
consistent (net_version vs NETWORKS.yaml), and honest — a full validator set
must imply a fresh tip, which asserts the readiness lie head-on. Mainnet is RED
on three of them. That is the correct result; do not widen the budget.
Coverage was a function of which spec somebody happened to write: Safe's 31
routes, MPC's 15 and the whole admin surface had ZERO tests between them while
api.lux.financial, whose API is dead, carried 293. So routes become DATA —
e2e/routes.ts, 156 declared pages across 11 surfaces, iterated by 60-surfaces
(anonymous) and 61-console (signed in). A new page is one line there.
HTTP status is not evidence: SEVEN of the nine origins answer 200 for a path
that does not exist (measured with a /zzz-bogus control each; only Safe and the
explorer 404 honestly, and those two get a live control test every run). Every
route is therefore opened in a browser and must have PAINTED — real text into a
non-empty root, AND not the app's error boundary. Two assertions, because a
crash screen sails through a length check: exchange /pool is 200 with 553
characters that all say "Something went wrong". One reload before failing,
which is exactly the line between a page with no retry and a broken page.
First run found, each with a screenshot:
exchange /positions /pool /pool/v2 /bridge /teleport crash screen at 200
exchange /vote white screen, 0 chars
explorer /tokens /stats /advanced-filter crash screen at 200
console /dashboard/{safes,mpc,bridge,dex,deploy} no network, chainId,
address or height anywhere on screen — five of the nine surfaces the owner
named, and nothing on them tells an operator which chain they are on
Also fixed, all of them tests that could not fail:
- Peer connectivity was `catch { console.log('firewalled — PASS'); return }`.
/v1/info is not uniformly firewalled, it is inconsistently deployed (POST:
testnet 200, mainnet 404, devnet 404). Now asserts mesh depth wherever it is
served and falls back to the P-Chain validator set, which a single-node
network fails.
- 02-exchange and 20-exchange-deep skipped 58 cases behind lux.exchange, whose
TLS handshake fails from this network in curl, python and chromium alike.
exchange.lux.network is the same deployment at 200 on all 22 routes.
helpers.exchangeBase() picks the door that answers; the vanity domain being
down is now one deliberate assertion instead of a silence over 22 pages.
- 13-iam asserted a hand-built /.well-known/jwks that returns 200 text/html —
the SPA catch-all — so JSON.parse threw. Follows the discovery document now,
which declares /v1/iam/.well-known/jwks, and checks the content-type.
- 50-login sliced userinfo to 300 chars and then parsed it, so a real sign-in
failed on "Unterminated string in JSON" AFTER proving IAM accepted the token.
Parsed in-page now, three claims out, nothing else in the log.
Login proven end to end: [email protected] -> lux.id -> /dashboard, iss=lux.id,
aud=lux-cloud-org-lux, userinfo 200, sign-out present. 3/3.
Read-only throughout. Every screen in 61-console can move value — treasury,
safes, mpc, bridge. It navigates and screenshots. No form submitted, nothing
prepared, nothing signed, nothing broadcast, nothing re-keyed.
Co-authored-by: Hanzo Dev <[email protected]>
123 lines
6.3 KiB
TypeScript
123 lines
6.3 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { APIRequestContext } from '@playwright/test'
|
|
import { getUrls, exchangeBase, httpStatus, snap } from './helpers'
|
|
|
|
const urls = getUrls()
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The exchange has TWO doors and this spec used to know only one.
|
|
// ---------------------------------------------------------------------------
|
|
// `skipIfUnreachable` was a rubber stamp on the wrong host: `lux.exchange`
|
|
// fails the TLS handshake from this network (curl, python and chromium all
|
|
// agree — net::ERR_SSL_PROTOCOL_ERROR), so every page test here skipped and all
|
|
// 36 of 20-exchange-deep's skipped with them. A 49-route product tested by
|
|
// nothing, reported as green.
|
|
//
|
|
// `exchange.lux.network` is the SAME deployment, addressed by its cluster
|
|
// hostname, and serves 200 on all 22 routes probed. helpers.exchangeBase()
|
|
// picks whichever door answers, once per worker, and says which. The vanity
|
|
// host's reachability is now its own assertion below — a real finding, stated
|
|
// once, instead of a skip that silences twenty-two pages.
|
|
async function base(request: APIRequestContext): Promise<string> {
|
|
return await exchangeBase(request)
|
|
}
|
|
|
|
// ── Exchange Reachability ───────────────────────────────────────────
|
|
test.describe('Exchange — Reachability', () => {
|
|
test('the exchange answers on at least one of its hostnames', async ({ request }) => {
|
|
const vanity = await httpStatus(request, urls.exchange, 15_000)
|
|
const origin = await httpStatus(request, urls.exchangeOrigin, 15_000)
|
|
console.log(` ${new URL(urls.exchange).host}: ${vanity}`)
|
|
console.log(` ${new URL(urls.exchangeOrigin).host}: ${origin}`)
|
|
expect(Math.max(vanity, origin),
|
|
`neither ${urls.exchange} nor ${urls.exchangeOrigin} responded — the exchange is down`)
|
|
.toBeGreaterThan(0)
|
|
})
|
|
|
|
test('lux.exchange — the vanity domain investors type — resolves and serves', async ({ request }) => {
|
|
// Held separately and deliberately. MEASURED 2026-07-26: this is RED from
|
|
// this network with `tlsv1 alert protocol version`, while the cluster host
|
|
// behind it is fine — so the app is up and its front door is not. That is
|
|
// exactly one finding and it is stated exactly once, here, instead of
|
|
// silently skipping the twenty-two route tests that follow.
|
|
const status = await httpStatus(request, urls.exchange, 15_000)
|
|
console.log(` ${urls.exchange} → ${status}`)
|
|
expect(status,
|
|
`${urls.exchange} does not complete a TLS handshake from here, so an investor typing the `
|
|
+ `vanity domain gets nothing — while ${urls.exchangeOrigin} serves the same app at 200`)
|
|
.toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
// ── Exchange Frontend ───────────────────────────────────────────────
|
|
test.describe('Exchange — Frontend Accessibility', () => {
|
|
test('lux.exchange loads', async ({ request, page }) => {
|
|
const res = await page.goto(await base(request), { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
expect(res?.status()).toBe(200)
|
|
await snap(page, 'exchange-home')
|
|
})
|
|
|
|
test('exchange has visible header/nav', async ({ request, page }) => {
|
|
await page.goto(await base(request), { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(3000)
|
|
const header = page.locator('header, nav, [class*="header"], [class*="nav"]').first()
|
|
await expect(header).toBeVisible({ timeout: 15_000 })
|
|
await snap(page, 'exchange-header')
|
|
})
|
|
})
|
|
|
|
// ── Exchange — Swap UI ──────────────────────────────────────────────
|
|
test.describe('Exchange — Swap Interface', () => {
|
|
test('swap page loads', async ({ request, page }) => {
|
|
await page.goto(`${await base(request)}/swap`, { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(3000)
|
|
await snap(page, 'exchange-swap')
|
|
const content = await page.textContent('body')
|
|
expect(content?.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('swap has token selectors', async ({ request, page }) => {
|
|
await page.goto(`${await base(request)}/swap`, { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(5000)
|
|
const swapUI = page.locator(
|
|
'button:has-text("Select"), button:has-text("LUX"), [class*="token"], [class*="swap"], [class*="Swap"]'
|
|
).first()
|
|
const visible = await swapUI.isVisible({ timeout: 10_000 }).catch(() => false)
|
|
if (visible) {
|
|
await snap(page, 'exchange-swap-tokens')
|
|
}
|
|
expect(await page.title()).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
// ── Exchange — Pool UI ──────────────────────────────────────────────
|
|
test.describe('Exchange — Liquidity Pools', () => {
|
|
test('pool page loads', async ({ request, page }) => {
|
|
await page.goto(`${await base(request)}/pool`, { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(3000)
|
|
await snap(page, 'exchange-pool')
|
|
expect(await page.title()).toBeTruthy()
|
|
})
|
|
|
|
test('add liquidity page accessible', async ({ request, page }) => {
|
|
await page.goto(`${await base(request)}/add`, { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(3000)
|
|
await snap(page, 'exchange-add-liquidity')
|
|
expect(await page.title()).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
// ── Exchange — Network Detection ────────────────────────────────────
|
|
test.describe('Exchange — Network Configuration', () => {
|
|
test('exchange connects to correct chain', async ({ request, page }) => {
|
|
await page.goto(await base(request), { timeout: 30_000, waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(5000)
|
|
const content = await page.textContent('body')
|
|
const hasNetwork = content?.toLowerCase().includes('lux') ||
|
|
content?.toLowerCase().includes('96369') ||
|
|
content?.toLowerCase().includes('swap')
|
|
expect(hasNetwork).toBeTruthy()
|
|
await snap(page, 'exchange-network')
|
|
})
|
|
})
|