Files
timmy-talking-turd/tests/training-ingest.test.js
T
timmy 860c84f0fe
Quality gates / quality (pull_request) Failing after 9m42s
feat(corpus): auditable manifest provenance, tombstones, and lifecycle guards (#25)
Promote the ingest script into an auditable corpus pipeline. Each sample now
records consent (version + timestamp), source kind and raw-input digest, both
hashes, the derivation pipeline, review status, split, and lifecycle state, with
the caller's path never written down.

Withdrawal is a new `--revoke <contribution-id> --reason` operation: it deletes
the derived image and any raw pre-derivation copy, then appends a tombstone that
addresses each removed artifact by contribution id and digest while dropping
labels, subject key, and paths. History is append-only, so the original sample
record survives the withdrawal it documents.

Illegal moves are refused instead of silently applied: revoked -> active,
revoking an unknown or already-revoked contribution, an unknown state, a
duplicate contribution id, and a second label set for the same derived image.
The duplicate guard runs before any byte is written, so a refused ingest cannot
strand an image in the active dataset.

Tests written first and watched fail (tests/corpus-manifest.test.py): provenance,
tombstone + byte removal, active-record filtering, and the transition guards.
The transition helper itself passed on first write and is reported as a
hold-out check rather than a RED cycle. Wired into the quality workflow and
check:syntax; contract documented in docs/CORPUS-MANIFEST.md.

No browser surface changed, so record_release_demo.mjs is untouched and claims
nothing new: the nearest honest visible effect is the withdrawn-receipt
tombstone already recorded for #24.
2026-10-24 10:58:30 +00:00

57 lines
2.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
test('training ingest strips metadata, derives a pseudonymous split, and writes reviewable labels', async () => {
const out = await mkdtemp(join(tmpdir(), 'timmy-ingest-'));
try {
const run = spawnSync('python3', [
'scripts/ingest_training_photo.py',
'--input', 'tests/fixtures/synthetic-type4.jpg',
'--output-dir', out,
'--subject-id', 'subject-123',
'--consent-version', 'research-v1',
'--contribution-id', '6f1d0c9e-2c4a-4f3b-9d1e-8a7b6c5d4e3f',
'--bristol-type', '4',
'--color', 'brown',
'--quality', 'good',
'--reviewer', 'user-confirmed',
], { encoding: 'utf8', env: { ...process.env, TIMMY_DATASET_SALT: 'test-only-salt' } });
assert.equal(run.status, 0, run.stderr);
const record = JSON.parse(run.stdout);
assert.match(record.imageId, /^[a-f0-9]{64}$/);
assert.ok(['train', 'validation', 'test'].includes(record.split));
assert.equal(record.subjectKey.length, 16);
assert.equal(record.consentVersion, 'research-v1');
assert.deepEqual(record.labels, { bristolType: 4, color: 'brown', quality: 'good' });
assert.equal(record.review.status, 'user-confirmed');
assert.equal('inputPath' in record, false);
const derived = join(out, record.relativePath);
assert.ok((await stat(derived)).size > 100);
const manifest = (await readFile(join(out, 'manifest.jsonl'), 'utf8')).trim().split('\n').map(JSON.parse);
assert.deepEqual(manifest, [record]);
const exif = spawnSync('python3', ['-c', "from PIL import Image;import sys;print(len(Image.open(sys.argv[1]).getexif()))", derived], { encoding: 'utf8' });
assert.equal(exif.stdout.trim(), '0');
} finally {
await rm(out, { recursive: true, force: true });
}
});
test('training ingest rejects records without explicit consent or human review', () => {
const run = spawnSync('python3', [
'scripts/ingest_training_photo.py',
'--input', 'tests/fixtures/synthetic-type4.jpg',
'--output-dir', '/tmp/should-not-exist-timmy',
'--subject-id', 'subject-123',
'--contribution-id', '6f1d0c9e-2c4a-4f3b-9d1e-8a7b6c5d4e3f',
'--bristol-type', '4',
'--color', 'brown',
'--quality', 'good',
], { encoding: 'utf8', env: { ...process.env, TIMMY_DATASET_SALT: 'test-only-salt' } });
assert.notEqual(run.status, 0);
assert.match(run.stderr, /consent/i);
});