vestige/apps/dashboard/e2e/palace-launch-gate.spec.ts
Sam Valladares 65595b9ffc checkpoint(dashboard): mobile reflow + field richness + fake-data fixes + predict FSRS urgency
Verified-green pre-OS-shell checkpoint. Mobile portrait reflow (portraitAdapt +
MobileNav), desktop field-richness on 7 text organs, 4 fake-data fixes (settings
version, TRIGGER BIRTH removed, predict real per-memory urgency, palace DIAG),
timeline dim-backdrop, stats depth-floor, em-dash cleanup, per-tier launch-gate
fill floors. check 0/0, 977 files. Cinema WIP isolated to stash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 10:32:22 +07:00

228 lines
9.6 KiB
TypeScript

// ─────────────────────────────────────────────────────────────────────────────
// THE LAUNCH GATE — "click any orb, land on a working page, every time."
//
// The Spatial Palace (/dashboard/palace) is a live 3D WebGPU constellation. As of
// the Jul 2026 launch curation it surfaces 9 HERO organs (ORGAN_REGIONS in
// src/lib/observatory/palace-map.ts); the other 10 organs still exist and are
// reachable by direct URL, just not palace-linked. A pointerdown on an orb runs
// pass.pickAt -> goto(`${base}${href}`), diving into that organ.
//
// This one spec is the ship gate, in two parts:
// 1. PALACE LOOP — the palace opens live, a real click-to-enter dive lands on a
// curated organ, and every one of the 9 curated organs lands on a LIVE,
// non-black, error-free page (then returns to the palace).
// 2. FULL URL SMOKE — all 20 routes (the 10 hidden organs included) mount a
// canvas and render a non-black, error-free field by direct URL.
//
// If even one lands dead/black/errored, this spec fails and we do not ship.
//
// Anti-false-green: non-black is asserted on DECODED luminance (avgLum + variance
// from a real compositor screenshot), never the sampleCanvas `rendered` flag
// whose size>20_000 fallback can pass a black field. WebGPU-readback constraint:
// reading the live canvas via Canvas2D returns BLACK in ANGLE/headless, so every
// pixel proof goes through sampleCanvas's screenshot path.
// ─────────────────────────────────────────────────────────────────────────────
import { test, expect, type Page } from '@playwright/test';
import { BASE, captureErrors, expectNoErrors, sampleCanvas, isAnimating } from './helpers/dashboard';
// The 9 organs the launch palace surfaces (mirrors ORGAN_REGIONS). settings is
// now a full-WebGPU in-canvas console like every other organ (no DOM panel).
const CURATED_ORGANS = [
'observatory',
'graph',
'memories',
'timeline',
'feed',
'explore',
'reasoning',
'stats',
'settings'
];
// The 10 organs hidden from the palace but still reachable by direct URL.
const HIDDEN_ORGANS = [
'contradictions',
'blackbox',
'duplicates',
'memory-prs',
'activation',
'dreams',
'schedule',
'importance',
'patterns',
'intentions'
];
const ALL_ROUTES = ['palace', ...CURATED_ORGANS, ...HIDDEN_ORGANS];
const CURATED_URL_RE = new RegExp(`/dashboard/(?:${CURATED_ORGANS.join('|')})(?:[/?#]|$)`);
// Some organs build their field over a couple seconds; give every landing a
// generous settle before sampling so slow-mounting organs aren't flaky.
const SETTLE_MS = 3200;
// /reasoning auto-runs a real 8-stage deep_reference on mount and stages its
// evidence galaxy from the response — that round-trip legitimately takes longer
// than a static field, so it gets an extended settle before we sample.
const SLOW_ORGANS: Record<string, number> = { reasoning: 12_000 };
// The liveness bar. TWO tiers, matching the SHIPPED design direction (Sam's
// Jul 2026 correction: "the field is COMPLETELY OVERPOWERING THE TEXT" — the
// field must be a DIM, legible BACKDROP on text-heavy organs, and the hero
// only on the visual organs).
//
// VISUAL organs (palace, graph, observatory, memories, activation, explore,
// timeline, blackbox): the field IS the hero — it fills the screen. High bar.
//
// TEXT-HEAVY organs (stats, settings, reasoning, feed, schedule, importance,
// contradictions, patterns, intentions, dreams, memory-prs, duplicates): the
// field is a DIM breathing backdrop behind a reading well so the MSDF text
// wins the contrast fight (each carries setIntensity ~0.18-0.26 + a reading
// well). fill% legitimately reads LOW here BY DESIGN — a high fill would mean
// the blinding-blob bug is back. The bar is still "lit + structured + moving,
// not black", just a lower fill floor.
//
// The gate samples a 96x96 decode of a real compositor screenshot.
const MIN_FILL_PCT_VISUAL = 30; // field is the hero → must be full-bleed
const MIN_FILL_PCT_TEXT = 6; // dim backdrop by design → lit but not blob
// Organs where the field is the HERO (full-bleed expected). Everything else is
// a text-heavy instrument with a dim backdrop.
const VISUAL_ORGANS = new Set([
'palace',
'graph',
'observatory',
'memories',
'activation',
'explore',
'timeline',
'blackbox'
]);
function minFillFor(label: string): number {
// label is like "/stats" or "dived organ .../stats" or "palace hub" — match
// the organ name loosely so both smoke and tour callers resolve correctly.
for (const organ of VISUAL_ORGANS) {
if (label.includes(organ)) return MIN_FILL_PCT_VISUAL;
}
return MIN_FILL_PCT_TEXT;
}
/** Assert a genuinely-lit, MOVING, error-free field on the current route. */
async function assertLiveField(
page: Page,
label: string
): Promise<{ avgLum: number; variance: number; fillPct: number }> {
const canvas = page.locator('canvas').first();
await canvas.waitFor({ state: 'attached', timeout: 15_000 });
// Slow organs (reasoning's deep_reference round-trip) settle longer.
const slow = Object.entries(SLOW_ORGANS).find(([name]) => label.includes(name));
await page.waitForTimeout(slow ? slow[1] : SETTLE_MS);
const s = await sampleCanvas(page);
expect(s.avgLum, `${label} avgLum (field must not be black)`).toBeGreaterThan(3);
expect(s.variance, `${label} variance (field must have structure)`).toBeGreaterThan(6);
const minFill = minFillFor(label);
expect(
s.fillPct,
`${label} fill% (field must be lit + structured; visual organs full-bleed, text organs a dim backdrop)`
).toBeGreaterThan(minFill);
return { avgLum: s.avgLum, variance: s.variance, fillPct: s.fillPct };
}
/** Probe a grid across the orbiting constellation until a pointerdown dives. */
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;
const offsets: Array<[number, number]> = [
[0, 0],
[0.08, 0], [-0.08, 0], [0, 0.08], [0, -0.08],
[0.16, 0.1], [-0.16, 0.1], [0.16, -0.1], [-0.16, -0.1],
[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);
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.up();
try {
await page.waitForURL(CURATED_URL_RE, { timeout: 900 });
return page.url();
} catch {
/* missed every orb at this probe point — keep spiralling */
}
}
await page.waitForTimeout(1200);
}
return null;
}
test.describe('LAUNCH GATE — palace loop', () => {
test('palace opens live and a real click-to-enter dive lands on a curated organ', async ({ page }) => {
const errors = captureErrors(page);
await page.goto(`${BASE}/palace`, { waitUntil: 'networkidle' });
await assertLiveField(page, 'palace');
expect(await isAnimating(page, 800), 'palace constellation should be orbiting').toBe(true);
const entered = await clickToEnter(page);
expect(entered, 'a pointerdown on an orb should dive into a curated organ').not.toBeNull();
expect(entered!, `landed URL should be a curated organ: ${entered}`).toMatch(CURATED_URL_RE);
await assertLiveField(page, `dived organ ${entered}`);
expectNoErrors(errors);
});
test('every curated organ lands on a live, error-free page and returns to the palace', async ({ page }) => {
// The tour dives 9 organs sequentially, each with a full settle + palace
// return, so it legitimately runs longer than the 60s per-test default.
test.setTimeout(180_000);
const errors = captureErrors(page);
await page.goto(`${BASE}/palace`, { waitUntil: 'networkidle' });
await assertLiveField(page, 'palace hub');
const report: Array<{ path: string; avgLum: number; variance: number; fillPct: number }> = [];
for (const path of CURATED_ORGANS) {
// Dive — exactly what the palace pick handler runs: goto(base + href).
await page.goto(`${BASE}/${path}`, { waitUntil: 'networkidle' });
const { avgLum, variance, fillPct } = await assertLiveField(page, `organ /${path}`);
report.push({ path, avgLum, variance, fillPct });
await test.info().attach(`organ-${path}.png`, {
body: await page.screenshot(),
contentType: 'image/png'
});
// Return to the palace between dives (the real orbit loop).
await page.goto(`${BASE}/palace`, { waitUntil: 'networkidle' });
await page.waitForTimeout(400);
}
await test.info().attach('launch-gate-report.json', {
body: Buffer.from(JSON.stringify(report, null, 2)),
contentType: 'application/json'
});
// Zero page / WebGPU-validation errors across the ENTIRE curated tour.
expectNoErrors(errors);
});
});
test.describe('LAUNCH GATE — full URL smoke (all 20 routes)', () => {
for (const path of ALL_ROUTES) {
test(`/${path} mounts a live, full-bleed, MOVING field`, async ({ page }) => {
const errors = captureErrors(page);
await page.goto(`${BASE}/${path}`, { waitUntil: 'networkidle' });
await assertLiveField(page, `/${path}`);
// Every organ must MOVE (living field, not a frozen frame). palace +
// timeline + the 17 field organs all orbit deterministically at rest.
expect(await isAnimating(page, 800), `/${path} field should be animating`).toBe(true);
expectNoErrors(errors);
});
}
});