You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			125 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			125 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
| import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
 | |
| import { Database } from 'bun:sqlite';
 | |
| import { initializeDatabase } from '../../../src/db';
 | |
| import { ResponseQueue } from '../../../src/services/response-queue';
 | |
| 
 | |
| let testDb: Database;
 | |
| let originalDbInstance: Database;
 | |
| 
 | |
| function toIso(dt: Date): string {
 | |
|   return dt.toISOString().replace('T', ' ').replace('Z', '');
 | |
| }
 | |
| 
 | |
| describe('ResponseQueue cleanup/retention', () => {
 | |
|   beforeAll(() => {
 | |
|     testDb = new Database(':memory:');
 | |
|     initializeDatabase(testDb);
 | |
|     originalDbInstance = (ResponseQueue as any).dbInstance;
 | |
|     (ResponseQueue as any).dbInstance = testDb;
 | |
|   });
 | |
| 
 | |
|   afterAll(() => {
 | |
|     (ResponseQueue as any).dbInstance = originalDbInstance;
 | |
|     testDb.close();
 | |
|   });
 | |
| 
 | |
|   beforeEach(() => {
 | |
|     testDb.exec('DELETE FROM response_queue');
 | |
|   });
 | |
| 
 | |
|   test('does not delete queued or processing items regardless of age', async () => {
 | |
|     const old = toIso(new Date(2000, 0, 1));
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`).run('u1','m1','queued', old);
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`).run('u2','m2','processing', old);
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status) VALUES (?,?,?)`).run('u3','m3','queued');
 | |
| 
 | |
|     const res = await (ResponseQueue as any).runCleanupOnce(new Date());
 | |
|     expect(res.totalDeleted).toBe(0);
 | |
| 
 | |
|     const counts = testDb.query(`SELECT status, COUNT(*) as c FROM response_queue GROUP BY status ORDER BY status`).all() as any[];
 | |
|     const map = Object.fromEntries(counts.map(r => [r.status, r.c]));
 | |
|     expect(map['queued']).toBe(2);
 | |
|     expect(map['processing']).toBe(1);
 | |
|   });
 | |
| 
 | |
|   test('deletes sent older than 14 days but keeps recent', async () => {
 | |
|     const now = new Date();
 | |
|     const days14Ago = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
 | |
|     const days13Ago = new Date(now.getTime() - 13 * 24 * 60 * 60 * 1000);
 | |
|     const thresholdExact = toIso(days14Ago); // exact boundary
 | |
| 
 | |
|     // exactly at threshold (should NOT delete because comparison is strict <)
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`)
 | |
|       .run('u1','m1','sent', thresholdExact);
 | |
|     // older than threshold
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`)
 | |
|       .run('u2','m2','sent', toIso(new Date(days14Ago.getTime() - 1000)));
 | |
|     // newer than threshold
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`)
 | |
|       .run('u3','m3','sent', toIso(days13Ago));
 | |
| 
 | |
|     const res = await (ResponseQueue as any).runCleanupOnce(now);
 | |
|     expect(res.deletedSent).toBe(1);
 | |
| 
 | |
|     const rows = testDb.query(`SELECT status, updated_at FROM response_queue WHERE status='sent' ORDER BY updated_at`).all() as any[];
 | |
|     expect(rows.length).toBe(2);
 | |
|     expect(rows[0].updated_at).toBe(thresholdExact);
 | |
|     expect(rows[1].updated_at).toBe(toIso(days13Ago));
 | |
|   });
 | |
| 
 | |
|   test('deletes failed older than 30 days but keeps newer', async () => {
 | |
|     const now = new Date();
 | |
|     const days30Ago = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
 | |
|     const days29Ago = new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000);
 | |
| 
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`)
 | |
|       .run('u1','m1','failed', toIso(new Date(days30Ago.getTime() - 1000)));
 | |
|     testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`)
 | |
|       .run('u2','m2','failed', toIso(days29Ago));
 | |
| 
 | |
|     const res = await (ResponseQueue as any).runCleanupOnce(now);
 | |
|     expect(res.deletedFailed).toBe(1);
 | |
| 
 | |
|     const rows = testDb.query(`SELECT COUNT(*) as c FROM response_queue WHERE status='failed'`).get() as any;
 | |
|     expect(rows.c).toBe(1);
 | |
|   });
 | |
| 
 | |
|   test('batch deletes large sets in multiple passes', async () => {
 | |
|     (ResponseQueue as any).CLEANUP_BATCH = 500; // reduce for test
 | |
|     const old = toIso(new Date(2000, 0, 1));
 | |
|     const total = 1200;
 | |
| 
 | |
|     const insert = testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`);
 | |
|     testDb.transaction(() => {
 | |
|       for (let i = 0; i < total; i++) {
 | |
|         insert.run(`u${i}`, `m${i}`, 'sent', old);
 | |
|       }
 | |
|     })();
 | |
| 
 | |
|     const res = await (ResponseQueue as any).runCleanupOnce(new Date());
 | |
|     expect(res.deletedSent).toBe(total);
 | |
|     const count = testDb.query(`SELECT COUNT(*) as c FROM response_queue WHERE status='sent'`).get() as any;
 | |
|     expect(count.c).toBe(0);
 | |
|   });
 | |
| 
 | |
|   test('concurrent cleanup calls do not overlap', async () => {
 | |
|     const old = toIso(new Date(2000, 0, 1));
 | |
|     for (let i = 0; i < 50; i++) {
 | |
|       testDb.prepare(`INSERT INTO response_queue (recipient, message, status, updated_at) VALUES (?,?,?,?)`)
 | |
|         .run(`u${i}`, `m${i}`, 'sent', old);
 | |
|     }
 | |
| 
 | |
|     // Trigger two cleanups concurrently
 | |
|     const [r1, r2] = await Promise.all([
 | |
|       (ResponseQueue as any).runCleanupOnce(new Date()),
 | |
|       (ResponseQueue as any).runCleanupOnce(new Date()),
 | |
|     ]);
 | |
| 
 | |
|     const total = (r1.totalDeleted || 0) + (r2.totalDeleted || 0);
 | |
|     expect(total).toBe(50); // no double-deletes
 | |
| 
 | |
|     const remain = testDb.query(`SELECT COUNT(*) as c FROM response_queue`).get() as any;
 | |
|     expect(remain.c).toBe(0);
 | |
|   });
 | |
| });
 |