// @ts-nocheck import { test, mock } from 'node:test'; import assert from 'node:assert/strict'; import { JobManager, JOB_TIMEOUT_MS, MAX_FINISHED_JOBS, pruneFinishedJobs } from '../src/classes/JobManager'; import { makeChromeMock } from './chrome-mock'; // Drain pending microtasks (finalize() chains several awaits). setImmediate is // never mocked here, so it fires after the current microtask queue is empty. const flush = () => new Promise(r => setImmediate(r)); function makeJobs(specs) { const map = new Map(); for (const s of specs) map.set(s.id, { __timer: null, ...s }); return map; } test('pruneFinishedJobs: keeps everything when under the cap', () => { const jobs = makeJobs([ { id: 'a', status: 'done', finishedAt: 1 }, { id: 'b', status: 'done', finishedAt: 2 }, ]); const evicted = pruneFinishedJobs(jobs, 5); assert.deepEqual(evicted, []); assert.equal(jobs.size, 2); }); test('pruneFinishedJobs: evicts the oldest finished jobs beyond the cap', () => { const jobs = makeJobs([ { id: 'old', status: 'done', finishedAt: 10 }, { id: 'mid', status: 'error', finishedAt: 20 }, { id: 'new', status: 'done', finishedAt: 30 }, ]); const evicted = pruneFinishedJobs(jobs, 2); assert.deepEqual(evicted, ['old']); assert.deepEqual([...jobs.keys()], ['mid', 'new']); }); test('pruneFinishedJobs: never evicts running jobs, even past the cap', () => { const jobs = makeJobs([ { id: 'run1', status: 'running' }, { id: 'run2', status: 'running' }, { id: 'f1', status: 'done', finishedAt: 1 }, { id: 'f2', status: 'done', finishedAt: 2 }, { id: 'f3', status: 'done', finishedAt: 3 }, ]); pruneFinishedJobs(jobs, 1); // both running kept; only newest finished (f3) kept assert.deepEqual([...jobs.keys()].sort(), ['f3', 'run1', 'run2']); }); test('pruneFinishedJobs: clears the timer of every evicted job (no interval leak)', () => { const clear = mock.fn(); const jobs = makeJobs([ { id: 'a', status: 'done', finishedAt: 1, __timer: 101 }, { id: 'b', status: 'done', finishedAt: 2, __timer: 102 }, { id: 'c', status: 'done', finishedAt: 3, __timer: 103 }, ]); pruneFinishedJobs(jobs, 1, clear); const clearedArgs = clear.mock.calls.map(c => c.arguments[0]); assert.ok(clearedArgs.includes(101)); assert.ok(clearedArgs.includes(102)); assert.ok(!clearedArgs.includes(103)); // surviving job's timer untouched }); test('pruneFinishedJobs: treats missing finishedAt as oldest (evicted first)', () => { const jobs = makeJobs([ { id: 'nofin', status: 'done' }, { id: 'has', status: 'done', finishedAt: 5 }, ]); pruneFinishedJobs(jobs, 1); assert.deepEqual([...jobs.keys()], ['has']); }); test('pruneFinishedJobs: defaults to MAX_FINISHED_JOBS and bounds the retained set', () => { const specs = Array.from({ length: MAX_FINISHED_JOBS + 25 }, (_, i) => ({ id: `j${i}`, status: 'done', finishedAt: i, })); const jobs = makeJobs(specs); pruneFinishedJobs(jobs); assert.equal(jobs.size, MAX_FINISHED_JOBS); // the 25 oldest (j0..j24) are gone; newest survive assert.equal(jobs.has('j0'), false); assert.equal(jobs.has('j24'), false); assert.equal(jobs.has('j25'), true); }); test('JobManager: a resolving runner finishes the job and clears its timers', async () => { globalThis.chrome = makeChromeMock(); const mgr = new JobManager(); const { jobId } = await mgr.start('demo', {}, async () => ({ ok: 1 })); await flush(); const job = await mgr.status({ jobId }); assert.equal(job.status, 'done'); assert.deepEqual(job.result, { ok: 1 }); assert.equal(job.percent, 100); assert.equal(job.__timer ?? null, null); assert.equal(job.__watchdog ?? null, null); }); test('JobManager: watchdog finalizes a hung runner and stops the persist timer', async () => { mock.timers.enable({ apis: ['setInterval', 'setTimeout'] }); globalThis.chrome = makeChromeMock(); const mgr = new JobManager(); // Runner never settles — only the watchdog can finish this job. const { jobId } = await mgr.start('hang', {}, () => new Promise(() => {})); mock.timers.tick(JOB_TIMEOUT_MS); await flush(); const job = await mgr.status({ jobId }); assert.equal(job.status, 'error'); assert.match(job.error, /timed out/); assert.equal(job.cancelRequested, true); assert.equal(job.__timer ?? null, null); mock.timers.reset(); }); test('JobManager: a runner that settles after the watchdog cannot resurrect the job', async () => { mock.timers.enable({ apis: ['setInterval', 'setTimeout'] }); globalThis.chrome = makeChromeMock(); const mgr = new JobManager(); let settle; const { jobId } = await mgr.start('late', {}, () => new Promise(r => { settle = r; })); mock.timers.tick(JOB_TIMEOUT_MS); await flush(); settle({ tooLate: true }); // runner resolves only after the watchdog fired await flush(); const job = await mgr.status({ jobId }); assert.equal(job.status, 'error'); // stayed error — finalize() ran exactly once assert.equal(job.result, null); mock.timers.reset(); }); test('JobManager: persisted set keeps running jobs even past the finished cap', async () => { mock.timers.enable({ apis: ['setInterval', 'setTimeout'] }); globalThis.chrome = makeChromeMock(); const mgr = new JobManager(); await mgr.start('runner', {}, () => new Promise(() => {})); // stays running for (let i = 0; i < MAX_FINISHED_JOBS + 5; i++) { await mgr.start(`done${i}`, {}, async () => i); } await flush(); const stored = globalThis.chrome.storage.local._store.recentJobs; const running = stored.filter(j => j.status === 'running'); assert.equal(running.length, 1, 'the running job must never be evicted from storage'); assert.ok(stored.length <= MAX_FINISHED_JOBS + 1, 'finished jobs stay capped'); mock.timers.reset(); });