feat(demo): show the server refusing hostile bytes, not just the device guard (#16)
Quality gates / quality (push) Canceled after 0s
Quality gates / quality (push) Canceled after 0s
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.
This commit is contained in:
1 parent
c31d0a510f
commit
2019d515c0
2 files changed
+52
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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('<!doctype html><html><body>named .jpg, not an image</body></html>');
|
||||
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');
|
||||
|
||||
Reference in new issue
Block a user