Files
timmy-talking-turd/tests/mobile-capture.acceptance.mjs
T
Timmy 46f961668f
Quality gates / quality (pull_request) Successful in 3m29s
feat: pin the self-hosted worker container and gate readiness on the loaded model (#15)
A worker that answers /v1/models has not necessarily loaded its weights, so the app
was offering an AI control that could only fail after the upload. Readiness now
passes only once the pinned model is loaded, and the not-loaded state reaches manual
Bristol entry in one tap. Verified against the real pinned llama.cpp build: it serves
503 while loading and the gate reports the distinct not-loaded state.

Adds the reproducible container recipe (digest-pinned bases, pinned llama.cpp commit,
hash-verified read-only weights, unprivileged, loopback-only publish), one shared
post-load readiness gate used by the host bootstrap and the image healthcheck, and a
target-GPU benchmark that refuses to write a receipt it did not measure.

Refs #15
2026-10-13 08:20:21 +00:00

62 lines
2.8 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
const browser = await chromium.launch({ headless: true });
const viewports = [
{ name: '390x844', width: 390, height: 844 },
{ name: 'iPhone 15 class', width: 393, height: 852 },
];
for (const viewport of viewports) {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 2,
serviceWorkers: 'block',
});
const page = await context.newPage();
let analysisRequests = 0;
await page.route('**/api/vision-status', route => route.fulfill({
status: 200,
contentType: 'application/json',
// The fixture mirrors the real /api/vision-status contract, which reports whether the pinned
// model was actually seen loaded (#15) alongside provider reachability.
body: JSON.stringify({ enabled: true, profile: 'selfhost', providerReady: true, modelSeen: true, model: 'synthetic-test-model' }),
}));
await page.route('**/api/analyze', route => {
analysisRequests += 1;
return route.abort();
});
await page.goto('http://127.0.0.1:4173', { waitUntil: 'networkidle' });
await page.locator('[data-scan]').click();
const camera = page.locator('#camera-photo');
const gallery = page.locator('#gallery-photo');
assert.equal(await camera.getAttribute('capture'), 'environment', `${viewport.name}: camera input uses the rear camera`);
assert.equal(await gallery.getAttribute('capture'), null, `${viewport.name}: gallery input does not force camera capture`);
assert.equal(await page.getByText('Take photo', { exact: true }).isVisible(), true);
assert.equal(await page.getByText('Choose from gallery', { exact: true }).isVisible(), true);
await camera.dispatchEvent('cancel');
assert.match(await page.locator('[role="status"]').innerText(), /camera.*closed|permission.*denied/i);
assert.equal(await page.getByText('Continue without AI', { exact: true }).isVisible(), true);
assert.equal(analysisRequests, 0, `${viewport.name}: cancellation never uploads`);
await page.locator('#gallery-photo').setInputFiles({
name: 'corrupt-synthetic.jpg',
mimeType: 'image/jpeg',
buffer: Buffer.from('not an image'),
});
assert.match(await page.locator('.scan-result').innerText(), /could not be read/i);
await page.getByText('Try another photo', { exact: true }).click();
await page.locator('#gallery-photo').setInputFiles('tests/fixtures/synthetic-type4.jpg');
assert.equal(await page.locator('#analyze-photo').isDisabled(), true);
assert.equal(await page.locator('#retake-photo').isVisible(), true);
assert.equal(analysisRequests, 0, `${viewport.name}: corrupt and unconsented photos never upload`);
await context.close();
}
await browser.close();
console.log('PASS camera/gallery paths recover from cancellation at 390x844 and iPhone-class viewport without upload');