Files
stack/packages/processing-control/tests/state.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

166 lines
9.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
const api = await import('../src/state.mjs').catch(e => {
if (e.code === 'ERR_MODULE_NOT_FOUND') return {};
throw e;
});
const root = t => {
const dir = mkdtempSync(join(tmpdir(), 'processing-state-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
return dir;
};
const actor = { business: 'demo', human: 'jason' };
const stateFile = dir => join(dir, 'control', 'processing.json');
const refusal = code => e => e.code === code;
// Removing durable publication must let a new reader launch after halt.
test('halt persists across readers and resume needs the current generation', t => {
assert.equal(typeof api.initializeControl, 'function');
const dir = root(t);
const initial = api.initializeControl(dir);
assert.equal(initial.halted, false);
const halted = api.transition(dir, { ...actor, halted: true });
assert.equal(halted.halted, true);
assert.notEqual(halted.generation, initial.generation);
assert.equal(api.readControl(dir).generation, halted.generation);
assert.throws(() => api.assertProcessing(dir), refusal('processing-halted'));
assert.throws(() => api.transition(dir, { ...actor, halted: false, generation: initial.generation }), refusal('processing-generation-changed'));
const resumed = api.transition(dir, { ...actor, halted: false, generation: halted.generation });
assert.equal(resumed.halted, false);
assert.doesNotThrow(() => api.assertProcessing(dir));
// A start that waited across halt+resume must not use its old admission.
assert.throws(() => api.assertProcessing(dir, initial.generation), refusal('processing-generation-changed'));
assert.equal(lstatSync(stateFile(dir)).mode & 0o777, 0o600);
assert.equal(lstatSync(join(dir, 'control')).mode & 0o777, 0o700);
});
// Reinitializing a missing file as off would bypass a previously published halt.
test('first initialization is explicit; missing state after control exists fails closed and halt repairs it', t => {
assert.equal(typeof api.initializeControl, 'function');
const dir = root(t);
assert.throws(() => api.readControl(dir), refusal('processing-uninitialized'));
const initial = api.initializeControl(dir);
assert.equal(api.initializeControl(dir).generation, initial.generation);
unlinkSync(stateFile(dir));
assert.throws(() => api.initializeControl(dir), refusal('processing-state-invalid'));
assert.throws(() => api.assertProcessing(dir), refusal('processing-state-invalid'));
assert.throws(() => api.transition(dir, { ...actor, halted: false, generation: initial.generation }), refusal('processing-state-invalid'));
const halted = api.transition(dir, { ...actor, halted: true });
assert.equal(halted.recovered, true);
assert.equal(halted.halted, true);
assert.equal(api.transition(dir, { ...actor, halted: false, generation: halted.generation }).halted, false);
});
// A read error, permissive mode or unbounded payload must never look like off.
for (const [name, edit] of [
['invalid JSON', file => writeFileSync(file, '{')],
['unknown keys', file => writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file)), extra: true }))],
['bad timestamp', file => writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file)), at: 'yesterday' }))],
['nonboolean state', file => writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file)), halted: 0 }))],
['bad generation', file => writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file)), generation: 'bad' }))],
['loose mode', file => chmodSync(file, 0o644)],
['oversized file', file => writeFileSync(file, ' '.repeat(8193))],
]) test(`refuses ${name} instead of enabling work`, t => {
assert.equal(typeof api.initializeControl, 'function');
const dir = root(t); api.initializeControl(dir); edit(stateFile(dir));
assert.throws(() => api.assertProcessing(dir), refusal('processing-state-invalid'));
});
test('a symlink leaf never follows its target; human halt replaces only the leaf', t => {
assert.equal(typeof api.initializeControl, 'function');
const dir = root(t); api.initializeControl(dir);
const outside = join(dir, 'outside'), target = readFileSync(stateFile(dir));
writeFileSync(outside, target, { mode: 0o600 });
unlinkSync(stateFile(dir)); symlinkSync(outside, stateFile(dir));
assert.throws(() => api.readControl(dir), refusal('processing-state-invalid'));
api.transition(dir, { ...actor, halted: true });
assert.deepEqual(readFileSync(outside), target);
assert.equal(lstatSync(stateFile(dir)).isSymbolicLink(), false);
});
test('unsafe parent and interrupted initialization never get repaired as off', t => {
assert.equal(typeof api.initializeControl, 'function');
const dir = root(t); mkdirSync(join(dir, 'control'), { mode: 0o700 });
assert.throws(() => api.initializeControl(dir), refusal('processing-state-invalid'));
chmodSync(join(dir, 'control'), 0o777);
assert.throws(() => api.transition(dir, { ...actor, halted: true }), refusal('processing-control-unsafe'));
});
test('repeated halt is a no-op and a different root keeps running', t => {
assert.equal(typeof api.initializeControl, 'function');
const a = root(t), b = root(t); api.initializeControl(a); api.initializeControl(b);
const first = api.transition(a, { ...actor, halted: true });
const before = readFileSync(stateFile(a));
assert.equal(api.transition(a, { ...actor, halted: true }).generation, first.generation);
assert.deepEqual(readFileSync(stateFile(a)), before);
assert.doesNotThrow(() => api.assertProcessing(b));
});
// Coercing undefined/numbers through a regex would publish unverifiable actor metadata.
test('transition rejects nonstring identity before publishing state', t => {
const dir = root(t); api.initializeControl(dir);
const before = readFileSync(stateFile(dir));
for (const bad of [undefined, null, 123, ['jason']]) {
assert.throws(() => api.transition(dir, { ...actor, human: bad, halted: true }), refusal('invalid-request'));
assert.deepEqual(readFileSync(stateFile(dir)), before);
}
});
// A lost control directory after power failure could permit a later off bootstrap.
test('bootstrap syncs the containing root as well as the state and control directory', t => {
const dir = root(t);
const script = `import fs from 'node:fs'; import {syncBuiltinESMExports} from 'node:module';
const real=fs.fsyncSync, seen=[];
fs.fsyncSync=fd=>{seen.push(fs.readlinkSync('/proc/self/fd/'+fd));return real(fd)};
syncBuiltinESMExports();
const {initializeControl}=await import(process.argv[1]);initializeControl(process.argv[2]);
console.log(JSON.stringify(seen));`;
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script,
new URL('../src/state.mjs', import.meta.url).href, dir], { encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
const seen = JSON.parse(result.stdout);
assert.ok(seen.includes(dir), 'control mkdir must be durable in the root');
assert.ok(seen.includes(join(dir, 'control')), 'state rename must be durable');
assert.ok(seen.some(p => p.startsWith(join(dir, 'control', '.state-'))), 'state bytes must be durable');
});
// Each fstat/read guard is exercised independently, rather than hidden by a later parse refusal.
test('state ownership and bounded read guards refuse before trusting a document', t => {
const dir = root(t); api.initializeControl(dir); const file = stateFile(dir);
const script = `import fs from 'node:fs';import {syncBuiltinESMExports} from 'node:module';
const stat=fs.fstatSync,read=fs.readSync;let reads=0;
const file=process.argv[2],which=process.argv[3],valid=fs.readFileSync(file);
if(which==='oversize')fs.writeFileSync(file,Buffer.concat([valid,Buffer.alloc(8193,32)]));
fs.fstatSync=fd=>{const s=stat(fd);if(fs.readlinkSync('/proc/self/fd/'+fd)===file){
if(which==='owner')s.uid++;
if(which==='grow')fs.writeFileSync(file,Buffer.concat([valid,Buffer.alloc(8193-valid.length,32)]));
}return s};
fs.readSync=(...args)=>{if(fs.readlinkSync('/proc/self/fd/'+args[0])===file)reads++;return read(...args)};syncBuiltinESMExports();
const {readControl}=await import(process.argv[1]);let code=null;
try{readControl(process.argv[4])}catch(e){code=e.code}console.log(JSON.stringify({code,reads}));`;
for (const which of ['owner', 'oversize', 'grow']) {
const valid = readFileSync(file);
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script,
new URL('../src/state.mjs', import.meta.url).href, file, which, dir], { encoding: 'utf8' });
writeFileSync(file, valid);
assert.equal(result.status, 0, result.stderr);
const answer = JSON.parse(result.stdout); assert.equal(answer.code, 'processing-state-invalid', which);
if (which !== 'grow') assert.equal(answer.reads, 0, `${which}: refuse before reading bytes`);
else assert.ok(answer.reads > 0);
}
});
test('canonical root aliases address one switch and never create independent control state', t => {
const a = root(t), b = root(t), alias = join(b, 'alias');
api.initializeControl(a); symlinkSync(a, alias, 'dir');
assert.equal(api.initializeControl(alias).generation, api.readControl(a).generation);
const halted = api.transition(alias, { ...actor, halted: true });
assert.equal(api.readControl(a).generation, halted.generation);
assert.equal(api.readControl(a).halted, true);
});