Files
timmy-talking-turd/tests/ingress-capture.acceptance.mjs
T
timmy 9b72872466 feat(photo): give an unreadable file its own upload-free state instead of a failed analysis (#16)
Selecting a file the browser cannot decode used to render "Timmy couldn't analyze that safely",
which described an upload that never happened. It now states that the file isn't a photo Timmy can
read and that nothing was uploaded, with manual entry and the picker both one tap away. The state is
asserted in a new browser suite that also asks the running server to refuse the same bytes, and the
existing mobile capture suite asserts the new copy across both viewports.
2026-10-20 08:21:24 +00:00

71 lines
3.8 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
// Issue #16, as a user experiences it: a file that only claims to be a photo never becomes an upload,
// never reaches the model worker, and still leaves manual logging one tap away. The second half of
// this suite asks the real running server to refuse the same bytes, so the browser copy and the
// server boundary are proven in the same run.
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 2,
serviceWorkers: 'block',
});
const page = await context.newPage();
let analysisRequests = 0;
const analyzePayloads = [];
await page.route('**/api/vision-status', route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ enabled: true, profile: 'selfhost', providerReady: true, modelSeen: true, model: 'synthetic-test-model' }),
}));
await page.route('**/api/analyze', route => {
analysisRequests += 1;
analyzePayloads.push(route.request().postData() || '');
return route.continue();
});
await page.goto('http://127.0.0.1:4173', { waitUntil: 'networkidle' });
await page.locator('[data-scan]').click();
const spoofed = Buffer.from('<html><script>alert(1)</script></html>', 'utf8');
await page.locator('#gallery-photo').setInputFiles({ name: 'photo.jpg', mimeType: 'image/jpeg', buffer: spoofed });
const refusal = page.locator('[data-capture-error="unreadable"]');
await refusal.waitFor();
const copy = await refusal.innerText();
assert.match(copy, /isn.t a photo Timmy can read/i, 'the refusal names what is wrong with the file');
assert.match(copy, /Nothing was uploaded/i, 'the refusal states that nothing left the device');
assert.equal(await page.getByText(/couldn.t analyze that safely/i).count(), 0,
'an unreadable file must not be reported as a failed analysis');
assert.equal(analysisRequests, 0, 'an unreadable file must never be posted to /api/analyze');
assert.equal(await page.locator('#manual-from-scan').isVisible(), true, 'manual entry stays one tap away');
assert.equal(await page.locator('#retake-photo').isVisible(), true, 'the picker stays reachable');
// The same bytes, offered to the running server directly, are refused before any provider work.
const serverResponse = await page.evaluate(async payload => {
const response = await fetch(`${document.querySelector('meta[name="timmy-base-path"]')?.content.replace(/\/$/, '') || ''}/api/analyze`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
return { status: response.status, body: await response.json() };
}, { consent: true, imageDataUrl: `data:image/jpeg;base64,${spoofed.toString('base64')}` });
assert.equal(serverResponse.status, 400, `the server must refuse non-image bytes, saw ${serverResponse.status}`);
assert.match(serverResponse.body.error, /not a JPEG, PNG, or WebP photo/i);
// Only this deliberate probe reached the endpoint; the app itself never posted the unreadable file.
assert.equal(analysisRequests, 1, 'only the deliberate server probe may reach /api/analyze so far');
assert.match(analyzePayloads[0], /"consent":true/);
// A real (synthetic) photo still reaches the consent step, so the gate is a boundary and not a wall.
await page.locator('#retake-photo').click();
await page.locator('#gallery-photo').setInputFiles('tests/fixtures/synthetic-type4.jpg');
await page.locator('#ai-consent').waitFor();
assert.equal(await page.locator('[data-capture-error="unreadable"]').count(), 0);
assert.equal(analysisRequests, 1, 'a photo that is only selected is still not uploaded before consent');
await context.close();
await browser.close();
console.log('PASS unreadable photo bytes are refused on device and at the server, with manual entry intact');