diff --git a/internal/check-poster-degrades-honestly.mjs b/internal/check-poster-degrades-honestly.mjs index 5fd08b5d0..a7c9c24a3 100644 --- a/internal/check-poster-degrades-honestly.mjs +++ b/internal/check-poster-degrades-honestly.mjs @@ -15,16 +15,18 @@ // the correct product behaviour and the one BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB // claims in prose. This turns that claim into an assertion. // -// ⚠ IT MUST BE RUN AGAINST THE TREE ROOT, NOT public/. A leaf resolves assetsBase -// to '../../assets', which from /// is /assets. Serving -// public/ instead puts public/assets on that path — a directory that still holds -// 378 posters in a dev checkout — and the poster then loads with a 200, so this gate -// would silently test the opposite of what it claims. Measured 2026-09-24: the same -// click returned 200 under `--directory public` and 404 under -// `--directory public/doxa-maps`. Serve the TREE. +// ★ THE CONDITION IS FORCED, NOT FOUND. This gate used to look for a tree that served +// neither file and SKIP (exit 0) on any other. Two changes made that tree unreachable: +// the served tree now SHIPS the poster sources (the prayer packet's map needs them), and +// a wall whose host passes `sheet` GENERATES the poster first from the served +// lib/poster-render.js, so "no stored file" no longer means "nothing to print". It +// skipped every time, and the skip read as a pass. So the browser is now told the +// truth of the worst case directly: every posters/*.webp and *.html answers 404, and +// the generator does not load. What the reader is then shown is what this asserts, on +// any tree, whichever directory is served. // -// Usage: node internal/check-poster-degrades-honestly.mjs [zoneId] -import { chromium } from './lib/playwright-guarded.mjs' +// Usage: node internal/check-poster-degrades-honestly.mjs +import { chromium, NOT_MEASURED } from './lib/playwright-guarded.mjs' import { requireThisTree } from './_serves-this-tree.mjs' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { fileURLToPath as _fileURLToPath } from 'node:url' @@ -39,25 +41,9 @@ const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') // A file:// target passes straight through — there is no server there to be wrong about. await requireThisTree(WALL, ROOT, 'POSTER-DEGRADES-HONESTLY') if (!WALL) { - console.log('POSTER-DEGRADES: SKIP — no wall url given.') - console.log(' Serve the TREE ROOT (public/), not public/, or assetsBase resolves') - console.log(' outside the tree and the poster loads when it should 404.') - process.exit(0) -} - -// The precondition is the INVERSE of the other gate's: there must be NO poster and -// NO source. If either is reachable this is the wrong tree to be testing. -const probeId = process.argv[3] || 'laos' -const srcUrl = new URL(`../../assets/posters/${probeId}.html`, WALL).href -const imgUrl = new URL(`../../assets/posters/${probeId}.webp`, WALL).href -let reachable = [] -for (const [name, u] of [['html', srcUrl], ['webp', imgUrl]]) { - try { const r = await fetch(u); if (r.ok) reachable.push(name) } catch { /* unreachable is the point */ } -} -if (reachable.length) { - console.log(`POSTER-DEGRADES: SKIP — this tree DOES serve the poster ${reachable.join(' and ')}.`) - console.log(' That is the check:poster-print-fallback case. This gate asserts the tree with NEITHER.') - process.exit(0) + // NOT MEASURED, not a pass: with no wall there is nothing to click, and exit 0 here read as green. + console.log('POSTER-DEGRADES: NOT MEASURED — no wall url given (argv[2] or POSTER_WALL_URL).') + process.exit(NOT_MEASURED) } const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] }) @@ -69,6 +55,12 @@ page.on('download', () => { downloads += 1 }) const posterHits = [] page.on('response', (r) => { if (/\/posters\//.test(r.url())) posterHits.push(`${r.url().split('/').pop()}=${r.status()}`) }) +// THE WORST CASE, forced (see the header): no stored poster, no source, no generator. +// Card thumbnails live under previews/ and still load, so the wall looks as it does to a reader. +let generatorBlocked = 0 +await page.route(/\/posters\/[^/?]+\.(webp|html)(\?|$)/, (route) => route.fulfill({ status: 404, contentType: 'text/plain', body: 'not found' })) +await page.route(/\/lib\/poster-render(\.global)?\.js(\?|$)/, (route) => { generatorBlocked += 1; route.abort() }) + await page.goto(WALL, { waitUntil: 'load', timeout: 45000 }) await page.waitForTimeout(8000) @@ -151,6 +143,7 @@ console.log('POSTER-DEGRADES') console.log(` wall : ${WALL}`) console.log(` clicked : ${clicked || '(nothing)'}`) console.log(` poster fetches: ${JSON.stringify(posterHits)}`) +console.log(` generator : ${generatorBlocked} request(s) aborted${generatorBlocked ? '' : ' (this wall generated nothing first)'}`) console.log(` print frames : ${printFrames} downloads: ${downloads}`) console.log(` told the user : ${JSON.stringify(said) || '(nothing)'}`) diff --git a/internal/check-poster-print-fallback.mjs b/internal/check-poster-print-fallback.mjs index 0e9354245..968a13c3a 100644 --- a/internal/check-poster-print-fallback.mjs +++ b/internal/check-poster-print-fallback.mjs @@ -115,10 +115,21 @@ page.on('pageerror', (e) => errs.push(String(e).slice(0, 140))) // THE DEFAULT-DROP CONDITION, forced: the print-resolution image is absent, its // HTML source is not. Anything else would leave the gate at the mercy of whether // this particular checkout happens to carry 42MB of posters. +// +// ★ AND THE GENERATOR IS UNAVAILABLE. A wall whose host passes `sheet` now GENERATES +// the poster first (packet/posterSheets.js, running the served lib/poster-render.js) +// and never touches posters/ at all. Blocking only the .webp left this gate measuring +// the generator: measured twice on the served tree, 0 .webp and 0 .html requests, and a +// FAIL that said nothing about the fallback. The stored-file path is what a reader gets +// when that generator cannot load (a policy that refuses both the blob and the classic +// script, a failed integrity check, a print while offline), so the generator is blocked +// too, and the .html is the one way through. let webpBlocked = 0 let htmlServed = 0 +let generatorBlocked = 0 await page.route('**/posters/*.webp', (route) => { webpBlocked += 1; route.abort() }) await page.route('**/posters/*.html', (route) => { htmlServed += 1; route.continue() }) +await page.route(/\/lib\/poster-render(\.global)?\.js(\?|$)/, (route) => { generatorBlocked += 1; route.abort() }) await page.goto(WALL, { waitUntil: 'load', timeout: 45000 }) await page.waitForTimeout(8000) @@ -234,6 +245,7 @@ console.log('POSTER-PRINT-FALLBACK') console.log(` wall : ${WALL}`) console.log(` .webp requests : ${webpBlocked} (all aborted — the default-drop condition)`) console.log(` .html requests : ${htmlServed}`) +console.log(` generator : ${generatorBlocked} request(s) aborted${generatorBlocked ? '' : ' (this wall generated nothing first)'}`) console.log(` print button : ${clicked.ok ? 'found and clicked' : 'NOT FOUND'}`) console.log(` HUD : ${hud.length ? JSON.stringify(hud) : '(nothing)'}`) @@ -242,5 +254,5 @@ if (fails.length) { for (const f of fails) console.error(` - ${f}`) process.exit(1) } -console.log('\nPOSTER-PRINT-FALLBACK: PASS — with every .webp missing, the print preview still ' - + 'opens on a real rasterised poster instead of "Poster not available yet".') +console.log('\nPOSTER-PRINT-FALLBACK: PASS — with the generator unavailable and every .webp missing, the print ' + + 'preview still opens on a real rasterised poster instead of "Poster not available yet".') diff --git a/package.json b/package.json index 2e5dd9f3d..584709198 100644 --- a/package.json +++ b/package.json @@ -528,10 +528,10 @@ "check:print-frame-reclaim": "a print frame is kept until it has printed: openPrintFrame reclaims it 60 s after its load (not after its creation), at afterprint, or at a 10-minute ceiling, and printImageBlob revokes its object URL on the same clock. Fake timers; two-sided (the pre-fix copy fails). The Wall poster loads 132 MB of photos, and the old 60 s-from-creation clock removed the frame before it could print.", "check:poster-attribution": "the poster footer is a LICENCE statement, so it must credit whoever actually served the tiles. posterDefaults ships '© Mapbox © OpenStreetMap', which under the fleet's MapLibre default credits a company that served nothing. Checked live 2026-09-28: the OpenFreeMap STYLE declares no attribution on either source — the credit is in the TileJSON, merged into the SOURCE object after load — so it is read from map.getSource(id), flattened from HTML, deduped, and an unreadable map falls back rather than printing a blank line. Headless; in gates:pre.", "check:poster-card-fits": "a poster card's print-options row must stay inside its own card. Reported by the repo agent off a 1440px screenshot: 27/27 cards overflowing by 110px, 36 neighbour intrusions. .pfs already had width:82%, so SOURCE READING SAID IT WAS FINE — flex items default to min-width:auto and refuse to shrink. Asserts BOTH no-overlap AND that the controls keep a usable box, because a row whose buttons collapsed to nothing would also \"not overflow\". Sweeps 390/768/1024/1440/1920px and asserts the row sits BELOW the preview too — measuring only left/right edges is how it passed a driver-visible regression. CARD_FIT_WIDTH overrides the list. SKIPs when nothing is served.", - "check:poster-degrades": "with NEITHER poster nor source — the DEFAULT drop — nothing must print and the reader must be told why. check:poster-print-fallback SKIPs that case, so the behaviour a reader most often meets had no gate. MUST be served from the TREE ROOT, not public/, or assetsBase resolves outside the tree and the poster loads when it should 404.", + "check:poster-degrades": "with NO stored poster, NO source and NO generator (forced in the browser: every posters/*.webp and *.html answers 404, lib/poster-render.js is aborted), nothing must print and the reader must be told why. Measures any served tree; NOT MEASURED without a wall url.", "check:poster-fetch-guard": "a host's 200 fallback page is never taken for a poster. On a drop-in dev host a missing posters/.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.", "check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.", - "check:poster-print-fallback": "clicks the REAL poster print control with every posters/*.webp aborted, and asserts the preview RASTERISES from posters/.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; SKIPs otherwise.", + "check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.", "check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.", "check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT= so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.", "check:preset-config-merge": "a preset page (///index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (go-int's upg-neighbor presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",