import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, writeFileSync, symlinkSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; import { randomUUID } from 'node:crypto'; import { makeRoot } from './helpers.mjs'; import { ensureJournal, requestStop, stopRequested, stopPath } from '../src/journal.mjs'; const api = await import('../src/journal.mjs'); function fixture() { const dir = join(makeRoot(), 'journal'); ensureJournal(dir); return dir; } test('resume clears only its own generation-tagged Discord STOP and keeps manual or newer brakes', () => { assert.equal(typeof api.requestProcessingStop, 'function'); const dir = fixture(), generation = randomUUID(); api.requestProcessingStop(dir, generation); const first = readFileSync(stopPath(dir)); api.requestProcessingStop(dir, generation); assert.deepEqual(readFileSync(stopPath(dir)), first); assert.equal(api.clearProcessingStop(dir, randomUUID()), false); assert.equal(api.clearProcessingStop(dir, generation), true); assert.equal(stopRequested(dir), false); api.requestProcessingStop(dir, generation); requestStop(dir, 'cli stop'); const mixed = readFileSync(stopPath(dir)); assert.equal(api.clearProcessingStop(dir, generation), false); assert.deepEqual(readFileSync(stopPath(dir)), mixed); }); test('unreadable, malformed and symlink STOP never get removed or followed by processing recovery', () => { assert.equal(typeof api.requestProcessingStop, 'function'); const dir = fixture(), generation = randomUUID(); writeFileSync(stopPath(dir), 'unreadable operator brake\n', { mode: 0o600 }); assert.equal(api.clearProcessingStop(dir, generation), false); unlinkSync(stopPath(dir)); const outside = join(dir, 'outside'); writeFileSync(outside, 'untouched', { mode: 0o600 }); symlinkSync(outside, stopPath(dir)); assert.equal(api.clearProcessingStop(dir, generation), false); assert.throws(() => api.requestProcessingStop(dir, generation)); assert.equal(readFileSync(outside, 'utf8'), 'untouched'); }); test('coordinator halt writes the Discord brake and resume clears only its own stopped runtime', async () => { const { initializeControl } = await import('../../processing-control/src/state.mjs'); const { registerRuntime, updateRuntime } = await import('../../processing-control/src/runtimes.mjs'); const { createProcessing } = await import('../../processing-control/src/coordinator.mjs'); const { discordProcessingHooks } = await import('../src/processing.mjs'); const root = makeRoot(); initializeControl(root); const dir = join(root, 'discord', 'test-seat'); ensureJournal(dir); const processing = createProcessing({ dataRoot: root, ...discordProcessingHooks(root) }); const first = registerRuntime(root, { kind: 'discord', business: 'instance', run: 'test-seat' }); updateRuntime(root, first.id, { phase: 'ended' }); const halted = await processing.halt({ business: 'demo', human: 'jason' }); assert.equal(halted.quiescent, true); assert.equal(stopRequested(dir), true); await processing.resume({ business: 'demo', human: 'jason', generation: halted.generation }); assert.equal(stopRequested(dir), false); const second = registerRuntime(root, { kind: 'discord', business: 'instance', run: 'test-seat' }); updateRuntime(root, second.id, { phase: 'ended' }); const again = await processing.halt({ business: 'demo', human: 'jason' }); requestStop(dir, 'cli stop'); const bytes = readFileSync(stopPath(dir)); await processing.resume({ business: 'demo', human: 'jason', generation: again.generation }); assert.deepEqual(readFileSync(stopPath(dir)), bytes); await processing.close(); });