vestige/apps/dashboard/e2e/palace-navigation.spec.ts
Sam Valladares 844b91a804 feat(dashboard): every organ is a living WebGPU field (LivingFieldPass)
Turn all 17 sparse organs from text-on-black voids into full-bleed, moving,
bioluminescent WebGPU fields. Each cell maps to a real backend fact; verified
live on the real brain with a decoded fill% + motion + zero-error probe.

Foundation: one reusable LivingFieldPass (src/lib/observatory/field/) does
splat -> separable-blur (render passes) -> membrane base coat -> HDR cells with
orbital drift and a CPU pick-mirror. Each organ writes a ~20-line data->cells
mapper (cell-layout.ts: layoutGalaxy / layoutRings / FIELD_HUE).

Organs brought alive (fill% before -> after, all zero WebGPU errors):
stats 0.8->80.9, observatory 10.8->81.4, graph 12.3->82.4, memories ->78.4,
blackbox 18.2->83.3, contradictions 1.6->69.6, dreams 0.3->64.5,
settings 2.7->76.4, reasoning 2.7->39.2, patterns 7.1->45.6,
activation 10->64.9, feed ->89, explore ->83.3, intentions ->66.6,
importance ->73.7, memory-prs ->74.4, schedule ->37.1.

Backend surfaces: add api.memoryChangelog() + ChangelogResponse; surface
suppress/unsuppress on the memories field (shift/alt-click -> scar).

Tests: palace-launch-gate now asserts fillPct>30 + isAnimating (not just
non-black), with a longer settle for reasoning's deep_reference and a raised
timeout for the curated tour. all-routes-smoke treats settings as the WebGPU
field it now is. Gates green: check 0/0, build ok, palace-launch-gate 22/22 +
tour, all-routes-smoke 20/20.

Protected work untouched: MemoryCinema, graph/cinema, observatory scene
shaders, node-renderer, palace-map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 06:20:35 +07:00

238 lines
9.5 KiB
TypeScript

