Merge pull request #45: enforce reproducible quality gates
All checks were successful
Quality gates / quality (push) Successful in 1m18s

This commit is contained in:
Timmy 2026-08-19 12:06:38 +00:00
commit 8ab1e85bd1
8 changed files with 128 additions and 1 deletions

View File

@ -0,0 +1,56 @@
name: Quality gates
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out source
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install reproducibly
run: |
npm ci
python3 -m pip install --break-system-packages -r requirements-test.txt
- name: Install browser
run: npx playwright install --with-deps chromium
- name: Unit and security tests
run: npm test
- name: Mobile browser acceptance
run: |
npm start > /tmp/timmy-server.log 2>&1 &
server_pid=$!
trap 'kill "$server_pid"' EXIT
for attempt in $(seq 1 30); do
if curl --fail --silent http://127.0.0.1:4173/ > /dev/null; then
break
fi
if [ "$attempt" -eq 30 ]; then
cat /tmp/timmy-server.log
exit 1
fi
sleep 1
done
npm run test:ui
npm run test:photo
- name: Dependency audit
run: npm audit --audit-level=high
- name: Syntax checks
run: npm run check:syntax
- name: Diff hygiene
run: npm run check:diff

View File

@ -4,9 +4,11 @@
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/domain.test.js tests/analysis.test.js tests/vision-service.test.js tests/vision-config.test.js tests/training-ingest.test.js",
"test": "node --test tests/domain.test.js tests/analysis.test.js tests/vision-service.test.js tests/vision-config.test.js tests/training-ingest.test.js tests/ci-workflow.test.js tests/release-demo.test.js",
"test:ui": "node tests/ui.acceptance.mjs",
"test:photo": "node tests/photo-first.acceptance.mjs",
"check:syntax": "node --check app.js && node --check server.mjs && node --check service-worker.js && node --check src/analysis.js && node --check src/domain.js && node --check src/vision-config.js && node --check src/vision-service.js && node --check scripts/record_release_demo.mjs && bash -n scripts/run_selfhost_smolvlm.sh && python3 -m py_compile scripts/ingest_training_photo.py scripts/build_release.py",
"check:diff": "bash scripts/check_diff.sh",
"start": "node server.mjs"
},
"devDependencies": {

1
requirements-test.txt Normal file
View File

@ -0,0 +1 @@
Pillow==12.3.0

14
scripts/check_diff.sh Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${GITHUB_BASE_REF:-}" ]] && git rev-parse --verify --quiet "origin/$GITHUB_BASE_REF" >/dev/null; then
git diff --check "origin/$GITHUB_BASE_REF...HEAD"
elif [[ -n "${GITHUB_EVENT_BEFORE:-}" ]] && [[ "$GITHUB_EVENT_BEFORE" != "0000000000000000000000000000000000000000" ]] && git cat-file -e "$GITHUB_EVENT_BEFORE^{commit}" 2>/dev/null; then
git diff --check "$GITHUB_EVENT_BEFORE..HEAD"
elif git rev-parse --verify --quiet HEAD^ >/dev/null; then
git diff --check HEAD^..HEAD
else
git diff --check HEAD
fi
git diff --check

View File

@ -87,6 +87,7 @@ async function tap(selector, after = 650) {
}
await caption(`TIMMY ${version} • FEATURE DEMO`, 1300);
await caption('Automated checks replay this synthetic path before review', 1300);
await caption('New path: private, photo-first stool logging', 1100);
await tap('[data-scan]', 500);
await page.getByText(/Self-hosted model ready/i).waitFor();

39
tests/ci-workflow.test.js Normal file
View File

@ -0,0 +1,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const workflowPath = new URL('../.gitea/workflows/quality.yml', import.meta.url);
const diffCheckPath = new URL('../scripts/check_diff.sh', import.meta.url);
const pythonRequirementsPath = new URL('../requirements-test.txt', import.meta.url);
test('Gitea CI gates pull requests and main with the reproducible quality suite', async () => {
const workflow = await readFile(workflowPath, 'utf8');
assert.match(workflow, /^name: Quality gates$/m);
assert.match(workflow, /^on:\n push:\n branches: \[main\]\n pull_request:\n branches: \[main\]$/m);
assert.match(workflow, /^permissions:\n contents: read$/m);
assert.match(workflow, /^ timeout-minutes: 15$/m);
assert.match(workflow, /fetch-depth: 0/);
assert.match(workflow, /npm ci/);
assert.match(workflow, /python3 -m pip install --break-system-packages -r requirements-test\.txt/);
assert.match(workflow, /npm test/);
assert.match(workflow, /npm run test:ui/);
assert.match(workflow, /npm run test:photo/);
assert.match(workflow, /npm audit --audit-level=high/);
assert.match(workflow, /npm run check:syntax/);
assert.match(workflow, /npm run check:diff/);
});
test('CI pins the Python image dependency required by the full unit suite', async () => {
const requirements = await readFile(pythonRequirementsPath, 'utf8');
assert.match(requirements, /^Pillow==12\.3\.0$/m);
});
test('diff hygiene checks only the pull request or latest commit range', async () => {
const script = await readFile(diffCheckPath, 'utf8');
assert.match(script, /origin\/\$GITHUB_BASE_REF\.\.\.HEAD/);
assert.match(script, /HEAD\^\.\.HEAD/);
assert.doesNotMatch(script, /hash-object -t tree/);
});

View File

@ -41,6 +41,7 @@ await page.locator('#analyze-photo').click();
await page.getByText(/83% confidence/i).waitFor();
assert.equal(await page.getByText('Type 4', { exact: true }).isVisible(), true);
assert.equal(await page.getByText('brown', { exact: true }).isVisible(), true);
assert.match(await page.locator('.scan-result .fine').innerText(), /not a diagnosis/i);
await page.screenshot({ path: 'artifacts/photo-first-result-mobile.png', fullPage: false });
await page.locator('#use-suggestion').click();

View File

@ -0,0 +1,13 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const demoPath = new URL('../scripts/record_release_demo.mjs', import.meta.url);
test('release demo visibly explains the CI-protected browser path without overstating safety', async () => {
const demo = await readFile(demoPath, 'utf8');
assert.match(demo, /Automated checks replay this synthetic path before review/);
assert.match(demo, /tests\/fixtures\/synthetic-type4\.jpg/);
assert.match(demo, /Visual assistance — never a diagnosis\./);
});