import { expandApplicationHtml } from '../build/application-html.js'; import { readStylesheet } from './testSupport/readStylesheet.mjs'; import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js'; import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import crypto from 'node:crypto'; import { ENVIRONMENTAL_LABEL_CHOICE, EXCLUSIVE_SURFACE_CLASSES, FIRST_RUN_MISSIONS, FIRST_RUN_SESSION_KEY, FIRST_RUN_STORAGE_KEY, environmentalLabel, exclusiveSurfaceActive, rememberFirstRunSessionDismissed, runFirstRunChoice, setFirstRunSuppressed, shouldShowFirstRun, } from './firstRunExperience.js'; function memoryStorage(key, value = null) { const values = new Map(value == null ? [] : [[key, value]]); return { getItem: (name) => values.get(name) ?? null, setItem: (name, next) => values.set(name, next), removeItem: (name) => values.delete(name), read: () => values.get(key) ?? null, }; } const fresh = () => ({ storage: memoryStorage(FIRST_RUN_STORAGE_KEY), sessionStorageRef: memoryStorage(FIRST_RUN_SESSION_KEY), location: { search: '' }, }); // ── Show policy ────────────────────────────────────────────────────────────── test('a fresh session receives the launcher, and keeps receiving it', () => { assert.equal(shouldShowFirstRun(fresh()), true); // Not one-shot: a previous session's completion does not suppress a new one. const returning = fresh(); returning.sessionStorageRef = memoryStorage(FIRST_RUN_SESSION_KEY); assert.equal(shouldShowFirstRun(returning), true); }); test('dismissal is session-scoped; only the checkbox suppresses durably', () => { const session = memoryStorage(FIRST_RUN_SESSION_KEY); const storage = memoryStorage(FIRST_RUN_STORAGE_KEY); rememberFirstRunSessionDismissed(session); assert.equal(session.read(), 'dismissed'); // Gone for THIS session... assert.equal( shouldShowFirstRun({ storage, sessionStorageRef: session, location: { search: '' }, }), false, ); // ...and back in the next one, because sessionStorage did not survive it. assert.equal( shouldShowFirstRun({ storage, sessionStorageRef: memoryStorage(FIRST_RUN_SESSION_KEY), location: { search: '' }, }), true, ); // Session dismissal must never have written the durable key. assert.equal(storage.read(), null); }); test('the checkbox writes and clears durable suppression, and a storage reset undoes it', () => { const storage = memoryStorage(FIRST_RUN_STORAGE_KEY); setFirstRunSuppressed(true, storage); assert.equal(storage.read(), 'suppressed'); assert.equal( shouldShowFirstRun({ storage, sessionStorageRef: memoryStorage(FIRST_RUN_SESSION_KEY), location: { search: '' }, }), false, ); // Unticking before dismissing takes the suppression back. setFirstRunSuppressed(false, storage); assert.equal(storage.read(), null); assert.equal( shouldShowFirstRun({ storage, sessionStorageRef: memoryStorage(FIRST_RUN_SESSION_KEY), location: { search: '' }, }), true, ); // A cleared/hard-reset profile shows it again — an accepted, documented cost. setFirstRunSuppressed(true, storage); assert.equal( shouldShowFirstRun({ storage: memoryStorage(FIRST_RUN_STORAGE_KEY), sessionStorageRef: memoryStorage(FIRST_RUN_SESSION_KEY), location: { search: '' }, }), true, ); }); test('welcome params work in both directions and outrank both suppressions', () => { const suppressed = memoryStorage(FIRST_RUN_STORAGE_KEY, 'suppressed'); const dismissed = memoryStorage(FIRST_RUN_SESSION_KEY, 'dismissed'); // ?welcome=1 replays past the checkbox AND past a session dismissal. assert.equal( shouldShowFirstRun({ storage: suppressed, sessionStorageRef: dismissed, location: { search: '?welcome=1' }, }), true, ); // ?welcome=0 suppresses a session that would otherwise see it. assert.equal( shouldShowFirstRun({ ...fresh(), location: { search: '?welcome=0' } }), false, ); // A share link outranks everything, including the replay hatch. assert.equal( shouldShowFirstRun({ hasShareState: true, ...fresh(), location: { search: '?welcome=1' }, }), false, ); }); test('a share link never sees the launcher — its author already chose the view', () => { assert.equal(shouldShowFirstRun({ hasShareState: true, ...fresh() }), false); }); test('privacy-restricted storage fails open and every write stays best-effort', () => { const blocked = { getItem: () => { throw new Error('blocked'); }, setItem: () => { throw new Error('blocked'); }, removeItem: () => { throw new Error('blocked'); }, }; assert.equal( shouldShowFirstRun({ storage: blocked, sessionStorageRef: blocked }), true, ); assert.doesNotThrow(() => setFirstRunSuppressed(true, blocked)); assert.doesNotThrow(() => rememberFirstRunSessionDismissed(blocked)); }); test('a THROWING storage getter still fails open — Safari private mode', () => { // The harder case, and the one a default parameter cannot survive: it is not // getItem that throws, it is reading `globalThis.localStorage` AT ALL. A // default like `storage = globalThis.localStorage` evaluates that getter // before the function body starts, so the SecurityError escapes every // try/catch in the module and the launcher silently never appears. const saved = Object.getOwnPropertyDescriptor(globalThis, 'localStorage'); const savedSession = Object.getOwnPropertyDescriptor( globalThis, 'sessionStorage', ); const hostile = { configurable: true, get() { throw new Error('SecurityError: The operation is insecure.'); }, }; Object.defineProperty(globalThis, 'localStorage', hostile); Object.defineProperty(globalThis, 'sessionStorage', hostile); try { // No storage arguments at all: this is exactly how the app calls it. assert.doesNotThrow( () => shouldShowFirstRun({ location: { search: '' } }), 'a hostile storage getter must not escape shouldShowFirstRun', ); assert.equal( shouldShowFirstRun({ location: { search: '' } }), true, 'a visitor whose storage throws must still SEE the launcher', ); assert.doesNotThrow(() => setFirstRunSuppressed(true)); assert.doesNotThrow(() => setFirstRunSuppressed(false)); assert.doesNotThrow(() => rememberFirstRunSessionDismissed()); } finally { if (saved) Object.defineProperty(globalThis, 'localStorage', saved); else delete globalThis.localStorage; if (savedSession) Object.defineProperty(globalThis, 'sessionStorage', savedSession); else delete globalThis.sessionStorage; } }); test('no storage is touched from a default parameter position', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); // Comments stripped first: the block explaining this very defect quotes the // bad pattern, and matching prose instead of code would make the pin a liar. const code = module .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/^\s*\/\/.*$/gm, ''); assert.doesNotMatch( code, /=\s*globalThis\.(local|session)Storage/, 'storage must be resolved lazily inside a try, never as a default parameter', ); // The only reads of the global areas live inside the guarded resolver. const globalReads = [...code.matchAll(/globalThis\.(local|session)Storage/g)]; assert.equal( globalReads.length, 2, 'exactly two global storage reads, both in resolveStore', ); const resolver = code.slice( code.indexOf('function resolveStore'), code.indexOf('function readStored'), ); assert.equal( [...resolver.matchAll(/globalThis\.(local|session)Storage/g)].length, 2, 'both global storage reads must be inside resolveStore, inside its try', ); assert.match( resolver, /try \{[\s\S]*globalThis\.sessionStorage[\s\S]*\} catch/, ); assert.match(code, /function readStored\(kind, injected, key\)/); assert.match(code, /function writeStored\(kind, injected, key, value\)/); }); // ── ESC arbitration: one surface, one key, never an invisible handler ──────── test('the JS and CSS lists of screen-claiming surfaces stay in step', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); const css = readStylesheet(new URL('../style.css', import.meta.url)); assert.deepEqual([...EXCLUSIVE_SURFACE_CLASSES].sort(), [ 'cockpit-mode', 'recording-mode', 'scene-playback-mode', 'ui-clean-view', ]); // A surface that hides the card in CSS but is missing from the JS list would // leave an invisible launcher holding the ESC handler — blocker 2 exactly. const hideRule = css.slice( css.indexOf('body.ui-clean-view #first-run-launcher'), css.indexOf( 'display: none', css.indexOf('body.ui-clean-view #first-run-launcher'), ), ); const inCss = [ ...hideRule.matchAll(/body\.([a-z-]+) #first-run-launcher/g), ].map((m) => m[1]); assert.deepEqual( inCss.sort(), [...EXCLUSIVE_SURFACE_CLASSES].sort(), 'every screen-claiming surface must appear in BOTH lists', ); assert.match( module, /export const EXCLUSIVE_SURFACE_CLASSES = Object\.freeze\(\[/, ); }); test('exclusiveSurfaceActive reads the live body classes', () => { const make = (classes) => ({ body: { classList: { contains: (name) => classes.includes(name) } }, }); assert.equal(exclusiveSurfaceActive(make([])), false); for (const name of EXCLUSIVE_SURFACE_CLASSES) { assert.equal( exclusiveSurfaceActive(make([name])), true, `${name} must count as exclusive`, ); } assert.equal(exclusiveSurfaceActive(make(['some-other-class'])), false); assert.equal(exclusiveSurfaceActive(undefined), false); assert.equal(exclusiveSurfaceActive({}), false); }); test('the key handler refuses to act for a card that is not really on screen', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); assert.match(module, /isActive: \(\) => !closing && isTopmost\(\)/); // Real visibility, not just the class: the class survives while CSS hides the // card, which is precisely how a Scene left an invisible ESC handler armed. assert.match(module, /const isTopmost = \(\) =>\s*root\.isConnected/); assert.match( module, /&&\s*root\.getClientRects\(\)\.length > 0 &&\s*!coveredByOverlay\(\);/, ); const keyboard = fs.readFileSync( new URL('./ui/surfaceKeyboard.js', import.meta.url), 'utf8', ); const handler = keyboard.slice( keyboard.indexOf('const onKeyDown = (event) => {'), ); assert.match( handler.slice(0, handler.indexOf("if (event.key === 'Escape')")), /!isActive\(\)/, 'the handler must bail before consuming anything when it is not topmost', ); }); test('an overlay with NO class to watch still disarms the launcher', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); const css = readStylesheet(new URL('../style.css', import.meta.url)); // The repro, pinned as the stacking it actually is: the attribution lightbox // is full-screen ABOVE the card and announces itself with nothing. The card // keeps its box, so getClientRects() alone called it visible, ESC dismissed a // buried launcher and wrote the session flag while the lightbox stayed open. const overlay = css.slice(css.indexOf('.cesium-credit-lightbox-overlay {')); assert.match( overlay.slice(0, overlay.indexOf('}')), /z-index: 200 !important/, ); const launcher = css.slice(css.indexOf('#first-run-launcher {')); assert.match(launcher.slice(0, launcher.indexOf('}')), /z-index: 175/); // Answered generically — a hit test at the card's own centre, NOT one more // class to keep in step with one more overlay. const covered = module.slice( module.indexOf('const coveredByOverlay = () => {'), module.indexOf('const isTopmost = ()'), ); assert.ok(covered.length > 0, 'coveredByOverlay must exist'); assert.match(covered, /rect\.left \+ rect\.width \/ 2/); assert.match(covered, /rect\.top \+ rect\.height \/ 2/); assert.match(covered, /return Boolean\(hit\) && !root\.contains\(hit\);/); // ...and every inconclusive answer is UNCOVERED, so a guard added to stop the // launcher acting under an overlay can never become why ESC stopped working. assert.match( covered, /if \(typeof documentRef\.elementFromPoint !== 'function'\) return false;/, ); assert.match( covered, /if \(!\(rect\.width > 0 && rect\.height > 0\)\) return false;/, ); assert.match(covered, /\} catch \{\s*\n\s*return false;\s*\n\s*\}/); }); test('one ESC does one thing — the radio disclosure stops the launcher outright', () => { const ui = fs.readFileSync( new URL('./ui/radioBindings.js', import.meta.url), 'utf8', ); const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); // stopPropagation() does NOT stop later listeners on the SAME document, so the // disclosure's earlier capture handler closed the disclosure and the launcher // dismissed itself off the same key. The earlier listener is the only one that // can stop the later one — and only the immediate form does it. const radioEsc = ui.slice(ui.indexOf("event.key !== 'Escape' ||")); const claim = radioEsc.slice(0, radioEsc.indexOf('setRadioDisclosure(false')); assert.match(claim, /event\.preventDefault\(\);/); assert.match(claim, /event\.stopImmediatePropagation\(\);/); assert.doesNotMatch( claim, /event\.stopPropagation\(\);/, 'the plain form leaves the launcher listening and is the defect itself', ); // Belt on the launcher side: a key another surface already marked is not ours, // whether or not that surface remembered to silence us. const keyboard = fs.readFileSync( new URL('./ui/surfaceKeyboard.js', import.meta.url), 'utf8', ); const handler = keyboard.slice( keyboard.indexOf('const onKeyDown = (event) => {'), ); assert.match( handler.slice(0, handler.indexOf("if (event.key === 'Escape')")), /event\.defaultPrevented\) return;/, "a marked key must be somebody else's key", ); }); test('a refused write takes the tick back instead of promising "never again"', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); // The write stays best-effort; the OUTCOME is now reported, because a box left // ticked after a refused write tells the visitor the launcher is gone for good // while it is already guaranteed to return next session. const blocked = { getItem: () => null, setItem: () => { throw new Error('blocked'); }, removeItem: () => { throw new Error('blocked'); }, }; assert.equal(setFirstRunSuppressed(true, blocked), false); assert.equal(setFirstRunSuppressed(false, blocked), false); // No storage area at all is a refusal too — nothing was persisted either way. assert.equal(setFirstRunSuppressed(true, null), false); assert.equal(setFirstRunSuppressed(false, null), false); // ...and a working store still reports success, or the checkbox would revert // on every tick and the pin above would be measuring nothing. const working = memoryStorage(FIRST_RUN_STORAGE_KEY); assert.equal(setFirstRunSuppressed(true, working), true); assert.equal(working.read(), 'suppressed'); assert.equal(setFirstRunSuppressed(false, working), true); assert.equal(working.read(), null); const handler = module.slice( module.indexOf('const onSuppressChange = (event) => {'), module.indexOf(' const keyboard = createSurfaceKeyboard({'), ); assert.match( handler, /if \(setFirstRunSuppressed\(wanted, storage\)\) return;/, ); assert.match( handler, /box\.checked = !wanted;/, 'a refused write must revert the tick', ); }); test('a surface class that never clears is an ACCEPTED no-show, not a timer', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); const state = fs.readFileSync( new URL('../docs/CURRENT-STATE.md', import.meta.url), 'utf8', ); // A "reveal anyway after N seconds" would trade a benign no-show for the card // punching through a recording in progress — recordings run long, and none of // the four classes is restored at startup, so a blocked init is an error path. // The slice deliberately spans the note AND the function it governs: a pin // that stopped at the comment would let a timer be added one line below the // paragraph saying there isn't one. const accepted = module.slice( module.indexOf('ACCEPTED, DELIBERATELY NOT TIMED OUT'), module.indexOf(' const onViewportResize'), ); assert.ok( accepted.length > 0, 'the acceptance must be written where the next editor will read it', ); assert.match( accepted, /const syncToExclusiveSurfaces = \(\) => \{/, 'the pin must cover the function the note governs, not just the note', ); assert.doesNotMatch( accepted, /setTimeout|setInterval/, 'the acceptance is the decision NOT to time this out', ); assert.match(accepted, /docs\/CURRENT-STATE\.md/); assert.match( state, /a surface class that never clears means no launcher for that page/i, ); }); test('the scroll fade only appears when the list really overflows', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); const css = readStylesheet(new URL('../style.css', import.meta.url)); // A fade on a card where all five tiles fit promises a sixth mission that does // not exist, which is worse than no affordance at all. assert.match( module, /const overflows = choiceList\.scrollHeight > choiceList\.clientHeight \+ 1;/, ); assert.match( module, /choiceList\.dataset\.scrollable = String\(overflows\);/, ); // ...and the CSS must be gated on that flag, never on the media query alone. assert.match( css, /\.first-run-choices\[data-scrollable='true'\] \{[\s\S]*?mask-image/, ); const bareFade = css.match(/^\s*\.first-run-choices \{[\s\S]*?\n\}/m)?.[0] || ''; assert.doesNotMatch( bareFade, /mask-image/, 'the fade must never apply unconditionally', ); // Rotating a phone changes which tiles fit, so it re-measures. assert.match(module, /addEventListener\?\.\('resize', onViewportResize\)/); assert.match(module, /removeEventListener\?\.\('resize', onViewportResize\)/); }); test('the launcher yields on engage and waits when a surface is already up', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); // Both directions from one observer: yield if it is up and something takes // the screen; wait to reveal if something already has it. assert.match( module, /if \(revealed && blocked\) yieldToExclusiveSurface\(\);\s*\n\s*else if \(!revealed && !blocked\) reveal\(\);/, ); // Yielding is session-scoped and must not steal focus from the new surface. assert.match(module, /dismiss\(\{ restoreFocus: false \}\)/); // A cheap attribute watch, not a per-frame poll — the render governor must // not see a new hold because of onboarding chrome. assert.match(module, /attributes: true,\s*attributeFilter: \['class'\]/); assert.match(module, /surfaceObserver\?\.disconnect\(\)/); assert.doesNotMatch( module, /setInterval|requestAnimationFrame\(function poll/, ); }); // ── Per-mission behavior ───────────────────────────────────────────────────── function missionSpy({ contextOk = true, layerResult = () => true, globe = async () => ({ ok: true }), } = {}) { const calls = { contextModes: [], layerIds: [], globeFlights: 0 }; return { calls, deps: { setContextMode: async (mode) => { calls.contextModes.push(mode); return contextOk ? { ok: true, mode } : { ok: false, failedLayerIds: ['rocket-launches'] }; }, setLayerEnabled: async (layerId) => { calls.layerIds.push(layerId); return layerResult(layerId); }, flyToGlobe: async () => { calls.globeFlights += 1; return globe(); }, }, }; } test('the menu is the four owner-ordered missions', () => { // INFRASTRUCTURE was removed after the owner playtested it: enabling all // three bundled layers at once put ~5,700 entities on a full-earth view and // tanked the frame rate. The layers stay reachable by hand and by voice; what // went is the one-click globe-scale dump. Restoring the tile needs the // globe-LOD declutter first. assert.deepEqual(Object.keys(FIRST_RUN_MISSIONS), [ 'contacts', 'space-missions', 'environmental', 'explore', ]); assert.equal( FIRST_RUN_MISSIONS.infrastructure, undefined, 'the infrastructure mission must be gone, not dormant', ); }); test('Live Contacts and Space Missions go through the one setContextMode facade', async () => { for (const [choice, mode] of [ ['contacts', 'contacts'], ['space-missions', 'space-missions'], ]) { const spy = missionSpy(); const outcome = await runFirstRunChoice(choice, spy.deps); assert.equal(outcome.ok, true); assert.deepEqual(spy.calls.contextModes, [mode]); // A Context mission owns no layers and no camera of its own — the facade does. assert.deepEqual(spy.calls.layerIds, []); assert.equal(spy.calls.globeFlights, 0); } }); test('Environmental enables BOTH its feeds and pulls out to the globe', async () => { const spy = missionSpy(); const outcome = await runFirstRunChoice('environmental', spy.deps); assert.equal(outcome.ok, true); assert.deepEqual(spy.calls.layerIds, ['earthquakes', 'local-firms']); assert.equal(spy.calls.globeFlights, 1); }); test('the tile is the FULLY CONFIGURED experience: quakes and fires together', () => { // Owner ruling, 2026-08-23: the launcher optimizes for the configured app, so // ENVIRONMENTAL means live USGS earthquakes AND NASA FIRMS active fires. const environmental = FIRST_RUN_MISSIONS.environmental; assert.deepEqual(environmental.layerIds, ['earthquakes', 'local-firms']); // Keyless, the honest surface is the LAYER ROW ("KEY REQUIRED"), which the // FIRMS layer already reports. The misleading part is the GLOBAL chip folding // that row into LOAD FAILED — a defect in the shared state machine, ledgered // post-launch, and the note must stay where the next editor will read it // rather than being re-discovered as a launcher bug. const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); const table = module.slice( module.indexOf(' environmental: Object.freeze({'), module.indexOf(' explore:'), ); assert.match(table, /KEY REQUIRED/); assert.match(table, /src\/loadingFeedback\.js/); assert.match(table, /LEDGERED post-launch/); }); test('every visitor gets the same tile — there is no degraded keyless variant', async () => { // The mission does not branch on configuration: it asks for both layers for // everyone, and a keyless FIRMS reports its own state at its own row rather // than changing what the tile does. const spy = missionSpy({ layerResult: () => true }); const outcome = await runFirstRunChoice('environmental', spy.deps); assert.equal(outcome.ok, true); assert.deepEqual(outcome.failedLayerIds, []); assert.deepEqual(spy.calls.layerIds, ['earthquakes', 'local-firms']); const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); assert.doesNotMatch( module.slice(module.indexOf('export async function runFirstRunChoice')), /FIRMS_MAP_KEY|hasKey|keyless\s*\?/, 'the mission must not fork on whether a key is configured', ); }); test('a refused layer fails the mission by name, and a stalled flight never does', async () => { const refused = missionSpy({ layerResult: (id) => id !== 'earthquakes' }); const outcome = await runFirstRunChoice('environmental', refused.deps); assert.equal(outcome.ok, false); assert.deepEqual(outcome.failedLayerIds, ['earthquakes']); // The globe flight is framing. A cancelled or throwing flight is not a failure. const flightDown = missionSpy({ globe: () => { throw new Error('cancelled'); }, }); assert.equal( (await runFirstRunChoice('environmental', flightDown.deps)).ok, true, ); }); test('Explore manually touches nothing at all, and an unknown choice is inert', async () => { const spy = missionSpy(); assert.equal((await runFirstRunChoice('explore', spy.deps)).ok, true); assert.deepEqual(spy.calls, { contextModes: [], layerIds: [], globeFlights: 0, }); assert.equal((await runFirstRunChoice('nope', spy.deps)).ok, false); assert.deepEqual(spy.calls, { contextModes: [], layerIds: [], globeFlights: 0, }); }); test('a failed Context mission reports the layers the facade named', async () => { const spy = missionSpy({ contextOk: false }); const outcome = await runFirstRunChoice('space-missions', spy.deps); assert.equal(outcome.ok, false); assert.deepEqual(outcome.result.failedLayerIds, ['rocket-launches']); }); test('the fires/quakes tile name is switchable from one constant', () => { assert.equal(environmentalLabel('ENVIRONMENTAL').title, 'ENVIRONMENTAL'); assert.equal(environmentalLabel('EARTH_WATCH').title, 'EARTH WATCH'); assert.equal(environmentalLabel('ACTIVE_EVENTS').title, 'ACTIVE EVENTS'); assert.equal(environmentalLabel('nonsense').title, 'ENVIRONMENTAL'); assert.equal( environmentalLabel().title, environmentalLabel(ENVIRONMENTAL_LABEL_CHOICE).title, ); }); // ── Defaults interplay: what a mission is allowed to persist ───────────────── test('no mission writes a preference the visitor did not choose by picking it', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); const code = module.slice( module.indexOf('export function shouldShowFirstRun'), ); // Layer enables ARE durable in this app and a mission tile IS that choice, so // they run at the same origin a click on those rows uses. assert.match(code, /setEnabled\(layerId, true, \{ origin: 'user' \}\)/); // Detection is owned by the reasonable-defaults landing and, while Contacts is // active, by contactsDetectionPolicy. A mission has no opinion on any of it. for (const forbidden of [ '_detectionUserOverridden', '_setDetectionMode', '_applyDetectionPreset', '_setDetectionAllocation', 'setDetectionTuning', // 3D models and feather default to origin 'user' and would persist a choice // nobody made by picking a mission. '_setModels3dEnabled', '_setModels3dMode', '_setModels3dParams', 'setFeather', ]) { assert.doesNotMatch( code, new RegExp(forbidden), `a mission must never touch ${forbidden}`, ); } // The only durable panel write is the Context reveal, and only on the Context // missions — the globe missions open no panel at all. const panelWrites = code.match(/setPanelCollapsed/g) || []; assert.equal( panelWrites.length, 1, 'exactly one panel reveal, on the Context path', ); const contextPath = code.slice( code.indexOf('setContextMode: async (mode)'), code.indexOf('setLayerEnabled:'), ); assert.match( contextPath, /result\?\.ok[\s\S]*?setPanelCollapsed\?\.\('global-context-panel', false, \{\s*explicit: true,?\s*\}\)/, ); }); test('the decision table is written down where the next editor will read it', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); assert.match( module, /MISSION → APP STATE, AND WHAT IT IS ALLOWED TO PERSIST/, ); for (const row of ['TOUCHED, DURABLE', 'TOUCHED, SESSION', 'NOT TOUCHED']) { assert.ok( module.includes(row), `decision table is missing its "${row}" rows`, ); } assert.match(module, /SHOW POLICY/); }); // ── Markup, startup ordering, accessibility ───────────────────────────────── test('markup, startup ordering and accessibility remain pinned', () => { const html = expandApplicationHtml( fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'), ); const startup = fs.readFileSync( new URL('./app/startupChrome.js', import.meta.url), 'utf8', ); const css = readStylesheet(new URL('../style.css', import.meta.url)); assert.match( html, /id="first-run-launcher" role="dialog"[^>]*aria-labelledby="first-run-title"[^>]*hidden/, ); assert.equal((html.match(/data-first-run-choice=/g) || []).length, 4); assert.match( html, /data-first-run-status[^>]*role="status"[^>]*aria-live="polite"/, ); assert.match(html, //); assert.match(html, //); // Subcopy must name BOTH feeds the tile turns on — a tile that promised only // half of what it does is the defect this replaced. Only the VISIBLE // text counts; the comment beside it naturally says the words too. const envTile = html.slice( html.indexOf('data-first-run-choice="environmental"'), ); const visible = envTile.slice( envTile.indexOf(''), envTile.indexOf(''), ); assert.match(visible, /earthquakes/i); assert.match( visible, /fires?/i, 'the tile must promise the fires it enables', ); // The card's one persuasive line is OWNER-AUTHORED and pinned verbatim, // unspaced em dash included. This is copy, not prose to be improved in a // passing edit — changing it needs the owner, not a nicer-sounding rewrite. assert.ok( html.includes( '

It feels like a forbidden cockpit' + '—then you realize the sources are public and the data is real.

', ), 'the owner-authored first-run line must ship exactly as written', ); // Menu order is the owner's, read straight off the markup. const order = [...html.matchAll(/data-first-run-choice="([a-z-]+)"/g)].map( (match) => match[1], ); assert.deepEqual(order, [ 'contacts', 'space-missions', 'environmental', 'explore', ]); assert.doesNotMatch( html, /data-first-run-choice="infrastructure"/, 'the removed tile must leave no markup behind', ); assert.match(startup, /styleManager\.initialRestorePromise/); assert.ok( startup.indexOf("loadingScreen.classList.add('hidden')") < startup.indexOf( "loadingScreen.addEventListener('transitionend', revealFirstRun", ), ); assert.match( startup, /initializeWelcome\?\.\(\{ styleManager, dataManager \}\)/, ); assert.match(css, /body\.ui-clean-view #first-run-launcher/); assert.match(css, /body\.recording-mode #first-run-launcher/); // Scoped, not a bare search: style.css has other reduced-motion blocks, and // matching one of THOSE would let the launcher's own opt-out be deleted. const reducedMotion = [ ...css.matchAll( /@media \(prefers-reduced-motion: reduce\) \{([\s\S]*?)\n\}/g, ), ].map((match) => match[1]); assert.ok( reducedMotion.some( (block) => block.includes('#first-run-launcher') && block.includes('.first-run-choices'), ), 'the launcher must keep its OWN prefers-reduced-motion block, covering the card and the scrolling list', ); // The card must be click-through until revealed and again while it fades out. const base = css.slice( css.indexOf('#first-run-launcher {'), css.indexOf('#first-run-launcher.visible'), ); assert.match(base, /pointer-events: none/); assert.match( css, /#first-run-launcher\.visible \{[\s\S]*?pointer-events: auto/, ); // The card is a flex column so its mission list can scroll on a short // viewport — and an AUTHOR `display` on this id outranks the UA's // `[hidden] { display: none }`, which would strand the card in the // accessibility tree until it is revealed or removed. assert.match(base, /display: flex/); assert.match(base, /max-height: calc\(100dvh/); assert.match(css, /#first-run-launcher\[hidden\] \{\s*display: none;\s*\}/); // Only the mission list may scroll: the heading, checkbox and status line // have to stay on screen at every height. const choicesBlock = css.match(/\.first-run-choices \{([^}]*)\}/)?.[1] || ''; assert.match(choicesBlock, /min-height: 0;/); assert.match(choicesBlock, /overflow-y: auto;/); }); test('the launcher keeps focus, restores it, and never disables the focused button', () => { const module = fs.readFileSync( new URL('./firstRunExperience.js', import.meta.url), 'utf8', ); // aria-disabled, never the `disabled` property: disabling a focused button // drops the keyboard to and strands the visitor outside the launcher. assert.match( module, /button\.setAttribute\('aria-disabled', String\(next\)\)/, ); assert.doesNotMatch(module, /button\.disabled = /); // Tab is confined to the launcher, and ESC always releases it. const keyboard = fs.readFileSync( new URL('./ui/surfaceKeyboard.js', import.meta.url), 'utf8', ); assert.match(module, /keyboard\.activate\(\)/); assert.match(module, /keyboard\.deactivate\(\{ restoreFocus \}\)/); assert.match(keyboard, /event\.key !== 'Tab'/); assert.match(keyboard, /event\.key === 'Escape'/); assert.match(keyboard, /target\?\.focus/); // Capture phase, so the app's global letter hotkeys cannot eat the launcher's keys. assert.match(keyboard, /addEventListener\('keydown', onKeyDown, true\)/); }); test('the DISPLAY rail starts collapsed on a first run, and a stored choice wins', () => { const ui = fs.readFileSync( new URL('./ui/panelPositionControls.js', import.meta.url), 'utf8', ); // The rail opened by default to advertise HUD / DETECT / 3D. Those default ON // now, so it was opening to offer controls for things already happening — // while competing with the mission card for the one first impression there is. assert.match( ui, /if \(panelId === 'pp-toggles' && stored === null\) collapsed = true;/, 'first run must leave DISPLAY collapsed', ); // The first-run default may only apply when NOTHING is stored: a visitor who // opened the rail keeps it open, and one who closed it keeps it closed. const block = ui.slice( ui.indexOf( 'stored = localStorage.getItem(this._panelCollapseStorageKey(panelId))', ), ); const guard = block.slice(0, block.indexOf('panelEl.classList.toggle')); assert.match(guard, /if \(stored === '1'\) collapsed = true;/); assert.match(guard, /if \(stored === '0'\) collapsed = false;/); assert.ok( guard.indexOf("stored === '0'") < guard.indexOf('pp-toggles'), 'the stored-state reads must come before the first-run default', ); }); // ── Voice: instruction-only, tool schema unchanged ───────────────────── test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping is instructions only', () => { // Analyst layers and the separate satellite-pass tool deliberately extend the schema. // Canonical serialization pins every tool name, description, property and // ordering while allowing source formatting. Derived from the unchanged // release schema before formatting (the previous source-byte pin passed). const legacyTools = structuredClone(GEV_REALTIME_TOOLS).filter( (tool) => tool.name !== 'set_cyber_sonar', ); const hudLayout = legacyTools.find((tool) => tool.name === 'set_hud') .parameters.properties.layout; assert.deepEqual(hudLayout.enum, [ 'tactical', 'operator', 'minimal', 'cyber', ]); // Cyber deliberately adds one layout; first-run missions still change no tools. hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber'); const block = JSON.stringify(legacyTools); // Re-derived for the voice layer manifest (generated layer enums, alias and // field hints), point-and-ask's pointer/referent arguments and the prompt // consolidation (shorter analyst, annotate_map and ISS wording), plus the // referent-only track_entity alternative (kept out of the model-facing // schema, which may not carry top-level anyOf), and Contacts requested-radius // list descriptions; the missions still ride existing tools. // Street Level and GNSS extend the manifest; broadcast caveats describe tracked and analyst aircraft. assert.equal(block.length, 30511, 'serialized tool schema length drifted'); assert.equal( crypto.createHash('sha256').update(block).digest('hex'), '62d5df9cedf413403e4fd0a7cbe94bb2cd13736c3d8fedc84bcccd63ad253eb6', 'the first-run missions must ride EXISTING tools: no schema edit, no cache bust', ); const instructions = fs.readFileSync( new URL('../server/providers/openai/instructions.js', import.meta.url), 'utf8', ); // ...and the mapping that makes them reachable by voice is one instruction // string, whose rollback is deleting that string. Anchored to a LIVE array // entry — a quote at the start of its own line — so commenting the paragraph // out reads as the removal it is, not as a passing substring match. assert.match( instructions, /\n\s+'NAMED VIEWS are shorthand/, 'the mission mapping must be an active instruction entry, not commented out', ); const mapping = instructions.slice( instructions.indexOf('NAMED VIEWS are shorthand'), ); const paragraph = mapping.slice(0, mapping.indexOf("',\n")); for (const layerId of [ 'local-datacenters', 'local-dams', 'telegeography-submarine-cables', 'local-firms', 'earthquakes', ]) { assert.ok( paragraph.includes(layerId), `mapping must name the existing ${layerId} enum value`, ); } assert.ok(paragraph.includes('zoom_to_globe')); assert.ok(paragraph.includes('set_layer_visibility')); }); test('every layer a mission drives is already in the shipped set_layer_visibility enum', () => { const tool = GEV_REALTIME_TOOLS.find( (tool) => tool.name === 'set_layer_visibility', ); const allowedLayers = tool.parameters.properties.layerId.enum; const missionLayerIds = Object.values(FIRST_RUN_MISSIONS).flatMap( (mission) => mission.layerIds || [], ); assert.ok(missionLayerIds.length > 0); for (const layerId of missionLayerIds) { assert.ok( allowedLayers.includes(layerId), `${layerId} must already be an allowed enum value`, ); } });