- 02-PERF-BASELINE.json: on-device baseline recorded against archi-dev-box
(real hardware, D-11's verification target). 13/15 surfaces measured
cleanly across 3 runs each; Mesh and Chat are recorded as unmeasured with
their reasons (Mesh: no connected mesh device on this node within the
wait window; Chat: AIUI's own loading overlay outlives the close-button
click budget) rather than silently marked already-fast, per plan rule.
- measure.ts: goHome() now falls back to a hard `page.goto('/dashboard')`
when the Chat surface's own close button is unreachable (blocked behind
AIUI's connecting overlay on real hardware) — without this, every
surface after Chat inherited a permanently hidden sidebar
(`v-show="!chatFullscreen"`) and cascaded to unmeasured. This recovery
path is exempted from the "no page.goto between surfaces" rule because
it exists only to break out of a stuck state, not to measure one.
- surface-perf.spec.ts: currentCommit() now shells out with `process.cwd()`
instead of `__dirname`, which is unavailable under this package's
`"type": "module"` runtime and was silently recording every run header's
`commit` field as 'unknown'.
No file under neode-ui/src/ was modified by this task (git diff --name-only
HEAD -- neode-ui/src is empty).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
// Re-runnable surface-perf harness (02-01-PLAN.md Task 1). Logs in using the
|
|
// exact flow from app-launch.spec.ts, walks every SURFACES row via
|
|
// measureSurface(), and writes the full result array + a run header to
|
|
// ARCHY_PERF_OUT (defaulting to e2e/test-results/surface-perf.json).
|
|
//
|
|
// Redaction is structural, not a cleanup pass: measure.ts's RpcTracker only
|
|
// ever records method name + timing (T-02-06) — request/response bodies,
|
|
// page text and screenshots are never captured into the artifact.
|
|
import { execSync } from 'node:child_process'
|
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { expect, test, type Page } from '@playwright/test'
|
|
import { measureSurface, type SurfaceMeasurement } from './measure'
|
|
import { SURFACES } from './surfaces'
|
|
|
|
const PASSWORD = process.env.ARCHY_PASSWORD ?? 'password123'
|
|
const RUNS = process.env.ARCHY_PERF_RUNS ? Number(process.env.ARCHY_PERF_RUNS) : 3
|
|
const OUT_PATH = resolve(process.cwd(), process.env.ARCHY_PERF_OUT ?? 'e2e/test-results/surface-perf.json')
|
|
|
|
async function login(page: Page): Promise<void> {
|
|
// Mirrors e2e/app-launch.spec.ts's login() verbatim — do not invent a
|
|
// second auth path.
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' })
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('neode_intro_seen', '1')
|
|
localStorage.setItem('neode_onboarding_complete', '1')
|
|
})
|
|
await page.goto('/login', { waitUntil: 'networkidle' })
|
|
|
|
const passwordInput = page.locator('input[type="password"]').first()
|
|
await passwordInput.waitFor({ timeout: 15_000 })
|
|
await passwordInput.fill(PASSWORD)
|
|
await page
|
|
.locator('button:has-text("Login"), button:has-text("Unlock"), button:has-text("Continue"), button[type="submit"]')
|
|
.first()
|
|
.click()
|
|
await page.waitForURL('**/dashboard**', { timeout: 20_000 })
|
|
}
|
|
|
|
function currentCommit(): string {
|
|
try {
|
|
// `__dirname` is unavailable under this package's `"type": "module"` ESM
|
|
// runtime (Playwright's own transform swallows the ReferenceError into
|
|
// the catch below, silently yielding 'unknown') — use `process.cwd()`
|
|
// instead, which Playwright always sets to the project root it was
|
|
// invoked from.
|
|
return execSync('git rev-parse --short HEAD', { cwd: process.cwd() }).toString().trim()
|
|
} catch {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
test('surface-perf: measure every D-09 surface and write the baseline artifact', async ({ page, baseURL }) => {
|
|
test.setTimeout(20 * 60 * 1000) // 15 surfaces x 3 runs x network round-trips can run long on a real node
|
|
|
|
await login(page)
|
|
|
|
const results: SurfaceMeasurement[] = []
|
|
const skipped: string[] = []
|
|
|
|
for (const surface of SURFACES) {
|
|
try {
|
|
const measurement = await measureSurface(page, surface, { runs: RUNS })
|
|
results.push(measurement)
|
|
if (measurement.error) skipped.push(`${surface.id}: ${measurement.error}`)
|
|
} catch (err) {
|
|
// A surface that throws outside measureSurface's own per-run try/catch
|
|
// (e.g. login state got corrupted) is still recorded, never dropped —
|
|
// an unmeasured surface must never silently disappear from the array.
|
|
results.push({
|
|
id: surface.id,
|
|
label: surface.label,
|
|
path: surface.path,
|
|
kind: surface.kind,
|
|
runs: RUNS,
|
|
samples: [],
|
|
firstVisitMs: null,
|
|
revisitMs: null,
|
|
firstVisitRpcCount: null,
|
|
revisitRpcCount: null,
|
|
revisitRpcCalls: [],
|
|
maxConcurrentRpc: null,
|
|
rpcWallClockMs: null,
|
|
remounted: null,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
})
|
|
skipped.push(`${surface.id}: ${err instanceof Error ? err.message : String(err)}`)
|
|
}
|
|
}
|
|
|
|
const header = {
|
|
baseUrl: baseURL ?? process.env.ARCHY_BASE_URL ?? 'http://192.168.1.228',
|
|
takenAt: new Date().toISOString(),
|
|
commit: currentCommit(),
|
|
runs: RUNS,
|
|
notes: skipped.length > 0 ? `Skipped/errored surfaces: ${skipped.join('; ')}` : 'All surfaces measured cleanly.',
|
|
}
|
|
|
|
const artifact = { ...header, results }
|
|
|
|
mkdirSync(dirname(OUT_PATH), { recursive: true })
|
|
writeFileSync(OUT_PATH, JSON.stringify(artifact, null, 2))
|
|
|
|
expect(results.length).toBe(SURFACES.length)
|
|
})
|