Quality gates / quality (pull_request) Successful in 3m29s
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
152 lines
7.9 KiB
JavaScript
152 lines
7.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const dockerfilePath = new URL('../deploy/worker/Dockerfile', import.meta.url);
|
|
const receiptsPath = new URL('../deploy/worker/receipts.json', import.meta.url);
|
|
const buildScriptPath = new URL('../deploy/worker/build_image.sh', import.meta.url);
|
|
const runScriptPath = new URL('../deploy/worker/run_worker_container.sh', import.meta.url);
|
|
const dockerignorePath = new URL('../.dockerignore', import.meta.url);
|
|
const bootstrapPath = new URL('../scripts/bootstrap_selfhost_smolvlm.sh', import.meta.url);
|
|
|
|
async function recipe() {
|
|
const [dockerfile, receipts, buildScript, runScript, dockerignore, bootstrap] = await Promise.all([
|
|
readFile(dockerfilePath, 'utf8'),
|
|
readFile(receiptsPath, 'utf8').then(JSON.parse),
|
|
readFile(buildScriptPath, 'utf8'),
|
|
readFile(runScriptPath, 'utf8'),
|
|
readFile(dockerignorePath, 'utf8'),
|
|
readFile(bootstrapPath, 'utf8'),
|
|
]);
|
|
return { dockerfile, receipts, buildScript, runScript, dockerignore, bootstrap };
|
|
}
|
|
|
|
function bootstrapDefault(script, name) {
|
|
const match = new RegExp(`^${name}="\\$\\{[A-Z0-9_]*:-([^}]+)\\}"$`, 'm').exec(script);
|
|
assert.ok(match, `bootstrap script must declare a default for ${name}`);
|
|
return match[1];
|
|
}
|
|
|
|
test('the worker image pins llama.cpp and both model hashes to the bootstrap receipts', async () => {
|
|
const { dockerfile, receipts, bootstrap } = await recipe();
|
|
const commit = bootstrapDefault(bootstrap, 'LLAMA_CPP_COMMIT');
|
|
const modelSha = bootstrapDefault(bootstrap, 'MODEL_SHA256');
|
|
const mmprojSha = bootstrapDefault(bootstrap, 'MMPROJ_SHA256');
|
|
|
|
// Receipts are the runtime/CI source of truth and must not drift from the host bootstrap.
|
|
assert.equal(receipts.llamaCpp.commit, commit);
|
|
assert.equal(receipts.llamaCpp.repository, 'https://github.com/ggml-org/llama.cpp');
|
|
assert.deepEqual(receipts.models.map(model => model.sha256), [modelSha, mmprojSha]);
|
|
assert.deepEqual(receipts.models.map(model => model.file), [
|
|
'SmolVLM2-2.2B-Instruct-Q4_K_M.gguf',
|
|
'mmproj-SmolVLM2-2.2B-Instruct-Q8_0.gguf',
|
|
]);
|
|
assert.match(receipts.llamaCpp.commit, /^[0-9a-f]{40}$/);
|
|
for (const model of receipts.models) assert.match(model.sha256, /^[0-9a-f]{64}$/);
|
|
|
|
// The image builds the same pinned revision: the build args default to the receipts.
|
|
assert.match(dockerfile, new RegExp(`^ARG LLAMA_CPP_COMMIT=${commit}$`, 'm'));
|
|
assert.match(dockerfile, new RegExp(`^ARG MODEL_SHA256=${modelSha}$`, 'm'));
|
|
assert.match(dockerfile, new RegExp(`^ARG MMPROJ_SHA256=${mmprojSha}$`, 'm'));
|
|
assert.match(dockerfile, /git fetch --depth 1 origin "\$\{LLAMA_CPP_COMMIT\}"/);
|
|
assert.match(dockerfile, /git checkout --detach FETCH_HEAD/);
|
|
});
|
|
|
|
test('the worker image uses digest-pinned bases and refuses floating model sources', async () => {
|
|
const { dockerfile, receipts } = await recipe();
|
|
|
|
const froms = dockerfile.split('\n').filter(line => line.startsWith('FROM '));
|
|
assert.equal(froms.length, 2, 'the recipe is a build stage and a runtime stage');
|
|
for (const line of froms) assert.match(line, /@sha256:[0-9a-f]{64}/, `base must be digest-pinned: ${line}`);
|
|
// The recorded receipts and the recipe must name the same immutable bases.
|
|
const digests = froms.map(line => /@(sha256:[0-9a-f]{64})/.exec(line)[1]);
|
|
assert.deepEqual(digests, [receipts.baseImages.build.digest, receipts.baseImages.runtime.digest]);
|
|
assert.equal(receipts.baseImages.build.reference, 'nvidia/cuda:12.4.1-devel-ubuntu22.04');
|
|
assert.equal(receipts.baseImages.runtime.reference, 'nvidia/cuda:12.4.1-runtime-ubuntu22.04');
|
|
assert.doesNotMatch(dockerfile, /:latest|ggml-org\/llama\.cpp@?main|refs\/heads\/(?:main|master)/);
|
|
// Weights are never baked in; the model arrives as a read-only mount.
|
|
assert.doesNotMatch(dockerfile, /^COPY .*\.gguf/m);
|
|
assert.doesNotMatch(dockerfile, /^ADD /m);
|
|
assert.match(dockerfile, /\/models/);
|
|
});
|
|
|
|
test('the worker image runs unprivileged with no default credentials and a post-load healthcheck', async () => {
|
|
const { dockerfile } = await recipe();
|
|
// Directives may be wrapped with a line continuation; assert on the joined directive text.
|
|
const directives = dockerfile.replace(/\\\n\s*/g, ' ');
|
|
|
|
const user = /^USER (?!root)(\S+)$/m.exec(dockerfile);
|
|
assert.ok(user, 'the runtime stage must drop to an unprivileged user');
|
|
assert.match(user[1], /^\d+:\d+$/, 'use a numeric uid:gid so the image needs no passwd entry');
|
|
assert.doesNotMatch(dockerfile, /^ENV .*(?:API_KEY|TOKEN|PASSWORD|SECRET)[ =]/im);
|
|
// Health must gate on the loaded model, so the container is not "healthy" while it cannot answer.
|
|
assert.match(directives, /^HEALTHCHECK .*timmy-worker-ready/m);
|
|
assert.match(directives, /--start-period=\d+s/);
|
|
assert.match(directives, /--host", "127\.0\.0\.1"/, 'the health probe runs on the container loopback');
|
|
// The container listens inside its own network namespace; the run recipe, not the image, decides
|
|
// exposure and publishes on loopback only (asserted separately).
|
|
assert.match(dockerfile, /--host 0\.0\.0\.0|TIMMY_MODEL_HOST=0\.0\.0\.0/);
|
|
});
|
|
|
|
test('the build refuses a floating or missing pin before it reaches docker', async () => {
|
|
const floating = spawnSync('bash', [buildScriptPath.pathname, '--print-plan'], {
|
|
encoding: 'utf8',
|
|
env: { ...process.env, TIMMY_LLAMA_CPP_COMMIT: 'latest' },
|
|
});
|
|
assert.notEqual(floating.status, 0, 'a floating llama.cpp ref must be rejected');
|
|
assert.match(floating.stderr, /pin|40-hex|floating/i);
|
|
|
|
const missingSha = spawnSync('bash', [buildScriptPath.pathname, '--print-plan'], {
|
|
encoding: 'utf8',
|
|
env: { ...process.env, TIMMY_MODEL_SHA256: '' },
|
|
});
|
|
assert.notEqual(missingSha.status, 0, 'a missing model hash must be rejected');
|
|
|
|
const plan = spawnSync('bash', [buildScriptPath.pathname, '--print-plan'], { encoding: 'utf8' });
|
|
assert.equal(plan.status, 0, plan.stderr);
|
|
assert.match(plan.stdout, /llama\.cpp commit: [0-9a-f]{40}/);
|
|
assert.match(plan.stdout, /SmolVLM2-2\.2B-Instruct-Q4_K_M\.gguf sha256: [0-9a-f]{64}/);
|
|
assert.match(plan.stdout, /bind address: 127\.0\.0\.1:8080/);
|
|
assert.match(plan.stdout, /publish: 127\.0\.0\.1:8080/);
|
|
});
|
|
|
|
test('the run recipe publishes on loopback only and drops every privilege it can', async () => {
|
|
const { runScript } = await recipe();
|
|
|
|
assert.match(runScript, /127\.0\.0\.1:\$\{?PORT/);
|
|
assert.match(runScript, /--read-only/);
|
|
assert.match(runScript, /--cap-drop[= ]ALL/);
|
|
assert.match(runScript, /no-new-privileges/);
|
|
assert.match(runScript, /--security-opt/);
|
|
assert.doesNotMatch(runScript, /--privileged/);
|
|
assert.doesNotMatch(runScript, /-p "\$PORT:/);
|
|
assert.doesNotMatch(runScript, /\b0\.0\.0\.0\b/);
|
|
// The model mount is read-only, and no credential has a default value.
|
|
assert.match(runScript, /:ro\b/);
|
|
assert.doesNotMatch(runScript, /(?:API_KEY|TOKEN|PASSWORD)=[^\s"']/);
|
|
});
|
|
|
|
test('the run recipe refuses to publish the worker anywhere but loopback', async () => {
|
|
const hostile = spawnSync('bash', [runScriptPath.pathname, '--print-plan'], {
|
|
encoding: 'utf8',
|
|
env: { ...process.env, TIMMY_WORKER_PUBLISH_HOST: '0.0.0.0' },
|
|
});
|
|
assert.notEqual(hostile.status, 0, 'a non-loopback publish must be refused');
|
|
assert.match(hostile.stderr, /loopback|127\.0\.0\.1/i);
|
|
|
|
const plan = spawnSync('bash', [runScriptPath.pathname, '--print-plan'], { encoding: 'utf8' });
|
|
assert.equal(plan.status, 0, plan.stderr);
|
|
assert.match(plan.stdout, /publish: 127\.0\.0\.1:\d+/);
|
|
});
|
|
|
|
test('the build context excludes weights, user images, releases, and secrets', async () => {
|
|
const { dockerignore } = await recipe();
|
|
|
|
for (const pattern of ['*.gguf', 'artifacts/', 'video/', 'models/', '.git/', 'node_modules/', '.env', '.env.*']) {
|
|
assert.ok(dockerignore.split('\n').includes(pattern), `dockerignore must exclude ${pattern}`);
|
|
}
|
|
// Committed synthetic fixtures are fine, but a real medical corpus directory is never buildable.
|
|
assert.doesNotMatch(dockerignore, /^!.*\.gguf$/m);
|
|
});
|