vestige/apps/dashboard/e2e/organ-explore.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

198 lines
8.4 KiB
TypeScript
Raw 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.

// ─────────────────────────────────────────────────────────────────────────────
// EXPLORE — the semantic-expedition MSDF neighbor field (real-data ownership spec).
//
// This organ renders the REAL brain's semantic search neighbors as a
// cursor-reactive MSDF text field (TextLayerPass over the recall-path observatory
// scene). It consumes GET /api/search?q=Q&limit=40 and lays each hit out as a row:
// snippet | id | similarity% | retention%. The cursor swells/leans nearby glyphs;
// clicking a row navigates to that memory's detail page. loadNeighbors() guards an
// empty query (no ?q= → no request, calm "TYPE A QUERY" prompt), so the organ only
// hits the API when driven with a real ?q=.
//
// The 7-point organ contract proven here:
// 1. REACHABLE — /dashboard/explore?q=memory mounts a WebGPU canvas.
// 2. RENDERS REAL DATA — the same real neighbors curl returns drive the field
// (non-black render). similarity uses combinedScore, retention uses
// retentionStrength — both present in the real payload.
// 3. ALIVE — the field animates at idle (per-glyph time wobble + reveal +
// recall-path scene), no interaction required.
// 4. CRASH-FREE pick + hover — a hover sweep (pickAt + cursor-lens) and an
// off-row click grid never throw a page/WebGPU error; a targeted row click
// navigates to the memory detail page (goto), the valid non-crash outcome.
// pickAt mirrors the shader's aspect transform; the cursor swell/lean is a
// small sub-glyph jitter around a STATIC anchor, so picks track the rows.
// 5. HONEST states — no ?q= renders the calm "TYPE A QUERY" invite (never fake
// data, never a black/errored surface); a real-but-empty query says
// "NO NEIGHBORS FOR ...". Verified against the +page.svelte empty branch.
// ─────────────────────────────────────────────────────────────────────────────
import { test, expect } from '@playwright/test';
import {
BASE,
captureErrors,
expectNoErrors,
gotoRoute,
sampleCanvas,
isAnimating
} from './helpers/dashboard';
const API = process.env.VESTIGE_API_TARGET ?? 'http://127.0.0.1:3931';
test('explore organ mounts a canvas and renders the REAL neighbor field', async ({ page, request }) => {
// 2 (real data) — curl the real brain FIRST: a real query must return neighbors,
// and each must carry the fields the field lays out (content, id, and the
// similarity/retention channels combinedScore + retentionStrength).
const apiRes = await request.get(`${API}/api/search?q=memory&limit=40`);
expect(apiRes.ok(), 'GET /api/search?q=memory must be 200').toBe(true);
const payload = (await apiRes.json()) as {
query: string;
total: number;
durationMs: number;
results: { id: string; content: string; combinedScore?: number; retentionStrength?: number }[];
};
expect(payload.query, 'echoed query must match').toBe('memory');
expect(Array.isArray(payload.results)).toBe(true);
expect(payload.results.length, 'real brain should return neighbors for "memory"').toBeGreaterThan(0);
const first = payload.results[0];
expect(typeof first.id).toBe('string');
expect(typeof first.content).toBe('string');
// The depth (similarity) + weight (retention) channels come from these fields.
expect(typeof first.combinedScore).toBe('number');
expect(typeof first.retentionStrength).toBe('number');
const errors = captureErrors(page);
// Drive with a real ?q= so loadNeighbors() actually fetches (empty q is guarded).
await gotoRoute(page, '/explore?q=memory');
// The field fetches neighbors then reveals row-by-row over ~2s; settle first.
await page.waitForTimeout(3500);
// 1 + 2 — the canvas renders a non-black field (the real neighbor rows lit it up).
const sample = await sampleCanvas(page);
expect(
sample.rendered,
`explore field should render real neighbors (avgLum=${sample.avgLum} variance=${sample.variance})`
).toBe(true);
expectNoErrors(errors);
});
test('explore field is ALIVE at idle (no interaction required)', async ({ page }) => {
const errors = captureErrors(page);
await gotoRoute(page, '/explore?q=memory');
await page.waitForTimeout(3500);
const sample = await sampleCanvas(page);
expect(sample.rendered, `field must render before checking motion (avgLum=${sample.avgLum})`).toBe(
true
);
// Per-glyph time wobble + reveal + the recall-path scene animate continuously.
// Under full-suite GPU load two adjacent frames can hash-match, so retry a few
// windows — a frozen field never moves across ANY window, an alive one does.
let moved = false;
for (let i = 0; i < 4 && !moved; i++) {
moved = await isAnimating(page, 700);
}
expect(moved, 'explore neighbor field should animate at idle').toBe(true);
expectNoErrors(errors);
});
test('hovering + off-row clicks never crash; a row click navigates (real picks survive)', async ({
page
}) => {
const errors = captureErrors(page);
const canvas = await gotoRoute(page, '/explore?q=memory');
await page.waitForTimeout(3500);
const box = await canvas.boundingBox();
expect(box).not.toBeNull();
if (!box) return;
// Hover sweep — drives pointermove → pickAt + cursor-lens writes across the
// field (the parallax swell/lean path). Must not throw on any point.
const hoverPts = [
[0.2, 0.25],
[0.35, 0.4],
[0.3, 0.55],
[0.25, 0.7],
[0.4, 0.85],
[0.6, 0.5]
];
for (const [fx, fy] of hoverPts) {
await page.mouse.move(box.x + box.width * fx, box.y + box.height * fy);
await page.waitForTimeout(120);
}
// Off-row click grid — the rows anchor down the LEFT column (x=-0.88); the
// right half of the field has no pickable rows. Clicking there exercises
// pickAt returning null (no nav) and must survive every click with no error.
const offRowClicks = [
[0.7, 0.3],
[0.8, 0.5],
[0.75, 0.7],
[0.6, 0.85],
[0.9, 0.4]
];
for (const [fx, fy] of offRowClicks) {
await page.mouse.click(box.x + box.width * fx, box.y + box.height * fy);
await page.waitForTimeout(150);
}
// Field still renders + still on the explore route after off-row picks.
const after = await sampleCanvas(page);
expect(after.rendered, 'explore field still renders after off-row clicks + hover').toBe(true);
expect(page.url(), 'off-row clicks must NOT navigate away').toContain('/explore');
expectNoErrors(errors);
// A targeted row click navigates to the memory detail page (goto) — the valid
// non-crash outcome for hitting a real neighbor row. Rows anchor at logical
// NDC x=-0.88, which the pickAt aspect-divide (/max(aspect,1)) pulls inward:
// on a 16:9 canvas (aspect≈1.78) the left column lands at fx≈0.260.32, and
// the top row (ndcY≈0.72) sits at fy≈0.14. Verified live with a scan probe.
// Click a few points across that band to reliably land on a row.
const rowClicks = [
[0.28, 0.14],
[0.3, 0.14],
[0.26, 0.16],
[0.32, 0.15],
[0.28, 0.18]
];
let navigated = false;
for (const [fx, fy] of rowClicks) {
await page.mouse.click(box.x + box.width * fx, box.y + box.height * fy);
await page.waitForTimeout(400);
if (/\/memories\/.+/.test(page.url())) {
navigated = true;
break;
}
}
expect(
navigated,
`a left-column row click should navigate to a memory detail page (url=${page.url()})`
).toBe(true);
// Navigation itself must be crash-free too.
expectNoErrors(errors);
});
test('explore shows an HONEST empty-state invite with no ?q= (never fake data)', async ({ page }) => {
// With no ?q=, loadNeighbors() short-circuits (no request) and renders the calm
// "TYPE A QUERY" invite — NOT a black surface, NOT fake rows, NOT an error.
const errors = captureErrors(page);
await gotoRoute(page, '/explore');
await page.waitForTimeout(2500);
// The canvas still renders (the recall-path scene + the status line are lit),
// so the empty state is a calm honest surface, not a broken black frame.
const sample = await sampleCanvas(page);
expect(
sample.rendered,
`empty-state invite must render a calm non-black surface (avgLum=${sample.avgLum} var=${sample.variance})`
).toBe(true);
// No neighbor request should have fired for an empty query (the guard holds).
expect(page.url(), 'must stay on /explore with no query').toContain('/explore');
expectNoErrors(errors);
});