// Tests for the boot scene's pure timing/geometry (the light's band math, the // quote fade envelope, the staged cover's dwell guarantees). node test/bootscene.test.mjs import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { BANDS, bandLight, quoteOpacity, finishWait, coverWait, validQuoteLib } from "../web/static/bootscene.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } const sum = (l) => l[0] + l[1] + l[2] + l[3]; test("bands cover the road in order: green, amber, red, eleven", () => { assert.equal(BANDS.length, 5); assert.equal(BANDS[0], 0); assert.equal(BANDS[4], 1); for (let i = 1; i < BANDS.length; i++) assert.ok(BANDS[i] > BANDS[i - 1]); }); test("the soft frontier leads the level by a bounded margin", () => { // The light fades out ~0.14 past the frontier instead of ending at a hard // line. At level 0 that lead may kiss the very bottom of the road, but // beyond it there must be nothing. for (const u of [0.15, 0.2, 0.5, 0.9]) assert.equal(sum(bandLight(u, 0, 0)), 0); }); test("the frontier gates the climb: light below it, none above", () => { const l = 3; // frontier at 3/11 assert.ok(bandLight(0.1, l, 0)[0] > 0, "green near the source"); assert.equal(sum(bandLight(0.5, l, 0)), 0, "dark past the frontier"); }); test("the zen climb can never reach the red", () => { // The climb asymptotes at level 6.2 (frontier 0.564 + soft lead 0.14 → // light tops out below u=0.71): the frontier's bleed may warm the bottom // of the amber, but red (from 9/11 = 0.818) only ever arrives with the // punch. for (const u of [0.71, 0.8, 0.9, 0.95]) { assert.equal(sum(bandLight(u, 6.2, 0)), 0, `light found at u=${u} during climb`); } }); test("colour belongs to height, not to level: green stays green at ten", () => { const low = bandLight(0.2, 10, 0); assert.ok(low[0] > 0, "the bottom is still green at level ten"); assert.equal(low[1] + low[2] + low[3], 0, "no amber/red bleeding into the green band"); const mid = bandLight(0.7, 10, 0); assert.ok(mid[1] > 0, "amber lives above the green"); const high = bandLight(0.88, 10, 0); assert.ok(high[2] > 0, "red above the amber"); }); test("the eleventh band lights only with elevenGlow", () => { const u = 0.96; // inside the eleven band assert.equal(bandLight(u, 10, 0)[3], 0, "dark before the punch lands"); assert.ok(bandLight(u, 10, 1)[3] > 0, "lit at eleven"); }); test("light is hottest near the unseen source", () => { const near = sum(bandLight(0.05, 10, 0)); const far = sum(bandLight(0.5, 10, 0)); assert.ok(near > far, "intensity must fall with distance up the road"); }); test("quote envelope: fade in, hold, fade out", () => { const F = 2200, H = 7600; assert.equal(quoteOpacity(0, F, H), 0); assert.equal(quoteOpacity(F / 2, F, H), 0.5); assert.equal(quoteOpacity(F, F, H), 1); assert.equal(quoteOpacity(F + H / 2, F, H), 1); assert.ok(Math.abs(quoteOpacity(F + H + F / 2, F, H) - 0.5) < 1e-9); assert.equal(quoteOpacity(F + H + F, F, H), 0); assert.equal(quoteOpacity(F + H + F + 500, F, H), 0, "stays 0 past the cycle"); assert.equal(quoteOpacity(-100, F, H), 0, "never negative-age light"); }); test("reading-time guarantee: a fresh quote delays the punch, an old one doesn't", () => { assert.equal(finishWait(null, 4500), 0, "no quote on stage → punch immediately"); assert.equal(finishWait(0, 4500), 4500, "a quote that JUST appeared gets its full read"); assert.equal(finishWait(1000, 4500), 3500); assert.equal(finishWait(4500, 4500), 0); assert.equal(finishWait(60000, 4500), 0, "a long-displayed quote owes nothing"); }); test("staged cover: nothing assembled → no dwell owed", () => { assert.equal(coverWait(null, null), 0, "a fast boot fades before any stage enters"); }); test("staged cover: an entered brand gets its fade plus a beat", () => { assert.equal(coverWait(0, null), 850, "just entered: full fade (350) + dwell (500)"); assert.equal(coverWait(600, null), 250); assert.equal(coverWait(850, null), 0, "dwell spent — exactly when the quote's stage may open"); assert.equal(coverWait(60000, null), 0); }); test("staged cover: an entered quote gets its fade plus a read, subsuming the brand", () => { assert.equal(coverWait(9999, 0), 3200, "just dealt: full fade (2200) + read (1000)"); assert.equal(coverWait(9999, 2200), 1000, "fully faded in — the read is still owed"); assert.equal(coverWait(9999, 3200), 0, "read spent — exactly when the smoke's stage may open"); assert.equal(coverWait(null, 0), 3200, "the quote's guarantee stands on its own"); }); test("validQuoteLib accepts only a real library", () => { const good = Array.from({ length: 25 }, (_, i) => [`quote ${i}`, `who ${i}`]); assert.ok(validQuoteLib(good)); assert.ok(!validQuoteLib(null), "null"); assert.ok(!validQuoteLib("[]"), "a string"); assert.ok(!validQuoteLib([]), "empty"); assert.ok(!validQuoteLib(good.slice(0, 10)), "too small to be the library"); assert.ok(!validQuoteLib(Array.from({ length: 25 }, () => ["text only"])), "pairs missing an attribution"); assert.ok(!validQuoteLib(Array.from({ length: 25 }, () => ["", "who"])), "empty quote text"); }); test("the shipped library is valid, generous, and deduped", () => { const lib = JSON.parse(readFileSync(new URL("../web/static/quotes.json", import.meta.url), "utf8")); assert.ok(validQuoteLib(lib), "shape: an array of [text, attribution] pairs"); assert.ok(lib.length >= 300, `to-a-human enormous: only ${lib.length} quotes`); const texts = new Set(lib.map((q) => q[0])); assert.equal(texts.size, lib.length, "no duplicate quote texts"); for (const [text, attr] of lib) { assert.ok(text.length <= 240, `fits the card: "${text.slice(0, 50)}…" is ${text.length} chars`); assert.ok(attr.length <= 120, `attribution fits: "${attr.slice(0, 50)}…"`); } }); process.exit(failures ? 1 : 0);