'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); const { ScraperService } = require('../service'); test('status and watchdog read the checkpoint for each source layout', async t => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mc-checkpoint-location-')); t.after(() => fs.rm(root, { recursive: true, force: true })); for (const [source, id, folder] of [ ['weebcentral', 'W1', '.mcscraper-work-W1'], ['mangadex', 'D1', '.mcscraper-work-D1'], ['mangapill', 'P1', '.mcscraper-work-mangapill-P1'], ]) { const dir = path.join(root, folder); await fs.mkdir(dir); await fs.writeFile(path.join(dir, 'job.json'), JSON.stringify({ status: 'downloading', publicationMode: 'incremental', jobCompleted: 3, total: 10, updated: '2026-10-14T12:00:00Z', current: { downloadedPages: 4 }, })); const service = Object.create(ScraperService.prototype); service.libraryRoot = root; const job = await service.jobState({ source, seriesId: id, kind: 'download' }); assert.equal(job.completed, 3, source); assert.equal(job.current.downloadedPages, 4, source); } }); function stallService(job) { const service = new ScraperService({ stallMs: 100 }); service.state = { paused: false, sourcePaused: {}, currentIds: { a: 'job' }, incidents: [], queue: [{ id: 'job', source: 'a', title: 'Manga', status: 'downloading' }] }; service.jobState = async () => job.value; service.event = async () => {}; return service; } test('stall check leaves progressing and deliberately paused jobs alone', async () => { const job = { value: { completed: 0, current: { downloadedPages: 1 } } }; const service = stallService(job), item = service.state.queue[0]; await service.checkStalls(0); job.value = { completed: 0, current: { downloadedPages: 2 } }; await service.checkStalls(500000); service.state.paused = true; await service.checkStalls(1000000); assert.equal(item.stopReason, undefined); // Paused time does not count: the clock restarts on resume. service.state.paused = false; await service.checkStalls(1000050); assert.equal(item.stopReason, undefined); await service.checkStalls(1190000); assert.equal(item.stopReason, 'recovering'); assert.equal(item.stallRecoveries, 1); assert.equal(service.state.incidents[0].failure.kind, 'stall'); }); test('a second stall quarantines the job', async () => { const service = stallService({ value: { completed: 2, total: 5 } }), item = service.state.queue[0]; item.stallRecoveries = 1; await service.checkStalls(0); await service.checkStalls(1000000); assert.equal(item.stopReason, 'quarantined'); assert.equal(item.failure.kind, 'stall'); }); test('missing checkpoint does not trigger a false stall recovery', async () => { const service = stallService({ value: null }); for (const now of [0, 1000000, 2000000]) await service.checkStalls(now); assert.equal(service.state.queue[0].stopReason, undefined); assert.equal(service.state.incidents.length, 0); });