import test from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { once } from 'node:events'; 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/effects.mjs').catch(e => { if (e.code === 'ERR_MODULE_NOT_FOUND') return {}; throw e; }); function fixture(t) { const root = mkdtempSync(join(tmpdir(), 'processing-effect-')); initializeControl(root); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } const actor = { business: 'demo', human: 'jason', halted: true }; test('halt aborts an active local request and prevents a subsequent effect after halt/resume', { timeout: 4000 }, async t => { assert.equal(typeof api.withProcessing, 'function'); const root = fixture(t); let started; const ready = new Promise(resolve => { started = resolve; }); const work = api.withProcessing(root, async scope => { await new Promise(resolve => { scope.signal.addEventListener('abort', resolve, { once: true }); started(); }); scope.guard(); }); await ready; const halted = transition(root, actor); transition(root, { ...actor, halted: false, generation: halted.generation }); await assert.rejects(work, e => e.code === 'processing-generation-changed'); }); test('processing scopes isolate roots and cancel only the registered child group', async t => { assert.equal(typeof api.withProcessing, 'function'); const a = fixture(t), b = fixture(t); let admitted; const ready = new Promise(resolve => { admitted = resolve; }); const task = api.withProcessing(a, async scope => { const child = spawn(process.execPath, ['-e', "process.on('SIGTERM',()=>process.exit(0));console.log('ready');setInterval(()=>{},1000)"], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] }); const exit = once(child, 'exit'); t.after(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }); scope.track(child); await once(child.stdout, 'data'); admitted(); const outcome = await exit; assert.deepEqual(outcome, [0, null]); scope.guard(); }); await ready; transition(b, actor); await api.withProcessing(a, async scope => scope.guard()); transition(a, actor); await assert.rejects(task, e => e.code === 'processing-halted'); }); test('halted scopes never invoke their body and unscoped human work has no processing context', async t => { assert.equal(typeof api.withProcessing, 'function'); const root = fixture(t); transition(root, actor); let called = false; await assert.rejects(api.withProcessing(root, async () => { called = true; }), e => e.code === 'processing-halted'); assert.equal(called, false); assert.equal(api.currentProcessing(), undefined); }); test('host git is stopped promptly by the active scope rather than waiting for its long deadline', async t => { const { mkdirSync, writeFileSync } = await import('node:fs'); const { runGit, gitEnv } = await import('../../seat/src/checkout.mjs'); const root = fixture(t), bin = join(root, 'bin'); mkdirSync(bin); writeFileSync(join(bin, 'git'), `#!${process.execPath}\nprocess.on('SIGTERM',()=>process.exit(0));setInterval(()=>{},1000);\n`, { mode: 0o755 }); const begin = Date.now(); const task = api.withProcessing(root, async () => runGit(['status'], { ...gitEnv(), PATH: bin + ':' + process.env.PATH }, { timeoutMs: 10000, graceMs: 100 })); await new Promise(resolve => setTimeout(resolve, 100)); transition(root, actor); await assert.rejects(task, e => e.code === 'processing-halted'); assert.ok(Date.now() - begin < 2000, 'the halt cancels the actual git process'); }); test('a child leader exiting does not erase an unverified surviving group receipt', async t => { const { listRuntimes } = await import('../src/runtimes.mjs'); const root = fixture(t); let child; await api.withProcessing(root, async scope => { const script = `const {spawn}=require('node:child_process');spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:'ignore'});console.log('ready');setTimeout(()=>process.exit(0),80);`; child = spawn(process.execPath, ['-e', script], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] }); t.after(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }); const closed = once(child, 'close'); scope.track(child); await once(child.stdout, 'data'); await closed; }); const records = listRuntimes(root); assert.equal(records.length, 1, 'unknown group remains discoverable to the independent halt sweep'); assert.equal(records[0].pid, child.pid); assert.equal(records[0].group, true); });