From 2019d515c003d29b1a7ee4992b24828bb1b8854c Mon Sep 17 00:00:00 2001 From: Timmy Release Engineering Date: Sat, 31 Oct 2026 12:09:06 +0000 Subject: [PATCH] feat(demo): show the server refusing hostile bytes, not just the device guard (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #16 demo scene stopped at the client guard, so the boundary the ticket is actually about — the server deciding from bytes, before any model work — was never on camera. The recording now lifts its own /api/analyze interceptor for one probe, posts non-image bytes to the running app, and reads the status and copy back out of that response. A 200 fails the recording. Both captions are required release gates, so a later release cannot drop the scene silently. Rehearsed against the real server: 400 not_an_image, card measured legible (opaque, in frame, no scroll), interceptor restored before the next scene. --- scripts/build_release.py | 2 ++ scripts/record_release_demo.mjs | 50 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/scripts/build_release.py b/scripts/build_release.py index 953ff39..a21422b 100755 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -51,6 +51,8 @@ DEMO_REQUIRED_CAPTIONS = ( "Every pattern line names the confirmed logs behind it, and following one shows exactly those records", "Two logs is not a pattern: the summary refuses to describe a trend it cannot support", "A file that only claims to be a photo is refused on the device — nothing is uploaded", + "The device guard is not the only one: the same bytes, sent straight to the running server", + "Refused at the server too: 400 not_an_image, before any model work", "The answer states the count and cites the four confirmed logs it counted", "Following a citation opens exactly the confirmed log behind that answer", "Ask something the journal cannot answer and Timmy refuses instead of reassuring", diff --git a/scripts/record_release_demo.mjs b/scripts/record_release_demo.mjs index 8470719..a14f1b2 100644 --- a/scripts/record_release_demo.mjs +++ b/scripts/record_release_demo.mjs @@ -329,6 +329,56 @@ if (analyzeCalls !== 0) throw new Error('An unreadable file was posted to /api/a await caption('A file that only claims to be a photo is refused on the device — nothing is uploaded', 2600); await indicate('[data-capture-error="unreadable"]'); await caption('Manual logging stays one tap away, and the photo picker is one tap back', 2000); +// Issue #16 has two boundaries, and the device guard is only the first. The bytes below are offered +// straight to this running app with the demo's own /api/analyze interceptor lifted, so the status and +// the refusal copy on the card are read back from the real server rather than narrated. +await caption('The device guard is not the only one: the same bytes, sent straight to the running server', 2600); +await page.unroute('**/api/analyze'); +let serverRefusal; +try { + serverRefusal = await page.evaluate(async () => { + const base = (document.querySelector('meta[name="timmy-base-path"]')?.content || '/').replace(/\/$/, ''); + const bytes = new TextEncoder().encode('named .jpg, not an image'); + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + const response = await fetch(`${base}/api/analyze`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ consent: true, imageDataUrl: `data:image/jpeg;base64,${btoa(binary)}` }), + }); + return { status: response.status, body: await response.json(), contentType: response.headers.get('content-type') }; + }); +} finally { + await page.route('**/api/analyze', route => { + analyzeCalls += 1; + const response = analyzeOverride || { status: 200, body: suggestionBody }; + return route.fulfill({ status: response.status, contentType: 'application/json', body: JSON.stringify(response.body) }); + }); +} +// A 200 here would mean the boundary is untested and the demo is theatre, so the scene fails instead. +if (serverRefusal.status !== 400 || !/not a JPEG, PNG, or WebP photo/i.test(serverRefusal.body?.error || '')) { + throw new Error(`The running server must refuse non-image bytes, saw ${serverRefusal.status} ${JSON.stringify(serverRefusal.body)}`); +} +await evidenceCard({ + id: 'demo-ingress-card', + title: 'IMAGE INGRESS, REFUSED BY THE SERVER', + subtitle: 'The same non-image bytes, posted to this running app in this recording', + rows: [ + ['status', `${serverRefusal.status} refused`], + ['code', String(serverRefusal.body?.code || 'not_an_image')], + ['copy', String(serverRefusal.body?.error || '')], + ['—', ''], + ['provider', 'never called — no model request for these bytes'], + ['budget', 'counted by the per-device limiter before the provider is asked'], + ['after', 'the journal, the draft, and manual logging are untouched'], + ['—', ''], + ['scope', 'this probe is a non-image container. Polyglot tails, embedded metadata, dimensions, body size, and the rate limit are enforced and asserted in the suite'], + ], +}); +const ingressProof = await evidenceCardProof('demo-ingress-card'); +if (!ingressProof.ok) throw new Error(`server refusal card is not legible: ${JSON.stringify(ingressProof)}`); +await caption('Refused at the server too: 400 not_an_image, before any model work', 2600); +await page.evaluate(() => document.querySelector('#demo-ingress-card')?.remove()); await tap('#retake-photo', 450); await indicate('label[for="gallery-photo"]'); await page.locator('#gallery-photo').setInputFiles('tests/fixtures/synthetic-type4.jpg');