// ─────────────────────────────────────────────────────────────────────────────
// Spatial Palace — E2E navigation proof
//
// The Palace (/dashboard/palace) renders the 19 organ routes as ONE live 3D
// WebGPU constellation you orbit and click to enter. Source:
// src/routes/(app)/palace/+page.svelte
// - PalaceNodePass draws the orbiting orbs (own close-orbit camera).
// - TextLayerPass draws the MSDF HUD title "THE MEMORY PALACE" + per-orb labels.
// - onpointerdown -> pass.pickAt(ndc) -> goto(`${base}${hit.href}`) enters an organ.
//
// This spec proves, against the REAL brain (325+ memories) on :5199:
// 1. /palace mounts a live NON-BLACK WebGPU field that is ANIMATING (orbits).
// 2. The MSDF HUD title region is content-rich (bright text pixels top-left).
// 3. CLICK-TO-ENTER: a pointerdown on the constellation navigates to a
// /dashboard/<organ> route (nav is driven by onpointerdown, NOT click).
// 4. The entered organ's own WebGPU canvas renders.
// 5. No page errors and no WebGPU-validation errors fire.
//
// WebGPU-readback constraint: reading the live canvas via Canvas2D returns BLACK
// in this ANGLE/headless setup, so all pixel proofs go through the shared
// sampleCanvas / isAnimating helpers, which screenshot via the real compositor.
// ─────────────────────────────────────────────────────────────────────────────
import { test, expect, type Page } from '@playwright/test';
import {
BASE,
captureErrors,
expectNoErrors,
gotoRoute,
sampleCanvas,
isAnimating
} from './helpers/dashboard';
// Every organ href the palace can navigate into (mirrors ORGAN_REGIONS in
// src/lib/observatory/palace-map.ts). Any of these is a valid "entered" URL.
const ORGAN_PATHS = [
'observatory', 'reasoning', 'contradictions', 'blackbox', 'timeline',
'duplicates', 'graph', 'memory-prs', 'memories', 'feed', 'explore',
'activation', 'dreams', 'schedule', 'importance', 'patterns',
'intentions', 'stats', 'settings'
];
const ORGAN_URL_RE = new RegExp(`/dashboard/(?:${ORGAN_PATHS.join('|')})(?:[/?#]|$)`);
/**
* Sample the top-left HUD region where the MSDF title "THE MEMORY PALACE" is
* rendered (page places it at x:-0.94, y:0.88 in NDC — i.e. the upper-left).
* Returns avg luminance + variance of just that crop, decoded through the real
* compositor (same PNG-decode path sampleCanvas uses — Canvas2D on the LIVE
* WebGPU surface reads black, but a screenshot PNG decodes correctly).
*/
async function sampleTitleRegion(
page: Page
): Promise<{ avgLum: number; variance: number; size: number }> {
const canvas = page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) return { avgLum: 0, variance: 0, size: 0 };
// Top-left ~40% wide x ~18% tall — the HUD title band.
const clip = {
x: Math.round(box.x + box.width * 0.02),
y: Math.round(box.y + box.height * 0.02),
width: Math.round(box.width * 0.42),
height: Math.round(box.height * 0.16)
};
const buf = await page.screenshot({ clip });
const dataUrl = `data:image/png;base64,${buf.toString('base64')}`;
const stats = await page.evaluate(async (url) => {
const img = new Image();
await new Promise<void>((res, rej) => {
img.onload = () => res();
img.onerror = () => rej(new Error('img load'));
img.src = url;
});
const t = document.createElement('canvas');
t.width = 160;
t.height = 64;
const ctx = t.getContext('2d');
if (!ctx) return { avgLum: 0, variance: 0 };
ctx.drawImage(img, 0, 0, 160, 64);
const d = ctx.getImageData(0, 0, 160, 64).data;
let sum = 0;
const lums: number[] = [];
for (let i = 0; i < d.length; i += 4) {
const l = (d[i] + d[i + 1] + d[i + 2]) / 3;
lums.push(l);
sum += l;
}
const avg = sum / lums.length;
let v = 0;
for (const l of lums) v += (l - avg) * (l - avg);
return { avgLum: Math.round(avg), variance: Math.round(v / lums.length) };
}, dataUrl);
return { ...stats, size: buf.length };
}
/**
* Click points across the constellation until a pointerdown lands on an orb and
* navigates. The camera slowly ORBITS, so the center node drifts — we spiral out
* from center (50%,50%) probing a small grid, re-trying after a short settle so a
* moving orb rotates under the cursor. Returns the URL we landed on, or null.
*
* Uses page.mouse.down/up (real pointerdown) because the handler is
* onpointerdown — a plain click also fires pointerdown, but we assert the
* pointerdown path explicitly.
*/
async function clickToEnter(page: Page): Promise<string | null> {
const canvas = page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) return null;
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
// Fractions of half-width/height to probe, spiralling out from dead center.
// The center organ (/observatory) sits at the origin so 0,0 usually hits; the
// outer rings catch orbiting satellite orbs if the center just drifted away.
const offsets: Array<[number, number]> = [
[0, 0],
[0.08, 0], [-0.08, 0], [0, 0.08], [0, -0.08],
[0.16, 0.10], [-0.16, 0.10], [0.16, -0.10], [-0.16, -0.10],
[0.28, 0], [-0.28, 0], [0, 0.24], [0, -0.24],
[0.34, 0.18], [-0.34, 0.18], [0.34, -0.18], [-0.34, -0.18]
];
for (let pass = 0; pass < 3; pass++) {
for (const [ox, oy] of offsets) {
const x = cx + ox * (box.width / 2);
const y = cy + oy * (box.height / 2);
const before = page.url();
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.up();
// pointerdown -> pickAt -> goto is synchronous-ish; give the SPA router a beat.
try {
await page.waitForURL(ORGAN_URL_RE, { timeout: 900 });
return page.url();
} catch {
// URL didn't change (missed every orb at this point) — keep probing.
if (page.url() !== before && ORGAN_URL_RE.test(page.url())) {
return page.url();
}
}
}
// Let the constellation orbit a little so a fresh set of orbs slides under
// the probe grid, then retry.
await page.waitForTimeout(1200);
}
return null;
}
test.describe('Spatial Palace — navigation', () => {
test('renders a live, animating constellation with an MSDF HUD title', async ({ page }) => {
const errors = captureErrors(page);
await gotoRoute(page, '/palace');
// The palace builds its constellation + MSDF layer over a couple seconds.
await page.waitForTimeout(3000);
// (1) Live NON-BLACK field.
const sample = await sampleCanvas(page);
expect(
sample.rendered,
`palace field should render non-black — avgLum=${sample.avgLum} variance=${sample.variance}`
).toBe(true);
await test.info().attach('palace-field.png', {
body: await page.screenshot(),
contentType: 'image/png'
});
// (1b) Field is ANIMATING — the constellation orbits, so two frames differ.
const animating = await isAnimating(page, 800);
expect(animating, 'palace constellation should be orbiting (animated field)').toBe(true);
// (2) MSDF HUD title "THE MEMORY PALACE" is visibly rendered top-left.
// It's WebGPU text (no DOM node), so prove it via bright pixels + structure
// in the title crop, plus a content-rich full-frame screenshot.
const title = await sampleTitleRegion(page);
await test.info().attach('palace-title-region.png', {
body: await page.screenshot({
clip: await (async () => {
const b = (await page.locator('canvas').first().boundingBox())!;
return {
x: Math.round(b.x + b.width * 0.02),
y: Math.round(b.y + b.height * 0.02),
width: Math.round(b.width * 0.42),
height: Math.round(b.height * 0.16)
};
})()
}),
contentType: 'image/png'
});
expect(
title.avgLum > 3 && title.variance > 6,
`HUD title region should contain bright, structured text pixels — avgLum=${title.avgLum} variance=${title.variance} size=${title.size}`
).toBe(true);
// Whole-frame content-richness: a living MSDF+nebula palace compresses large.
const full = await page.screenshot();
expect(
full.length,
`palace frame should be content-rich (>100KB) — was ${full.length} bytes`
).toBeGreaterThan(100_000);
expectNoErrors(errors);
});
test('click-to-enter dives into an organ, whose canvas then renders', async ({ page }) => {
const errors = captureErrors(page);
await gotoRoute(page, '/palace');
await page.waitForTimeout(3000);
expect(page.url()).toContain('/palace');
// (3) pointerdown on the constellation navigates to /dashboard/<organ>.
const entered = await clickToEnter(page);
expect(
entered,
'a pointerdown on the constellation should navigate into an organ route'
).not.toBeNull();
expect(entered!, `landed URL should be an organ route: ${entered}`).toMatch(ORGAN_URL_RE);
await test.info().attach('palace-entered-organ.png', {
body: await page.screenshot(),
contentType: 'image/png'
});
// (4) The entered organ mounts + renders its own WebGPU canvas.
const organCanvas = page.locator('canvas').first();
await organCanvas.waitFor({ state: 'attached', timeout: 15_000 });
await page.waitForTimeout(2500);
const organSample = await sampleCanvas(page);
expect(
organSample.rendered,
`entered organ (${entered}) should render a non-black canvas — avgLum=${organSample.avgLum} variance=${organSample.variance}`
).toBe(true);
// (5) No page errors, no WebGPU-validation errors across the dive.
expectNoErrors(errors);
});
});