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.
166 lines
9.5 KiB
JavaScript
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);
|
|
});
|