timmy-talking-turd/tests/image-unavailable.test.js
Timmy 517c8dbac3
Some checks failed
Quality gates / quality (pull_request) Failing after 2m39s
Harden image ingress: pinned runtime, header-bomb rejection, concurrency ceiling, bounded rate limiter, canonical data-URL/polyglot contract, 503-on-unavailable, body-read timeout
Closes the PR 63 hostile-review blockers:
1. Immutable pinned Python/Pillow runtime (TIMMY_PYTHON absolute, --verify-pin),
   deployment/runtime re-encode smoke gate in build_release + deploy_staging.
2. Header-only width/height/total-pixel/bomb rejection before full decode; proves
   6000x6000 and 12000x12000 stay resource bounded (RSS + address-space caps).
3. Fail-fast decoder concurrency ceiling; tests count actual spawned children.
4. Rate-limiter key cardinality hard-bounded under 20k+ unexpired identities,
   trusted-loopback-proxy identity, no spoofable forwarded headers, fixed-window
   boundary burst smoothed by two-window sliding count.
5. build_release explicitly syntax/gates every new JS module + Python re-encoder +
   production-runtime smoke; CI runs reencode-image test and the runtime pin smoke.
6. Robust polyglot contract via canonical container parsing; rejects uppercase/mixed
   script, appended HTML, ZIP local/EOCD and archive tails, data after canonical
   JPEG/PNG/WebP end; no naive compressed-byte scans (false-positive controls pass).
7. Canonical base64 data-URL grammar with byte-exact round trip; rejects missing/excess
   padding, whitespace/CRLF, malformed and noncanonical encodings; exact MIME policy.
8. Missing Pillow / runtime-unavailable maps to sanitized 503 + manual fallback.
9. Inbound body-read timeout and stop-on-oversize; preserves sanitized 413, base path,
   provider suppression, and temp cleanup; socket torn down on rejection.

Audited prior partial edits: reused the sound source modules, re-wired new tests into
the unit/syntax gates, fixed a non-canonical readJson that destroyed the socket before
delivering 413/408, and hardened test flakiness (port reuse, EPIPE, startup races).
2026-08-22 23:22:42 +00:00

155 lines
7.1 KiB
JavaScript

