CI/CD / render (push) Waiting to run
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]>
143 lines
7.5 KiB
TypeScript
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')
|
|
})
|
|
})
|