Files
universe/e2e/32-work-boards.spec.ts
zeekayandHanzo Dev 78ee638610 test(e2e): correct work.lux/work.hanzo skip-gate — now serve (FIXTURE), not unreachable
work.lux / work.hanzo came up during rollout: they now serve the Dework shell
(HTTP 200) but render the amber FIXTURE fallback (owner 0x0000…0000 + "Missing or
invalid parameters") because their C-Chain work-market is undeployed (brands.ts
bounty == 0x0 — Lux frozen at flag-day tip, Hanzo key-gap). Keep them skip-gated
(un-skipping would fail the emerald-LIVE invariant), but fix the stale
"HTTP-unreachable" reason to the accurate go-live condition: flip when BountyV1 is
deployed on the org C-Chain and the board shows the emerald LIVE badge.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 15:46:03 -07:00

211 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, Page } from '@playwright/test'
import { CHAIN_IDS, orgRpc, ethCall, decodeAddress } from './helpers'
// ---------------------------------------------------------------------------
// Work boards — work.<org>.network on-chain bounty boards
// ---------------------------------------------------------------------------
// 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.
//
// 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).
// BountyV1.treasury() selector — keccak256("treasury()")[:4].
const TREASURY_SEL = '0x61d027b3'
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)}`
}
// 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
}
// 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,
}
})
}
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'))
}
})
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('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)
}
})
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 their work-market deploys ──
// work.lux / work.hanzo now SERVE the Dework shell (HTTP 200) but render the amber
// FIXTURE fallback — owner 0x0000…0000 + "Missing or invalid parameters" — because
// their C-Chain work-market is undeployed (brands.ts bounty == 0x0; Lux frozen at the
// flag-day tip, Hanzo key-gap), so there is no BountyV1 to read. The org C-Chains
// themselves are up (96369 / 36963). Flip each when BountyV1 is deployed on the org
// C-Chain AND the board flips to the emerald LIVE badge (a real read).
for (const org of ['lux', 'hanzo'] as const) {
test.describe(`Work Board — ${org} (work.${org}.network)`, () => {
test('renders live Dework board reading on-chain BountyV1', async () => {
test.skip(true, `live after CTO agent deploy — work.${org}.network serves the Dework shell but reads FIXTURE (brands.ts bounty == 0x0, board shows owner 0x0000…0000). Flip when BountyV1 is deployed on ${org}'s C-Chain and the board shows the emerald LIVE badge.`)
})
})
}