import test from 'node:test'; import assert from 'node:assert/strict'; import { world } from './world.mjs'; import { tasksAdapter } from '../src/adapter.mjs'; import { createProcessing } from '../../processing-control/src/coordinator.mjs'; import { transition } from '../../processing-control/src/state.mjs'; const actor = { business: 'demo', human: 'jason', halted: true }; test('halted tracker startup makes no fetch and a later clear permits its ordinary startup', async t => { const w = await world(t), processing = createProcessing({ dataRoot: w.root }); let calls = 0; const adapter = await tasksAdapter({ trackers: { demo: { baseUrl: 'http://vikunja.test', project: w.project } }, autostart: false, fetch: (...args) => { calls++; return w.fake.fetch(...args); }, log: () => {} }) ({ broker: w.broker, credentials: w.credentials, businesses: w.businesses, processing }); t.after(async () => { await adapter.close(); await processing.close(); }); const halted = transition(w.root, actor); await assert.rejects(adapter.start('demo'), e => e.code === 'processing-halted'); assert.equal(calls, 0); transition(w.root, { ...actor, halted: false, generation: halted.generation }); await adapter.start('demo'); assert.ok(calls > 0); assert.equal(adapter.status()[0].state, 'ready'); transition(w.root, actor); calls = 0; await assert.rejects(adapter.tick('demo'), e => e.code === 'processing-halted'); await assert.rejects(adapter.reconcile('demo'), e => e.code === 'processing-halted'); assert.equal(calls, 0); }); test('halt aborts the tracker HTTP request already in progress', async t => { const w = await world(t), processing = createProcessing({ dataRoot: w.root }); let ready, aborted = false; const started = new Promise(resolve => { ready = resolve; }); const adapter = await tasksAdapter({ trackers: { demo: { baseUrl: 'http://vikunja.test', project: w.project } }, autostart: false, log: () => {}, fetch: (_url, init) => new Promise((_resolve, reject) => { init.signal.addEventListener('abort', () => { aborted = true; reject(Error('fixture abort')); }, { once: true }); ready(); }) })({ broker: w.broker, credentials: w.credentials, businesses: w.businesses, processing }); t.after(async () => { await adapter.close(); await processing.close(); }); const run = adapter.start('demo'); await started; const began = Date.now(); transition(w.root, actor); await run.catch(() => {}); assert.equal(aborted, true); assert.ok(Date.now() - began < 2000, 'halt must abort before the ordinary 15-second HTTP deadline'); }); test('an initially halted background startup remains retryable after clear', async t => { const w = await world(t), processing = createProcessing({ dataRoot: w.root }), ticks = []; const halted = transition(w.root, actor); let calls = 0; const adapter = await tasksAdapter({ trackers: { demo: { baseUrl: 'http://vikunja.test', project: w.project } }, log: () => {}, timers: { setInterval: fn => { ticks.push(fn); return { unref() {} }; }, clearInterval() {} }, fetch: (...args) => { calls++; return w.fake.fetch(...args); } }) ({ broker: w.broker, credentials: w.credentials, businesses: w.businesses, processing }); t.after(async () => { await adapter.close(); await processing.close(); }); await adapter.ready; assert.equal(calls, 0); assert.equal(adapter.status()[0].refused, 'processing-halted'); transition(w.root, { ...actor, halted: false, generation: halted.generation }); ticks[0](); for (let i = 0; i < 100 && adapter.status()[0].state !== 'ready'; i++) await new Promise(resolve => setTimeout(resolve, 10)); assert.equal(adapter.status()[0].state, 'ready'); assert.ok(calls > 0); });