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]>
211 lines
11 KiB
TypeScript
211 lines
11 KiB
TypeScript
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.`)
|
||
})
|
||
})
|
||
}
|