Files
universe/e2e/50-admin-console.spec.ts
zeekayandHanzo AI 15131f6af4
CI/CD / render (push) Waiting to run
chain letters are lowercase
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.

Co-authored-by: Hanzo AI <[email protected]>
2026-09-08 15:23:20 -07:00

143 lines
7.5 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { env } from './env'
import { snap } from './helpers'
// ---------------------------------------------------------------------------
// THE OPERATOR CONSOLE — /admin
// ---------------------------------------------------------------------------
// The Lux admin surface: bridge fees, wallets, MPC quorums, treasury Safes and
// thresholds, a contract registry probed on every network, nodes and validators,
// public-surface health, and the economy levers. It lives at /admin in the
// bootnode console image — the same image that serves lux.cloud — so it is a
// route tree, not a second deployment.
//
// WHERE THIS RUNS
//
// ADMIN_BASE=http://localhost:4321 during development, against the local
// static export
// default <cloud>/admin, i.e. lux.cloud/admin
//
// admin.lux.cloud is deliberately NOT the default target: it is fronted by
// admin-guard, so an unauthenticated run gets the guard's redirect and would be
// asserting the guard, not the console. Point ADMIN_BASE at it with a session
// cookie when you want to test the gated path.
//
// ASSERTIONS ARE POSITIVE, ALWAYS.
//
// Two false passes in one day came from asserting the ABSENCE of an error
// string: a blank page, a 404 body and a redirect to a login screen all contain
// no error text. Every test below names something the real page contains — a
// section title, a chain ID, a live figure — and fails when it is missing.
//
// THE FIRST TEST IS RED UNTIL CUTOVER, AND THAT IS THE POINT.
// The console is built and committed; the Ingress that points admin.lux.cloud at
// it (hanzoai/universe infra/k8s/admin-console/ingress-lux-operator.yaml) is
// written and owner-gated. Until the image carrying /admin is published, "the
// operator console is reachable" fails. Do not delete the test to get green.
const BASE = process.env.ADMIN_BASE ?? `${env.urls.cloud}/admin`
/** Pages, and one thing each MUST contain when it has really rendered. */
const PAGES: { path: string; name: string; must: RegExp }[] = [
{ path: '', name: 'overview', must: /Network overview/i },
{ path: '/nodes', name: 'nodes', must: /Nodes & validators/i },
{ path: '/surfaces', name: 'surfaces', must: /Not readable from a browser/i },
{ path: '/contracts', name: 'contracts', must: /Contract registry/i },
{ path: '/treasury', name: 'treasury', must: /Prepare a transfer/i },
{ path: '/wallets', name: 'wallets', must: /Where a wallet write actually happens/i },
{ path: '/mpc', name: 'mpc', must: /Check the image tag, not the config/i },
{ path: '/bridge', name: 'bridge', must: /The three fee values/i },
{ path: '/economy', name: 'economy', must: /Economy levers/i },
{ path: '/risks', name: 'risks', must: /Risk ledger|What this network is getting wrong/i },
]
test.describe(`Operator console — ${env.network} (${BASE})`, () => {
test('the operator console is reachable', async ({ page }) => {
const res = await page.goto(BASE, { waitUntil: 'domcontentloaded' })
expect(res?.status(), `${BASE} must serve the console`).toBeLessThan(400)
// Positive: the shell's own wordmark and its section rail.
await expect(page.getByText('Operator console').first()).toBeVisible()
await expect(page.getByRole('link', { name: /Contract registry/i })).toBeVisible()
await expect(page.getByRole('link', { name: /Economy levers/i })).toBeVisible()
})
for (const p of PAGES) {
test(`${p.name} renders its own content`, async ({ page }) => {
const res = await page.goto(`${BASE}${p.path}`, { waitUntil: 'domcontentloaded' })
test.skip((res?.status() ?? 500) >= 400, 'console not deployed yet — see the reachability test above')
await expect(page.locator('body')).toContainText(p.must, { timeout: 20_000 })
await snap(page, `admin-${p.name}`)
})
}
test('the network selector carries all three chain IDs', async ({ page }) => {
const res = await page.goto(BASE, { waitUntil: 'domcontentloaded' })
test.skip((res?.status() ?? 500) >= 400, 'console not deployed yet')
const rail = page.locator('.admin-netpick')
await expect(rail.getByRole('button', { name: /Mainnet/ })).toBeVisible()
for (const id of [env.chainIds.lux, 96368, 96367]) {
await expect(rail).toContainText(String(id))
}
})
test('the overview reads a live chain head, not a placeholder', async ({ page }) => {
const res = await page.goto(BASE, { waitUntil: 'domcontentloaded' })
test.skip((res?.status() ?? 500) >= 400, 'console not deployed yet')
// The height stat must become a real, grouped number. A skeleton or an
// em-dash fails this — which is the correct outcome when the RPC is down.
await expect(page.getByText(/^[1-9][0-9,]{3,}$/).first()).toBeVisible({ timeout: 30_000 })
// And the receipt must name the endpoint that produced it.
await expect(page.locator('.receipt').first()).toContainText('/v1/chain/c/rpc')
})
test('every value-moving control is behind the owner gate', async ({ page }) => {
const res = await page.goto(`${BASE}/treasury`, { waitUntil: 'domcontentloaded' })
test.skip((res?.status() ?? 500) >= 400, 'console not deployed yet')
await expect(page.getByText(/This surface does not move value/i)).toBeVisible()
const prepare = page.locator('button.btn.gate').first()
await expect(prepare).toBeVisible({ timeout: 20_000 })
await prepare.click()
// The preview must state the authority, the parameters and the stop.
const sheet = page.locator('.sheet')
await expect(sheet).toBeVisible()
await expect(sheet).toContainText(/Owner confirmation required/i)
await expect(sheet).toContainText(/Exactly what would be sent/i)
await expect(sheet).toContainText(/Signatures collected/i)
await expect(sheet).toContainText(/Stop\./)
await snap(page, 'admin-owner-gate')
// Confirming yields a handoff document that says, in the document itself,
// that nothing was executed.
await sheet.getByRole('button', { name: /I understand/i }).click()
await expect(sheet).toContainText('"executed": false')
await expect(sheet).toContainText('"requiresOwnerSignature": true')
})
test('the risk ledger names the findings an operator must not miss', async ({ page }) => {
const res = await page.goto(`${BASE}/risks`, { waitUntil: 'domcontentloaded' })
test.skip((res?.status() ?? 500) >= 400, 'console not deployed yet')
const body = page.locator('body')
// Each of these was a real trap. A console that stops surfacing them has
// regressed into the thing it was built to replace.
await expect(body).toContainText(/degree 0/i)
await expect(body).toContainText(/0\.0000 uptime/i)
await expect(body).toContainText(/graph pods/i)
await expect(body).toContainText(/Access-Control-Allow-Origin/i)
await expect(body).toContainText(/Check the image tag/i)
})
test('the bridge page shows all three fee values, not one', async ({ page }) => {
const res = await page.goto(`${BASE}/bridge`, { waitUntil: 'domcontentloaded' })
test.skip((res?.status() ?? 500) >= 400, 'console not deployed yet')
const body = page.locator('body')
await expect(body).toContainText(/the rate users actually pay/i)
await expect(body).toContainText(/the value the admin UI writes/i)
await expect(body).toContainText(/the on-chain teleporter rate/i)
await expect(body).toContainText(/dead knob/i)
// The live on-chain read, not the documented constant.
await expect(page.locator('.receipt').first()).toContainText('eth_call')
})
})