Files
universe/e2e/00-health.spec.ts
zeekayandHanzo Dev d9b5f7a9a3 test(e2e): assert the chain is MOVING, and open every page of every product
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]>
2026-07-26 20:20:52 -07:00

216 lines
9.6 KiB
TypeScript

import { test, expect } from '@playwright/test'
import {
getUrls, getEnv, isLocal, getChainIds,
rpcCall, ethBlockNumber, ethChainId, ethGasPrice,
getPeers,
getCurrentValidators, getBlockchains,
httpStatus, httpGetJson, benchmarkRpc,
} from './helpers'
const urls = getUrls()
const chainIds = getChainIds()
// ── Blockchain RPC ──────────────────────────────────────────────────
test.describe('Blockchain RPC — C-Chain', () => {
test('C-Chain returns valid block number', async ({ request }) => {
const block = await ethBlockNumber(request, urls.rpc)
console.log(` C-Chain block: ${block.toLocaleString()}`)
expect(block).toBeGreaterThan(0)
})
test('C-Chain returns correct chainId', async ({ request }) => {
const cid = await ethChainId(request, urls.rpc)
console.log(` chainId: ${cid}`)
expect(cid).toBe(chainIds.lux)
})
test('C-Chain returns non-zero gas price', async ({ request }) => {
const gp = await ethGasPrice(request, urls.rpc)
console.log(` gasPrice: ${Number(gp) / 1e9} gwei`)
expect(gp).toBeGreaterThan(0n)
})
test('C-Chain eth_getBalance works', async ({ request }) => {
const result = await rpcCall(request, urls.rpc, 'eth_getBalance', [
'0x0000000000000000000000000000000000000000', 'latest',
])
expect(result).toBeDefined()
})
})
// ── Node Info (public-RPC equivalents) ──────────────────────────────
// info.* (/v1/info) is firewalled at the gateway by design — assert the
// equivalent identity/liveness facts over the public C/P/X RPC instead.
test.describe('Node Info API', () => {
test('returns correct network ID', async ({ request }) => {
const nv = await rpcCall(request, urls.rpc, 'net_version') as string
console.log(` net_version: ${nv}`)
expect(nv).toBe(String(chainIds.lux))
})
test('returns node version', async ({ request }) => {
const ver = await rpcCall(request, urls.rpc, 'web3_clientVersion') as string
console.log(` clientVersion: ${ver}`)
expect(ver).toBeTruthy()
})
test('P-Chain is bootstrapped', async ({ request }) => {
const validators = await getCurrentValidators(request, urls.rpcPlatform)
expect(validators.length).toBeGreaterThan(0)
})
test('C-Chain is bootstrapped', async ({ request }) => {
const block = await ethBlockNumber(request, urls.rpc)
expect(block).toBeGreaterThan(0)
})
test('X-Chain is bootstrapped', async ({ request }) => {
const status = await httpStatus(request, urls.rpcXChain)
console.log(` X-Chain RPC: HTTP ${status}`)
// GET is 405 (POST-only JSON-RPC); either proves the chain RPC is served.
expect([200, 405]).toContain(status)
})
})
// ── Health Endpoint ─────────────────────────────────────────────────
// /v1/health is firewalled at the gateway by design; C-Chain block
// production is the public-facing liveness signal.
test.describe('Health API', () => {
test('node reports healthy', async ({ request }) => {
const block = await ethBlockNumber(request, urls.rpc)
console.log(` C-Chain block (liveness): ${block}`)
expect(block).toBeGreaterThan(0)
})
})
// ── Peer Mesh ───────────────────────────────────────────────────────
// This test used to be a guaranteed pass: `catch { console.log('firewalled —
// PASS'); return }` means it returns green whether info.peers is firewalled,
// broken, or serving one peer out of five. It passed on testnet with peers:4
// and would have passed identically against a node with none.
//
// MEASURED 2026-07-26 — /v1/info is not uniformly firewalled at all, it is
// inconsistently DEPLOYED. POST /v1/info: testnet 200, mainnet 404, devnet 404
// (the testnet gateway repair moved its info backend off /ext/* and the other
// two never got it). So "firewalled" was never the right story.
//
// Split into the two facts, each asserted where it is true: whether the
// endpoint is served is a per-network deployment fact and is reported, not
// asserted; the mesh depth is asserted wherever the endpoint answers. A network
// that exposes info.peers and reports 2 peers now goes RED instead of green.
test.describe('Peer Connectivity', () => {
test('info.peers reports the mesh wherever it is served', async ({ request }) => {
let peers: unknown[] | null = null
try {
peers = await getPeers(request, urls.rpcInfo)
} catch (e) {
console.log(` ${urls.rpcInfo} does not serve info.peers on ${getEnv()} `
+ `(${(e as Error).message.split('\n')[0]}) — mesh depth not assertable here`)
}
if (peers === null) {
// Not a pass by default: the node must still be demonstrably in a mesh,
// and the public P-Chain says so. A single-node network fails this.
const validators = await getCurrentValidators(request, urls.rpcPlatform)
console.log(` falling back to P-Chain validator set: ${validators.length}`)
expect(validators.length,
`neither info.peers nor the P-Chain validator set shows a mesh on ${getEnv()}`)
.toBeGreaterThanOrEqual(5)
return
}
console.log(` peers: ${peers.length}`)
expect(peers.length, `info.peers is served and reports ${peers.length} peers in a 5-node mesh`)
.toBeGreaterThanOrEqual(4)
})
})
// ── Validator Network ───────────────────────────────────────────────
test.describe('Validator Network', () => {
test('>= 5 validators on P-Chain', async ({ request }) => {
const validators = await getCurrentValidators(request, urls.rpcPlatform) as { nodeID: string }[]
console.log(` validators: ${validators.length}`)
expect(validators.length).toBeGreaterThanOrEqual(5)
const uniqueIds = new Set(validators.map(v => v.nodeID))
expect(uniqueIds.size).toBeGreaterThanOrEqual(5)
})
test('all primary chains bootstrapped', async ({ request }) => {
// info.isBootstrapped is firewalled; assert each chain's public RPC serves.
const cBlock = await ethBlockNumber(request, urls.rpc)
expect(cBlock).toBeGreaterThan(0)
const validators = await getCurrentValidators(request, urls.rpcPlatform)
expect(validators.length).toBeGreaterThan(0)
const xStatus = await httpStatus(request, urls.rpcXChain)
expect([200, 405]).toContain(xStatus)
console.log(` P(validators=${validators.length}) C(block=${cBlock}) X(HTTP ${xStatus})`)
})
test('>= 3 blockchains registered (P/C/X + app chains)', async ({ request }) => {
const chains = await getBlockchains(request, urls.rpcPlatform) as { name: string }[]
const names = chains.map(c => c.name)
console.log(` blockchains: ${chains.length} (${names.join(', ')})`)
// Primary chains: D(P), C, X. App chains tracked via separate net IDs.
expect(chains.length).toBeGreaterThanOrEqual(3)
})
})
// ── Zoo Cross-Chain ─────────────────────────────────────────────────
test.describe('Zoo Network Cross-Chain', () => {
test('Zoo C-Chain returns blocks', async ({ request }) => {
// Zoo RPC goes through a separate proxy — retry up to 3 times
let block = 0
for (let attempt = 0; attempt < 3; attempt++) {
try {
block = await ethBlockNumber(request, urls.rpcZoo)
break
} catch {
if (attempt < 2) await new Promise(r => setTimeout(r, 1000))
}
}
console.log(` Zoo block: ${block.toLocaleString()}`)
expect(block).toBeGreaterThan(0)
})
test('Lux + Zoo both producing blocks', async ({ request }) => {
const luxBlock = await ethBlockNumber(request, urls.rpc)
let zooBlock = 0
for (let attempt = 0; attempt < 3; attempt++) {
try {
zooBlock = await ethBlockNumber(request, urls.rpcZoo)
break
} catch {
if (attempt < 2) await new Promise(r => setTimeout(r, 1000))
}
}
console.log(` Lux=${luxBlock.toLocaleString()} Zoo=${zooBlock.toLocaleString()}`)
expect(luxBlock).toBeGreaterThan(0)
expect(zooBlock).toBeGreaterThan(0)
})
})
// ── MPC Service ─────────────────────────────────────────────────────
test.describe('MPC Service', () => {
test('MPC dashboard reachable', async ({ request }) => {
const status = await httpStatus(request, urls.mpc)
console.log(` MPC HTTP: ${status}`)
// 200, 301, 302, 307 (redirect to login), 403 (auth-protected)
expect([200, 301, 302, 307, 403]).toContain(status)
})
})
// ── Latency Benchmarks ──────────────────────────────────────────────
test.describe('RPC Latency Benchmarks', () => {
test('Lux C-Chain latency (10x eth_blockNumber)', async ({ request }) => {
const bench = await benchmarkRpc(request, urls.rpc, 'eth_blockNumber')
console.log(` avg=${bench.avg}ms p50=${bench.p50}ms p99=${bench.p99}ms errors=${bench.errors}`)
expect(bench.errors).toBeLessThan(5)
expect(bench.avg).toBeLessThan(5000)
})
test('Zoo RPC latency (10x eth_blockNumber)', async ({ request }) => {
const bench = await benchmarkRpc(request, urls.rpcZoo, 'eth_blockNumber')
console.log(` avg=${bench.avg}ms p50=${bench.p50}ms p99=${bench.p99}ms errors=${bench.errors}`)
expect(bench.errors).toBeLessThanOrEqual(5)
})
})