fix: persist weekly brief delivery claims and accepted messages

This commit is contained in:
2026-09-06 14:32:56 +03:00
parent d3a52caf2b
commit 99a7f2f571
6 changed files with 213 additions and 10 deletions
+99
View File
@@ -0,0 +1,99 @@
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:()=> '<p>Test</p>',
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');
});