Files
stack/packages/processing-control/tests/containers.test.mjs
T
rocko 4bdc7e0e86 processing: inspectable per-run stop seam for lifecycle fire
Unify halt and fire cancellation by verified run identity with durable first-owner receipts and shared deadlines. Integrate the instance control with the current host, library and tester-socket paths. Focused nine-file evidence is 62 passing tests; broad suites, mutants and review remain pending.
2026-10-21 22:42:12 -05:00

86 lines
5.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { initializeControl, transition } from '../src/state.mjs';
const api = await import('../src/containers.mjs').catch(e => { if (e.code === 'ERR_MODULE_NOT_FOUND') return {}; throw e; });
function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'processing-container-')); initializeControl(root); t.after(() => rmSync(root, { recursive: true, force: true })); return root; }
const actor = { business: 'demo', human: 'jason', halted: true };
test('container admission publishes its generated name and root/runtime labels before dispatch', t => {
assert.equal(typeof api.prepareContainer, 'function');
const root = fixture(t), r = api.prepareContainer(root, { kind: 'task', business: 'instance', run: 'run-1' });
const args = api.containerArgs(r, { args: ['request'] });
assert.match(r.containerName, /^mosaic-[a-f0-9]{12}-[a-f0-9-]{36}$/);
assert.ok(args.includes(r.containerName)); assert.ok(args.includes(`mosaic.root=${r.root}`));
assert.ok(args.includes(`mosaic.runtime=${r.id}`)); assert.ok(args.includes(`MOSAIC_PROCESSING_GENERATION=${r.generation}`));
assert.deepEqual(args.slice(-2), ['mosaic-agent', 'request']);
transition(root, actor); assert.throws(() => api.prepareContainer(root, { kind: 'task', business: 'instance', run: 'new' }), e => e.code === 'processing-halted');
});
function inspected(r, running = true, id = 'a'.repeat(64)) {
return { Id: id, Name: '/' + r.containerName, Config: { Labels: { 'mosaic.root': r.root, 'mosaic.runtime': r.id } }, State: { Running: running } };
}
test('container sweep sends TERM then KILL only after verifying exact id and both labels', async t => {
assert.equal(typeof api.createContainerStopper, 'function');
const root = fixture(t), r = api.prepareContainer(root, { kind: 'task', business: 'instance', run: 'one' });
const calls = []; let running = true;
const stop = api.createContainerStopper({ docker: async args => {
calls.push(args);
if (args[0] === 'inspect') return JSON.stringify(inspected(r, running));
if (args.includes('KILL')) running = false;
return '';
} });
const result = await stop(r, { graceMs: 30, killWaitMs: 100 });
assert.equal(result.outcome, 'killed'); assert.deepEqual(result.signals, ['SIGTERM', 'SIGKILL']);
assert.deepEqual(calls.filter(c => c[0] === 'kill'), [['kill', '--signal', 'TERM', 'a'.repeat(64)], ['kill', '--signal', 'KILL', 'a'.repeat(64)]]);
});
test('foreign labels and a replaced container id stay unknown and are never signalled', async t => {
assert.equal(typeof api.createContainerStopper, 'function');
const root = fixture(t), r = api.prepareContainer(root, { kind: 'task', business: 'instance', run: 'one' });
const calls = [];
const stop = api.createContainerStopper({ docker: async args => {
calls.push(args); const doc = inspected(r); doc.Config.Labels['mosaic.root'] = 'b'.repeat(64); return JSON.stringify(doc);
} });
assert.equal((await stop(r, { graceMs: 1, killWaitMs: 1 })).outcome, 'unknown');
assert.equal(calls.filter(c => c[0] === 'kill').length, 0);
let reads = 0; calls.length = 0;
const replacement = api.createContainerStopper({ docker: async args => {
calls.push(args); if (args[0] !== 'inspect') return '';
return JSON.stringify(inspected(r, true, (++reads > 1 ? 'b' : 'a').repeat(64)));
} });
assert.equal((await replacement(r, { graceMs: 1, killWaitMs: 1 })).outcome, 'unknown');
assert.equal(calls.filter(c => c[0] === 'kill').length, 0);
});
test('an unverified missing container remains unknown rather than claiming a pending start ended', async t => {
assert.equal(typeof api.createContainerStopper, 'function');
const root = fixture(t), r = api.prepareContainer(root, { kind: 'container', business: 'instance', run: 'one' });
const stop = api.createContainerStopper({ docker: async () => { throw new Error('no container'); } });
assert.equal((await stop(r, { graceMs: 5, killWaitMs: 5 })).outcome, 'unknown');
});
test('a removed --rm container is settled only by its trusted owner completion receipt', async t => {
const { updateRuntime } = await import('../src/runtimes.mjs');
const root = fixture(t), r = api.prepareContainer(root, { kind: 'task', business: 'instance', run: 'one' });
let removed = false;
const stop = api.createContainerStopper({ dataRoot: root, docker: async args => {
if (args[0] === 'kill') { removed = true; updateRuntime(root, r.id, { phase: 'ended' }); return ''; }
if (removed) throw Error('removed by --rm');
return JSON.stringify(inspected(r));
} });
const result = await stop(r, { graceMs: 30, killWaitMs: 100 });
assert.equal(result.outcome, 'terminated'); assert.deepEqual(result.signals, ['SIGTERM']);
});
test('after signalling a verified container, successful empty Docker listing proves --rm removed that id', async t => {
const root = fixture(t), r = api.prepareContainer(root, { kind: 'task', business: 'instance', run: 'one' });
let removed = false;
const stop = api.createContainerStopper({ docker: async args => {
if (args[0] === 'kill') { removed = true; return ''; }
if (args[0] === 'ps') return '';
if (removed) throw Error('removed');
return JSON.stringify(inspected(r));
} });
const result = await stop(r, { graceMs: 20, killWaitMs: 100 });
assert.equal(result.outcome, 'terminated'); assert.deepEqual(result.signals, ['SIGTERM']);
});