test(e2e): reflect live governance surfaces — Dework work-boards + hanzo.vote guard
32-work-boards: work.zoo + work.pars now both serve the redesigned Dework board (luxfi/work-board). Replace the stale literal assertions (BountyV1 / #0 / "N bounties" — removed by the redesign, the 3 false failures) with the real structural contract via one parameterized factory: Dework chrome (icon-rail + w-64 <aside> sidebar + Overview / Community Suggestions / Leaderboards / Combined Board nav), inline-<svg> BrandMark (the logo is not an <img>), the Bounties Space-category grid, the To Do / In Progress / In Review / Done kanban columns, the emerald LIVE badge (not the amber FIXTURE fallback) + org chainId, and a UI<->chain check (the rendered treasury truncation equals on-chain BountyV1.treasury()). Un-skip Pars (now live, chain 494949); Lux/Hanzo stay skip-gated (HTTP-unreachable, brands.ts bounty==0x0). 31-vote: the vote shells are retiring -> work.<org>.network. Arm a hanzo.vote shell-health guard that hammers cold loads and fails on the intermittent blank-#root white-screen (Buffer-crash class) OR the "Made with love by Lux" cross-brand leak — a real, known-flagged regression, kept armed (not muted/skipped). lux/zoo/pars green. work-boards: 1 -> 10 passing (3 false failures fixed, Pars un-skipped, 2 skip). vote: hanzo.vote deterministically flagged; lux/zoo/pars (6) green, 4 skip. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+102
-35
@@ -1,23 +1,32 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import { test, expect, Page, Browser } from '@playwright/test'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vote surfaces — the 4 <org>.vote governance front-ends
|
||||
// ---------------------------------------------------------------------------
|
||||
// Regression guards for the per-org vote UIs. What's ASSERTED NOW (all live):
|
||||
// • HTTP 200 + correct brand in <title> and rendered DOM
|
||||
// RETIRING → work.<org>.network. Owner direction: the vote shells (Decent-fork,
|
||||
// reading ZERO chain) converge onto the WORK BOARD, which is the genuinely-live
|
||||
// governance surface (see 32-work-boards). Until each shell is replaced, these stay
|
||||
// as regression guards. For the three healthy shells (lux/zoo/pars) that means:
|
||||
// • HTTP 200 + correct brand in <title> and rendered DOM.
|
||||
// • ZERO cross-brand leakage — a surface must never render another org's brand
|
||||
// (nor a forbidden competitor term). This is the white-label invariant.
|
||||
// (nor a forbidden competitor term). The white-label invariant.
|
||||
// • NOT a white-screen — #root has real descendants and the body has real text.
|
||||
// (Catches the pars.vote Buffer-crash regression class, where the shell 200s
|
||||
// but renders nothing.)
|
||||
// • The Connect Wallet control is present in the DOM.
|
||||
// The "reads a real on-chain proposal/DAO tile" assertion stays SKIPPED (with the
|
||||
// exact flip condition) so the spec is SAVED and ready the moment a surface goes live.
|
||||
//
|
||||
// SKIP-gated until the CTO agent wires each shell to chain: as of 2026-07-17 the
|
||||
// four <org>.vote apps are Decent-fork shells that read ZERO chain — the genuinely
|
||||
// live governance surface is the WORK BOARD (see 32-work-boards). Owner direction:
|
||||
// converge vote → work.<org>.network. The "reads a real on-chain proposal/DAO tile"
|
||||
// assertion stays skipped (with the exact flip condition) so the spec is SAVED and
|
||||
// ready the moment a surface goes live.
|
||||
// hanzo.vote — ARMED shell-health guard, EXPECTED RED (real, flagged defects).
|
||||
// hanzo.vote is a broken, retiring shell with TWO intermittent defects (both tied to
|
||||
// an unstable Decent-fork render, verified 2026-07-17 — hanzo ~50% blank, lux/zoo/pars
|
||||
// 0/N):
|
||||
// 1. blank #root white-screen — HTTP 200 but an EMPTY #root, from
|
||||
// `Cannot read properties of undefined (reading 'address')` after a 403 (the
|
||||
// Buffer-crash class).
|
||||
// 2. a "Made with love by Lux" footer — Lux's name on a Hanzo surface, a
|
||||
// cross-brand white-label violation.
|
||||
// A single load is a coin-flip, so the guard HAMMERS several fresh loads and fails on
|
||||
// ANY white-screen or ANY cross-brand leak. It is NOT muted or skipped — it stays red
|
||||
// until work.hanzo replaces the shell.
|
||||
|
||||
interface Surface {
|
||||
org: string
|
||||
@@ -30,11 +39,11 @@ const ORG_BRANDS = ['Lux', 'Zoo', 'Hanzo', 'Pars']
|
||||
// Competitor / forbidden lineage terms (owner rules: no Avalanche, no "snowman").
|
||||
const FORBIDDEN = ['Avalanche', 'Snowman', 'Ava Labs']
|
||||
|
||||
const SURFACES: Surface[] = ORG_BRANDS.map(brand => ({
|
||||
const SURFACES: Surface[] = ORG_BRANDS.map((brand) => ({
|
||||
org: brand.toLowerCase(),
|
||||
brand,
|
||||
url: `https://${brand.toLowerCase()}.vote`,
|
||||
foreign: [...ORG_BRANDS.filter(b => b !== brand), ...FORBIDDEN],
|
||||
foreign: [...ORG_BRANDS.filter((b) => b !== brand), ...FORBIDDEN],
|
||||
}))
|
||||
|
||||
async function render(page: Page, url: string) {
|
||||
@@ -49,31 +58,89 @@ async function render(page: Page, url: string) {
|
||||
return { status: res?.status() ?? 0, title, rootKids, bodyText }
|
||||
}
|
||||
|
||||
// One cold load in a FRESH context (no cache/cookies) — how the blank-crash reproduces.
|
||||
// Only HTTP 200 responses count toward the checks; a nav/infra failure (goto throws /
|
||||
// non-200) is retried and is NOT the defect.
|
||||
async function coldLoad(browser: Browser, url: string, foreign: string[]) {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const ctx = await browser.newContext()
|
||||
const page = await ctx.newPage()
|
||||
try {
|
||||
const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
const status = res?.status() ?? 0
|
||||
await page.waitForTimeout(4000)
|
||||
const data = await page.evaluate((foreign) => {
|
||||
const r = document.getElementById('root') || document.getElementById('app') || document.getElementById('__next')
|
||||
const bt = document.body.innerText || ''
|
||||
return {
|
||||
rootKids: r ? r.querySelectorAll('*').length : -1,
|
||||
bodyLen: bt.trim().length,
|
||||
foreignHits: foreign.filter((t) => new RegExp(`\\b${t}\\b`, 'i').test(bt)),
|
||||
}
|
||||
}, foreign)
|
||||
await ctx.close()
|
||||
if (status !== 200) continue // infra hiccup, not the defect — retry
|
||||
return { status, ...data }
|
||||
} catch {
|
||||
await ctx.close() // nav error — retry
|
||||
}
|
||||
}
|
||||
return { status: 0, rootKids: -1, bodyLen: 0, foreignHits: [] as string[] }
|
||||
}
|
||||
|
||||
for (const s of SURFACES) {
|
||||
test.describe(`Vote Surface — ${s.brand} (${s.org}.vote)`, () => {
|
||||
test('renders with correct brand and is not a white-screen', async ({ page }) => {
|
||||
const { status, title, rootKids, bodyText } = await render(page, s.url)
|
||||
const clean = bodyText.replace(/\s+/g, ' ').trim()
|
||||
console.log(` ${s.org}.vote: HTTP ${status} · title=${JSON.stringify(title)} · #root kids=${rootKids} · bodyLen=${clean.length}`)
|
||||
if (s.org === 'hanzo') {
|
||||
// ARMED guard — expected RED. See file header. Do NOT mute or skip: a green
|
||||
// result means hanzo.vote stopped blank-crashing AND leaking (or work.hanzo
|
||||
// replaced it).
|
||||
test('shell-health guard — no white-screen, no cross-brand leak (RETIRING → work.hanzo)', async ({ browser }) => {
|
||||
const ATTEMPTS = 6
|
||||
const whiteScreens: number[] = []
|
||||
const leaks = new Set<string>()
|
||||
for (let i = 0; i < ATTEMPTS; i++) {
|
||||
const r = await coldLoad(browser, s.url, s.foreign)
|
||||
const blank = r.status === 200 && (r.rootKids <= 20 || r.bodyLen < 100)
|
||||
if (blank) whiteScreens.push(i + 1)
|
||||
r.foreignHits.forEach((t) => leaks.add(t))
|
||||
console.log(
|
||||
` hanzo.vote load ${i + 1}/${ATTEMPTS}: HTTP ${r.status} · #root kids=${r.rootKids} · bodyLen=${r.bodyLen}` +
|
||||
`${blank ? ' <== WHITE-SCREEN' : ''}${r.foreignHits.length ? ' <== LEAK: ' + r.foreignHits.join(',') : ''}`,
|
||||
)
|
||||
}
|
||||
const defects: string[] = []
|
||||
if (whiteScreens.length) defects.push(`white-screened on loads [${whiteScreens.join(',')}]/${ATTEMPTS} (blank #root — Buffer-crash class)`)
|
||||
if (leaks.size) defects.push(`leaked foreign brand(s): ${[...leaks].join(', ')} (white-label violation)`)
|
||||
expect(
|
||||
defects,
|
||||
`hanzo.vote is broken — ${defects.join(' · ')}. KNOWN-FLAGGED regression; retiring → work.hanzo.`,
|
||||
).toEqual([])
|
||||
})
|
||||
} else {
|
||||
test('renders with correct brand and is not a white-screen', async ({ page }) => {
|
||||
const { status, title, rootKids, bodyText } = await render(page, s.url)
|
||||
const clean = bodyText.replace(/\s+/g, ' ').trim()
|
||||
console.log(` ${s.org}.vote: HTTP ${status} · title=${JSON.stringify(title)} · #root kids=${rootKids} · bodyLen=${clean.length}`)
|
||||
|
||||
expect(status).toBe(200)
|
||||
// Brand in the document title AND in the rendered body.
|
||||
expect(title).toMatch(new RegExp(s.brand, 'i'))
|
||||
expect(title).toMatch(/DAO/i)
|
||||
expect(clean).toMatch(new RegExp(`\\b${s.brand}\\b`, 'i'))
|
||||
// Not a white-screen: real DOM under #root and real visible text.
|
||||
expect(rootKids).toBeGreaterThan(20)
|
||||
expect(clean.length).toBeGreaterThan(100)
|
||||
// Connect Wallet control present.
|
||||
await expect(page.getByText(/connect wallet/i).first()).toBeVisible()
|
||||
})
|
||||
expect(status).toBe(200)
|
||||
// Brand in the document title AND in the rendered body.
|
||||
expect(title).toMatch(new RegExp(s.brand, 'i'))
|
||||
expect(title).toMatch(/DAO/i)
|
||||
expect(clean).toMatch(new RegExp(`\\b${s.brand}\\b`, 'i'))
|
||||
// Not a white-screen: real DOM under #root and real visible text.
|
||||
expect(rootKids).toBeGreaterThan(20)
|
||||
expect(clean.length).toBeGreaterThan(100)
|
||||
// Connect Wallet control present.
|
||||
await expect(page.getByText(/connect wallet/i).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('has ZERO cross-brand / competitor leakage', async ({ page }) => {
|
||||
const { bodyText } = await render(page, s.url)
|
||||
const hits = s.foreign.filter(term => new RegExp(`\\b${term}\\b`, 'i').test(bodyText))
|
||||
console.log(` ${s.org}.vote foreign hits: ${hits.length ? hits.join(', ') : 'NONE'}`)
|
||||
expect(hits, `${s.org}.vote leaked foreign brand(s): ${hits.join(', ')}`).toEqual([])
|
||||
})
|
||||
test('has ZERO cross-brand / competitor leakage', async ({ page }) => {
|
||||
const { bodyText } = await render(page, s.url)
|
||||
const hits = s.foreign.filter((term) => new RegExp(`\\b${term}\\b`, 'i').test(bodyText))
|
||||
console.log(` ${s.org}.vote foreign hits: ${hits.length ? hits.join(', ') : 'NONE'}`)
|
||||
expect(hits, `${s.org}.vote leaked foreign brand(s): ${hits.join(', ')}`).toEqual([])
|
||||
})
|
||||
}
|
||||
|
||||
test('reads a live on-chain proposal / DAO tile', async () => {
|
||||
test.skip(true, `live after CTO agent deploy — ${s.org}.vote is a Decent-fork shell reading zero chain (memory 2026-07-17); flip when it renders an on-chain proposal/DAO tile. Owner direction: converge to work.${s.org}.network.`)
|
||||
|
||||
+186
-78
@@ -1,100 +1,208 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
import { CHAIN_IDS, orgRpc, ethCall } from './helpers'
|
||||
import { CHAIN_IDS, orgRpc, ethCall, decodeAddress } from './helpers'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Work boards — work.<org>.network on-chain bounty boards
|
||||
// ---------------------------------------------------------------------------
|
||||
// The work board is the GENUINELY-LIVE governance surface: it reads BountyV1 +
|
||||
// ReputationV1 straight off the org's C-Chain. work.zoo.network is live now;
|
||||
// the other orgs are skip-gated until their boards deploy.
|
||||
// The genuinely-live governance surface: luxfi/work-board, the redesigned "Dework"
|
||||
// app, reads BountyV1 + ReputationV1 straight off each org's sovereign C-Chain.
|
||||
// work.zoo.network and work.pars.network both run it (one white-label build per
|
||||
// brand), so they share ONE structural contract — asserted here via a factory.
|
||||
//
|
||||
// work.zoo asserts (all live):
|
||||
// • HTTP 200 + Zoo brand + no cross-brand leakage
|
||||
// • the LIVE badge is present and green (NOT the FIXTURE fallback — that fallback
|
||||
// is what renders when the chain read fails, so its absence proves a real read)
|
||||
// • a bounty sits in the Paid column (matches on-chain stateOf(0) == Paid)
|
||||
// • the on-chain ReputationV1 block renders (completed / earned)
|
||||
// • the board's bounty count EQUALS BountyV1.bountyCount() on-chain (UI ↔ chain)
|
||||
// Assert STRUCTURE, never transient specifics the redesign made dynamic. The
|
||||
// redesign DROPPED the old literal chrome (there is no "BountyV1"/"#0"/"N bounties"
|
||||
// text on the landing) — asserting those literals was the STALE-SPEC failure this
|
||||
// file fixes. The Dework contract that IS stable:
|
||||
// • Dework chrome: a 64px icon-rail + a 256px <aside> sidebar, the primary nav
|
||||
// (Overview / Community Suggestions / Leaderboards / Combined Board), and an
|
||||
// inline-<svg> BrandMark (the org logo is NOT an <img>).
|
||||
// • The Bounties grid of Space (category) cards — count is config-driven (5–6
|
||||
// across deploys), so assert a floor + the cross-brand-universal categories.
|
||||
// • The Combined Board (#/board) kanban columns: To Do / In Progress / In Review / Done.
|
||||
// • An emerald LIVE badge (NOT the amber FIXTURE fallback — its absence proves a
|
||||
// real chain read) naming the org chainId.
|
||||
// • UI↔chain: the rendered owner/treasury truncation EQUALS BountyV1.treasury()
|
||||
// on-chain (the "N bounties == bountyCount()" check the redesign removed is
|
||||
// replaced by this address cross-check, which still holds).
|
||||
// • ZERO cross-brand leakage (the white-label invariant).
|
||||
//
|
||||
// The on-chain deployment facts themselves are asserted in 30-dao-onchain.spec.ts.
|
||||
// work.lux / work.hanzo are skip-gated (HTTP-unreachable; C-Chains carry no
|
||||
// work-market yet — brands.ts bounty == 0x0 for both).
|
||||
|
||||
const bountyCountSel = '0x3e362c96' // bountyCount() -> uint256
|
||||
// BountyV1.treasury() selector — keccak256("treasury()")[:4].
|
||||
const TREASURY_SEL = '0x61d027b3'
|
||||
|
||||
async function render(page: Page, url: string) {
|
||||
const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
await page.waitForTimeout(5000)
|
||||
const title = await page.title()
|
||||
const rootKids = await page.evaluate(() => {
|
||||
const r = document.getElementById('root') || document.getElementById('app')
|
||||
return r ? r.querySelectorAll('*').length : -1
|
||||
})
|
||||
const bodyText = (await page.evaluate(() => document.body.innerText || '')).replace(/\s+/g, ' ').trim()
|
||||
return { status: res?.status() ?? 0, title, rootKids, bodyText }
|
||||
const ORG_BRANDS = ['Lux', 'Zoo', 'Hanzo', 'Pars']
|
||||
const FORBIDDEN = ['Avalanche', 'Snowman', 'Ava Labs']
|
||||
|
||||
// Spaces present in EVERY brand's Space set (brands.ts) — the cross-brand-stable
|
||||
// subset. Per-brand extras (Zoo "AI & Research", Pars "Education & Research") vary
|
||||
// by deploy, so assert these four + a card-count floor, never a brittle exact list.
|
||||
const UNIVERSAL_SPACES = ['Engineering', 'Community & Social', 'Content Creation', 'Governance']
|
||||
const KANBAN_LANES = ['To Do', 'In Progress', 'In Review', 'Done']
|
||||
|
||||
// The boards render on-chain addresses truncated as 0x1234…abcd (6-char head + … +
|
||||
// 4-char tail). Build the same truncation from an on-chain read for the UI↔chain check.
|
||||
function shortAddr(addr: string): string {
|
||||
const a = addr.toLowerCase()
|
||||
return `${a.slice(0, 6)}…${a.slice(-4)}`
|
||||
}
|
||||
|
||||
// ── Zoo Work Board (LIVE) ──────────────────────────────────────────
|
||||
test.describe('Work Board — Zoo (work.zoo.network)', () => {
|
||||
const url = 'https://work.zoo.network'
|
||||
const rpc = orgRpc('zoo', 'mainnet')
|
||||
// Wait for the SPA to finish its on-chain read: the status badge (LIVE or FIXTURE)
|
||||
// is the "read done" signal — wait for it instead of a blind sleep so the on-chain
|
||||
// assertions are stable.
|
||||
async function loadBoard(page: Page, url: string): Promise<number> {
|
||||
const res = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
await page
|
||||
.waitForFunction(() => /\b(LIVE|FIXTURE)\b/.test(document.body.innerText || ''), null, { timeout: 25_000 })
|
||||
.catch(() => {})
|
||||
await page.waitForTimeout(1_500)
|
||||
return res?.status() ?? 0
|
||||
}
|
||||
|
||||
test('renders as the Zoo work board with no cross-brand leakage', async ({ page }) => {
|
||||
const { status, title, rootKids, bodyText } = await render(page, url)
|
||||
console.log(` work.zoo: HTTP ${status} · title=${JSON.stringify(title)} · #root kids=${rootKids} · bodyLen=${bodyText.length}`)
|
||||
expect(status).toBe(200)
|
||||
expect(title).toMatch(/Zoo/i)
|
||||
expect(title).toMatch(/work board/i)
|
||||
expect(rootKids).toBeGreaterThan(20)
|
||||
expect(bodyText).toMatch(/\bZoo\b/i)
|
||||
// Shows the chain + BountyV1 it reads.
|
||||
expect(bodyText).toMatch(new RegExp(String(CHAIN_IDS.mainnet.zoo)))
|
||||
expect(bodyText).toMatch(/BountyV1/i)
|
||||
// No other org brand nor competitor term.
|
||||
for (const term of ['Hanzo', 'Pars', 'Avalanche', 'Snowman']) {
|
||||
expect(bodyText, `leaked ${term}`).not.toMatch(new RegExp(`\\b${term}\\b`, 'i'))
|
||||
// One structural read of the rendered board.
|
||||
async function probe(page: Page) {
|
||||
return await page.evaluate(() => {
|
||||
const bt = (document.body.innerText || '').replace(/\s+/g, ' ').trim()
|
||||
const root = document.getElementById('root') || document.getElementById('app')
|
||||
const aside = document.querySelector('aside')
|
||||
const gridCards = Math.max(
|
||||
0,
|
||||
...Array.from(document.querySelectorAll('[class*="grid-cols"]')).map((e) => e.children.length),
|
||||
)
|
||||
const h2 = Array.from(document.querySelectorAll('h2')).map((e) => (e.textContent || '').trim())
|
||||
const liveEl = Array.from(document.querySelectorAll('*')).find(
|
||||
(e) => e.children.length === 0 && /^LIVE$/.test((e.textContent || '').trim()),
|
||||
)
|
||||
return {
|
||||
title: document.title,
|
||||
rootKids: root ? root.querySelectorAll('*').length : -1,
|
||||
bodyText: bt,
|
||||
hasAside: !!aside,
|
||||
asideCls: aside ? aside.className : '',
|
||||
svgCount: document.querySelectorAll('svg').length,
|
||||
gridCards,
|
||||
h2,
|
||||
liveCls: liveEl ? (liveEl as HTMLElement).className : null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('shows a LIVE badge that is green (not the FIXTURE fallback)', async ({ page }) => {
|
||||
const { bodyText } = await render(page, url)
|
||||
expect(bodyText).toMatch(/\bLIVE\b/)
|
||||
expect(bodyText, 'board fell back to FIXTURE data — chain read failed').not.toMatch(/FIXTURE/i)
|
||||
// Locate the badge leaf and confirm it carries a green (live) style, not amber/red.
|
||||
const badge = await page.evaluate(() => {
|
||||
const el = Array.from(document.querySelectorAll('*'))
|
||||
.find(e => e.children.length === 0 && /^LIVE$/.test((e.textContent || '').trim()))
|
||||
if (!el) return null
|
||||
return { cls: el.className, color: getComputedStyle(el).color }
|
||||
interface Board {
|
||||
org: 'lux' | 'zoo' | 'hanzo' | 'pars'
|
||||
brand: string
|
||||
url: string
|
||||
chainId: number
|
||||
bounty: string // BountyV1 address — for the on-chain treasury() cross-check
|
||||
}
|
||||
|
||||
// The ONE structural contract for a live Dework work board, per brand.
|
||||
function deworkBoard(b: Board) {
|
||||
const rpc = orgRpc(b.org, 'mainnet')
|
||||
const foreign = [...ORG_BRANDS.filter((x) => x !== b.brand), ...FORBIDDEN]
|
||||
const host = b.url.replace('https://', '')
|
||||
|
||||
test.describe(`Work Board — ${b.brand} · Dework board (${host})`, () => {
|
||||
test('renders the Dework chrome (rail + sidebar + nav), correct brand, no cross-brand leakage', async ({ page }) => {
|
||||
const status = await loadBoard(page, b.url)
|
||||
const p = await probe(page)
|
||||
console.log(` ${host}: HTTP ${status} · title=${JSON.stringify(p.title)} · #root kids=${p.rootKids} · aside=${p.hasAside} · svg=${p.svgCount} · gridCards=${p.gridCards}`)
|
||||
expect(status).toBe(200)
|
||||
expect(p.title).toMatch(new RegExp(b.brand, 'i'))
|
||||
expect(p.title).toMatch(/work board/i)
|
||||
expect(p.rootKids).toBeGreaterThan(20)
|
||||
// Dework chrome: the 256px <aside> sidebar rail + the SPA nav sections.
|
||||
expect(p.hasAside, 'no <aside> sidebar rail — not the Dework board').toBe(true)
|
||||
expect(p.asideCls).toMatch(/w-64/)
|
||||
for (const nav of ['Overview', 'Community Suggestions', 'Leaderboards', 'Combined Board']) {
|
||||
expect(p.bodyText, `missing sidebar nav: ${nav}`).toMatch(new RegExp(nav, 'i'))
|
||||
}
|
||||
// The org logo is an inline <svg> BrandMark (NOT an <img>) — assert the mark exists.
|
||||
expect(p.svgCount, 'no inline-SVG brand/icon marks').toBeGreaterThan(0)
|
||||
expect(p.bodyText).toMatch(new RegExp(`\\b${b.brand}\\b`))
|
||||
// White-label invariant: never render another org's brand nor a competitor term.
|
||||
for (const term of foreign) {
|
||||
expect(p.bodyText, `leaked ${term}`).not.toMatch(new RegExp(`\\b${term}\\b`, 'i'))
|
||||
}
|
||||
})
|
||||
console.log(` LIVE badge: ${JSON.stringify(badge)}`)
|
||||
expect(badge).not.toBeNull()
|
||||
expect(badge!.cls, 'LIVE badge is not styled green').toMatch(/emerald|green|lime|teal/i)
|
||||
})
|
||||
|
||||
test('a bounty sits in the Paid column with the on-chain ReputationV1 block', async ({ page }) => {
|
||||
const { bodyText } = await render(page, url)
|
||||
// Paid column present and populated (bounty #0 is Paid on-chain).
|
||||
expect(bodyText).toMatch(/Paid/i)
|
||||
expect(bodyText).toMatch(/#0/)
|
||||
// On-chain reputation block: worker rep travels with the address.
|
||||
expect(bodyText).toMatch(/on-chain rep/i)
|
||||
expect(bodyText).toMatch(/completed/i)
|
||||
expect(bodyText).toMatch(/earned/i)
|
||||
})
|
||||
test('shows the emerald LIVE badge (not the amber FIXTURE fallback) for the org chainId', async ({ page }) => {
|
||||
await loadBoard(page, b.url)
|
||||
const p = await probe(page)
|
||||
expect(p.bodyText).toMatch(/\bLIVE\b/)
|
||||
expect(p.bodyText, 'board fell back to FIXTURE data — chain read failed').not.toMatch(/FIXTURE/i)
|
||||
console.log(` ${host} LIVE badge cls: ${p.liveCls}`)
|
||||
expect(p.liveCls, 'no LIVE badge leaf').not.toBeNull()
|
||||
expect(p.liveCls!, 'LIVE badge is not styled green').toMatch(/emerald|green|lime|teal/i)
|
||||
// The badge names the chain it read.
|
||||
expect(p.bodyText).toMatch(new RegExp(`chain\\s*${b.chainId}`))
|
||||
})
|
||||
|
||||
test('board bounty count equals BountyV1.bountyCount() on-chain (UI ↔ chain)', async ({ page, request }) => {
|
||||
const onchain = parseInt(await ethCall(request, rpc, '0x3EDb4a0104614b4aC12D5babCE984291aE8BE8E7', bountyCountSel), 16)
|
||||
const { bodyText } = await render(page, url)
|
||||
const m = bodyText.match(/(\d+)\s*bounties/i)
|
||||
const shown = m ? parseInt(m[1], 10) : -1
|
||||
console.log(` bounties: on-chain=${onchain} · board shows=${shown}`)
|
||||
expect(onchain).toBeGreaterThanOrEqual(1)
|
||||
expect(shown).toBe(onchain)
|
||||
})
|
||||
})
|
||||
test('the Bounties grid renders the org Space (category) cards', async ({ page }) => {
|
||||
await loadBoard(page, b.url)
|
||||
const p = await probe(page)
|
||||
console.log(` ${host} Bounties grid cards: ${p.gridCards}`)
|
||||
expect(p.bodyText).toMatch(/Bounties/i)
|
||||
// Config-driven Space count (brands.ts) — 5 or 6 across deploys. Floor at 5,
|
||||
// never a brittle exact count.
|
||||
expect(p.gridCards).toBeGreaterThanOrEqual(5)
|
||||
for (const cat of UNIVERSAL_SPACES) {
|
||||
expect(p.bodyText, `missing Bounties category: ${cat}`).toContain(cat)
|
||||
}
|
||||
})
|
||||
|
||||
// ── Lux / Hanzo / Pars Work Boards — skip-gated until deployed ─────
|
||||
for (const org of ['lux', 'hanzo', 'pars']) {
|
||||
test('the Combined Board shows the To Do / In Progress / In Review / Done kanban columns', async ({ page }) => {
|
||||
// The kanban lives on the #/board route (hash router); the landing is the grid.
|
||||
await loadBoard(page, `${b.url}/#/board`)
|
||||
const p = await probe(page)
|
||||
console.log(` ${host} Combined Board columns: ${JSON.stringify(p.h2)}`)
|
||||
for (const lane of KANBAN_LANES) {
|
||||
expect(p.h2, `missing kanban column: ${lane}`).toContain(lane)
|
||||
}
|
||||
})
|
||||
|
||||
test('UI ↔ chain: the board reflects the on-chain treasury + chainId (real read)', async ({ page, request }) => {
|
||||
const treasury = decodeAddress(await ethCall(request, rpc, b.bounty, TREASURY_SEL))
|
||||
const short = shortAddr(treasury)
|
||||
await loadBoard(page, b.url)
|
||||
const p = await probe(page)
|
||||
console.log(` ${host} on-chain treasury=${treasury} · board shows short=${short}`)
|
||||
// The rendered owner/treasury truncation equals the on-chain BountyV1.treasury().
|
||||
expect(p.bodyText.toLowerCase()).toContain(short)
|
||||
// And the rendered chainId equals the org's.
|
||||
expect(p.bodyText).toMatch(new RegExp(String(b.chainId)))
|
||||
// A real read, not the fixture fallback.
|
||||
expect(p.bodyText).not.toMatch(/FIXTURE/i)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── Live Dework boards ─────────────────────────────────────────────
|
||||
deworkBoard({
|
||||
org: 'zoo',
|
||||
brand: 'Zoo',
|
||||
url: 'https://work.zoo.network',
|
||||
chainId: CHAIN_IDS.mainnet.zoo, // 200200
|
||||
bounty: '0x3EDb4a0104614b4aC12D5babCE984291aE8BE8E7',
|
||||
})
|
||||
deworkBoard({
|
||||
org: 'pars',
|
||||
brand: 'Pars',
|
||||
url: 'https://work.pars.network',
|
||||
chainId: CHAIN_IDS.mainnet.pars, // 494949
|
||||
bounty: '0x316B41c886c7D4B4e38cBB08a243776Ed977cf1F',
|
||||
})
|
||||
|
||||
// ── Lux / Hanzo Work Boards — skip-gated until deployed ────────────
|
||||
// work.lux / work.hanzo are HTTP-unreachable (curl → no response, 2026-07-17) and
|
||||
// their C-Chains carry no work-market yet (Lux frozen at flag-day tip; Hanzo
|
||||
// key-gap) — brands.ts has bounty == 0x0…0 for both. Flip each when
|
||||
// work.<org>.network serves the board AND BountyV1 is deployed on the org C-Chain.
|
||||
for (const org of ['lux', 'hanzo'] as const) {
|
||||
test.describe(`Work Board — ${org} (work.${org}.network)`, () => {
|
||||
test('renders live board reading on-chain BountyV1', async () => {
|
||||
test.skip(true, `live after CTO agent deploy — work.${org}.network is not serving (HTTP unreachable 2026-07-17); flip when the board is up and reads BountyV1 on ${org}'s C-Chain`)
|
||||
test('renders live Dework board reading on-chain BountyV1', async () => {
|
||||
test.skip(true, `live after CTO agent deploy — work.${org}.network is HTTP-unreachable and ${org}'s C-Chain work-market is undeployed (brands.ts bounty == 0x0). Flip when the board serves and BountyV1 is live on ${org}.`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user