import test from 'node:test'; import assert from 'node:assert/strict'; import { HistoryScheduler, type HistoryCaptureStore } from './scheduler.js'; function fixture(enabledAt='2026-10-30T13:00:00+08:00') { const captures:string[]=[],missed:string[]=[],failed:string[]=[]; let activates=0; const store:HistoryCaptureStore={ activate:async()=>{activates++;return{enabledAt};},dailyBatches:async()=>[], capture:async(month)=>{captures.push(month);return'captured';}, recordOutcome:async(month,status)=>{(status==='FAILED'?failed:missed).push(month);}, }; return {store,captures,missed,failed,activates:()=>activates}; } test('scheduler captures only in daily 23:55 minute and activation is persisted once',async()=>{ const f=fixture();let date=new Date('2026-10-31T23:54:59+08:00'); const scheduler=new HistoryScheduler(f.store,()=>date,()=>{}); await scheduler.tick();assert.deepEqual(f.captures,[]); date=new Date('2026-10-31T23:55:00+08:00');await scheduler.tick();assert.deepEqual(f.captures,['2026-10-31']);assert.equal(f.activates(),1); }); test('restart records missed eligible days without backfilling old live data or pre-activation periods',async()=>{ const f=fixture();const scheduler=new HistoryScheduler(f.store,()=>new Date('2026-11-02T08:00:00+08:00'),()=>{}); await scheduler.tick();assert.deepEqual(f.captures,[]);assert.deepEqual(f.missed,['2026-10-30','2026-10-31','2026-11-01']); const late=fixture('2026-10-31T23:55:10+08:00');await new HistoryScheduler(late.store,()=>new Date('2026-10-31T23:55:20+08:00'),()=>{}).tick(); assert.deepEqual(late.captures,[]);assert.deepEqual(late.missed,[]); }); test('completed days skip capture and existing failures are preserved after the window',async()=>{ const f=fixture();f.store.dailyBatches=async()=>[{day:'2026-10-30',status:'COMPLETE'}]; f.store.capture=async()=>{throw new Error('source unavailable');}; await new HistoryScheduler(f.store,()=>new Date('2026-10-31T23:55:20+08:00'),()=>{}).tick(); assert.deepEqual(f.missed,[]);assert.deepEqual(f.failed,['2026-10-31']); f.store.dailyBatches=async()=>[{day:'2026-10-30',status:'COMPLETE'},{day:'2026-10-31',status:'FAILED'}]; await new HistoryScheduler(f.store,()=>new Date('2026-10-31T23:56:00+08:00'),()=>{}).tick(); assert.deepEqual(f.failed,['2026-10-31']);assert.deepEqual(f.missed,[]); }); test('concurrent ticks and repeated start do not register duplicate jobs',async()=>{ const f=fixture();let release!:()=>void; f.store.capture=async()=>{f.captures.push('2026-10-31');await new Promise(r=>{release=r;});return'captured';}; const scheduler=new HistoryScheduler(f.store,()=>new Date('2026-10-31T23:55:20+08:00'),()=>{}); const stop=scheduler.start();scheduler.start(); await new Promise(r=>setImmediate(r));await scheduler.tick();assert.equal(f.captures.length,1); stop();release();await new Promise(r=>setImmediate(r)); }); test('consecutive dates in one month and across month boundary capture independently',async()=>{ const f=fixture(); for(const day of ['2026-10-30','2026-10-31','2026-11-01']) { await new HistoryScheduler(f.store,()=>new Date(`${day}T23:55:00+08:00`),()=>{}).tick(); } assert.deepEqual(f.captures,['2026-10-30','2026-10-31','2026-11-01']); });