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); });