import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawn } from 'node:child_process'; import { once } from 'node:events'; import { createBriefOutbox, briefWeekKey } from '../server/brief-outbox.mjs'; import { createBriefSender } from '../server/brief-delivery.mjs'; import { backupDataVolume, restoreDataVolume } from '../server/backup-data-volume.mjs'; function fixture(t) { const root=mkdtempSync(join(tmpdir(),'brief-outbox-')),directory=join(root,'data'); t.after(()=>rmSync(root,{recursive:true,force:true})); return {root,directory,outbox:createBriefOutbox(directory)}; } function sender(outbox, overrides={}) { return createBriefSender({outbox,clock:()=>new Date('2026-09-07T05:00:00Z'), store:{listUsers:()=>[{email:'test@example.test'}],getData:()=>null}, buildBrief:()=>({bos:false,hafta:'07 Eylül 2026'}),text:()=> 'Test',html:()=> '

Test

', telegramConfigured:()=>false,sendTelegram:async()=>true,emailConfigured:()=>true, sendEmail:async()=>({id:'synthetic-receipt'}),warn:()=>{},...overrides}); } test('accepted delivery survives reopening and does not send again; weeks and owners are isolated',async t=>{ const {outbox,directory}=fixture(t);let calls=0; const transport={sendEmail:async()=>{calls++;return {id:'receipt'};}}; assert.equal((await sender(outbox,transport)(async()=>[])).gonderilen,1); const again=await sender(createBriefOutbox(directory),transport)(async()=>[]); assert.equal(calls,1);assert.equal(again.atlanan,1); const next=sender(outbox,{...transport,clock:()=>new Date('2026-09-14T05:00:00Z')}); await next(async()=>[]);assert.equal(calls,2); const other=outbox.prepare('2026-09-07','other@example.test',[{text:'Test'}]); assert.equal(outbox.claim(other.id,0).state,'sending'); }); test('concurrent senders cannot call the provider for the same message twice',async t=>{ const {outbox,directory}=fixture(t);let release; const gate=new Promise(resolve=>{release=resolve;});let entered; const ready=new Promise(resolve=>{entered=resolve;});let calls=0; const transport={sendEmail:async()=>{calls++;entered();await gate;return {id:'receipt'};}}; const first=sender(outbox,transport)(async()=>[]); await ready; const second=await sender(createBriefOutbox(directory),transport)(async()=>[]); assert.equal(second.bekleyen,1);assert.equal(calls,1); release();await first; assert.equal(outbox.inspect()[0].state,'accepted'); }); test('ambiguous provider failure and changed payload remain held without automatic replay',async t=>{ const {outbox}=fixture(t);let calls=0; const transport={sendEmail:async()=>{calls++;throw new Error('Connection lost after send');}}; assert.equal((await sender(outbox,transport)(async()=>[])).hata,1); const retry=await sender(outbox,transport)(async()=>[]); assert.equal(retry.bekleyen,1);assert.equal(calls,1); const changed=await sender(outbox,{text:()=> 'Changed'})(async()=>[]); assert.equal(changed.hata,1);assert.equal(changed.bekleyen,1); assert.equal(outbox.inspect()[0].state,'uncertain'); }); test('feed failure creates no completion record and a subsequent fetch can proceed',async t=>{ const {outbox}=fixture(t),send=sender(outbox); await assert.rejects(send(async()=>{throw new Error('Feed down');}),/Feed down/); assert.deepEqual(outbox.inspect(),[]); assert.equal((await send(async()=>[])).gonderilen,1); }); test('SIGKILL after claiming leaves an uncertain message after lease expiry, never a fresh send',async t=>{ const {directory,outbox}=fixture(t); const moduleUrl=new URL('../server/brief-outbox.mjs',import.meta.url).href; const code=`import {createBriefOutbox} from ${JSON.stringify(moduleUrl)}; const box=createBriefOutbox(process.argv[1]); const batch=box.prepare('2026-09-07','test@example.test',[{text:'Test'}]); box.claim(batch.id,0,1000);process.stdout.write(batch.id+'\\n');setInterval(()=>{},1000);`; const child=spawn(process.execPath,['--input-type=module','-e',code,directory],{stdio:['ignore','pipe','pipe']}); t.after(()=>{if(child.exitCode===null)child.kill('SIGKILL');}); const output=await Promise.race([once(child.stdout,'data'),once(child,'exit').then(()=>{throw new Error('Child exited before claim');})]); const batch=output[0].toString().trim(); const stopped=once(child,'exit');child.kill('SIGKILL');await stopped; assert.equal(outbox.claim(batch,0,2000).state,'busy'); assert.equal(outbox.claim(batch,0,121001).state,'uncertain'); assert.equal(outbox.claim(batch,0,999999).state,'uncertain'); }); test('full-volume backup restores accepted message records and prevents replay',async t=>{ const {root,directory,outbox}=fixture(t); await sender(outbox)(async()=>[]); const backup=join(root,'backup'),restored=join(root,'restored'); const manifest=await backupDataVolume(directory,backup,{offline:true}); assert.ok(manifest.files.some(file=>file.name==='brief.sqlite'&&file.sqlite)); await restoreDataVolume(backup,restored); const send=sender(createBriefOutbox(restored),{sendEmail:async()=>{throw new Error('Must not replay');}}); const result=await send(async()=>[]);assert.equal(result.atlanan,1);assert.equal(result.hata,0); }); test('weekly id uses Monday in Istanbul even while UTC is still Sunday',()=>{ assert.equal(briefWeekKey(new Date('2026-09-06T20:59:59Z')),'2026-08-31'); assert.equal(briefWeekKey(new Date('2026-09-06T21:00:00Z')),'2026-09-07'); });