// Processing-unavailable contract.
//
// A production interpreter that cannot import Pillow is a server-side runtime
// fault, not hostile client input. It must surface as a sanitized
// processing-unavailable outcome with the manual-continue fallback (HTTP 503),
// never as a 400 that blames the user's photo for being corrupt.
import test from 'node:test';
import assert from 'node:assert/strict';
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
IngressUnavailableError,
validateImageIngress,
classifyIngressFailure,
verifyReencodeRuntime,
resolveInterpreter,
PINNED_PYTHON_VERSION,
PINNED_PILLOW_VERSION,
} from '../src/image-ingress.js';
import { analyzePhoto } from '../src/vision-service.js';
const fixture = (name) => fileURLToPath(new URL(`../tests/fixtures/${name}`, import.meta.url));
const clean = await readFile(fixture('ingress-clean.jpg'));
const cleanDataUrl = `data:image/jpeg;base64,${clean.toString('base64')}`;
// A stand-in interpreter that behaves exactly like the real re-encoder running
// on a runtime without Pillow: the documented unavailable verdict and exit 3.
async function stubInterpreter(name, body) {
const dir = join(tmpdir(), `timmy-ingress-stub-${name}`);
await mkdir(dir, { recursive: true });
const path = join(dir, 'python3');
await writeFile(path, body);
await chmod(path, 0o755);
return path;
}
const withPython = async (path, run) => {
const previous = process.env.TIMMY_PYTHON;
process.env.TIMMY_PYTHON = path;
try {
return await run();
} finally {
if (previous === undefined) delete process.env.TIMMY_PYTHON;
else process.env.TIMMY_PYTHON = previous;
}
};
test('a runtime without Pillow raises an unavailable ingress error, not a corrupt-input error', async () => {
const stub = await stubInterpreter('nopil',
'#!/bin/sh\necho \'{"ok": false, "error": "unavailable"}\'\nexit 3\n');
await withPython(stub, async () => {
const error = await validateImageIngress({ consent: true, imageDataUrl: cleanDataUrl })
.then(() => null, (caught) => caught);
assert.ok(error, 'ingress must fail when the runtime cannot process images');
assert.ok(error instanceof IngressUnavailableError,
'a missing runtime must be classified as unavailable, not as corrupt client input');
assert.match(error.message, /temporarily unavailable/i);
assert.match(error.message, /continue manually/i);
assert.doesNotMatch(error.message, /corrupt|malformed/i);
});
});
test('classifyIngressFailure maps unavailable ingress failures to 503 and corrupt input to 400', () => {
assert.equal(classifyIngressFailure(new IngressUnavailableError('Photo processing is temporarily unavailable. Continue manually.')), 503);
assert.equal(classifyIngressFailure(new Error('The photo is corrupt or malformed. Try a different photo or continue manually.')), 400);
assert.equal(classifyIngressFailure(new Error('Upload a JPEG, PNG, or WebP photo. That file is not a supported image.')), 400);
assert.equal(classifyIngressFailure(new Error('Explicit consent is required before AI analysis.')), 400);
assert.equal(classifyIngressFailure(new Error('The photo is too large. Use an image under 4 MB.')), 400);
});
test('a re-encoder timeout is also unavailable rather than corrupt', async () => {
const stub = await stubInterpreter('hang', '#!/bin/sh\nsleep 60\n');
await withPython(stub, async () => {
const error = await validateImageIngress({
consent: true, imageDataUrl: cleanDataUrl, reencodeTimeoutMs: 250,
}).then(() => null, (caught) => caught);
assert.ok(error instanceof IngressUnavailableError);
assert.match(error.message, /temporarily unavailable/i);
});
});
test('an interpreter that cannot be executed at all is unavailable, not corrupt', async () => {
await withPython('/nonexistent/timmy-python-does-not-exist', async () => {
const error = await validateImageIngress({ consent: true, imageDataUrl: cleanDataUrl })
.then(() => null, (caught) => caught);
assert.ok(error instanceof IngressUnavailableError,
'a missing interpreter is a server fault and must not be reported as corrupt input');
});
});
test('genuinely corrupt input is still classified as client error, not unavailable', async () => {
const garbage = await readFile(fixture('ingress-garbage.jpg'));
const error = await validateImageIngress({
consent: true, imageDataUrl: `data:image/jpeg;base64,${garbage.toString('base64')}`,
}).then(() => null, (caught) => caught);
assert.ok(error, 'garbage must be rejected');
assert.equal(error instanceof IngressUnavailableError, false);
assert.equal(classifyIngressFailure(error), 400);
});
test('the provider is never called when the runtime is unavailable', async () => {
const stub = await stubInterpreter('nopil2',
'#!/bin/sh\necho \'{"ok": false, "error": "unavailable"}\'\nexit 3\n');
await withPython(stub, async () => {
let providerCalled = false;
await assert.rejects(
() => analyzePhoto({
payload: { consent: true, imageDataUrl: cleanDataUrl },
config: { model: 'm', baseUrl: 'http://127.0.0.1:9/v1' },
fetchImpl: async () => { providerCalled = true; throw new Error('provider reached'); },
}),
/temporarily unavailable/i,
);
assert.equal(providerCalled, false);
});
});
test('the production interpreter path must be absolute, never a PATH lookup', () => {
// Development falls back to PATH `python3`.
assert.equal(resolveInterpreter({}), 'python3');
assert.equal(resolveInterpreter({ TIMMY_PYTHON: '' }), 'python3');
// Production requires an absolute, immutable interpreter path.
assert.equal(resolveInterpreter({ TIMMY_PYTHON: '/usr/local/lib/timmy-staging/python' }),
'/usr/local/lib/timmy-staging/python');
assert.throws(() => resolveInterpreter({ TIMMY_PYTHON: 'python3' }),
/absolute interpreter path/i);
assert.throws(() => resolveInterpreter({ TIMMY_PYTHON: 'relative/python' }),
/absolute interpreter path/i);
});
test('verifyReencodeRuntime confirms the pinned, immutable production toolchain', async () => {
// The real interpreter is the pinned 3.11 / Pillow 12.3.0 toolchain, so the
// runtime smoke must succeed and report exactly that pin.
const verdict = await verifyReencodeRuntime();
assert.equal(verdict.python, PINNED_PYTHON_VERSION);
assert.equal(verdict.pillow, PINNED_PILLOW_VERSION);
assert.deepEqual(verdict.pinned, { python: PINNED_PYTHON_VERSION, pillow: PINNED_PILLOW_VERSION });
});
test('verifyReencodeRuntime fails closed when pointed at a wrong interpreter', async () => {
// A stub interpreter that exits 3 (unavailable) must be reported as an
// ingress-capacity fault, never as corrupt client input.
const stub = await stubInterpreter('pinfail',
'#!/bin/sh\necho \'{"ok": false, "error": "unavailable"}\'\nexit 3\n');
await withPython(stub, async () => {
const error = await verifyReencodeRuntime().then(() => null, (caught) => caught);
assert.ok(error instanceof IngressUnavailableError,
'a non-pinned/unavailable runtime must fail closed as processing-unavailable');
});
});