test: agregar pruebas unitarias para RemindersService y CommandService
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>pull/1/head
							parent
							
								
									5c49f16c4e
								
							
						
					
					
						commit
						0f96c27928
					
				| @ -0,0 +1,93 @@ | |||||||
|  | import { describe, it, beforeEach, expect } from 'bun:test'; | ||||||
|  | import { Database } from 'bun:sqlite'; | ||||||
|  | import { initializeDatabase } from '../../../src/db'; | ||||||
|  | import { Migrator } from '../../../src/db/migrator'; | ||||||
|  | import { CommandService } from '../../../src/services/command'; | ||||||
|  | 
 | ||||||
|  | describe('CommandService - configurar recordatorios', () => { | ||||||
|  |   let memdb: Database; | ||||||
|  |   const SENDER = '34600123456'; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     process.env.NODE_ENV = 'test'; | ||||||
|  |     process.env.TZ = 'Europe/Madrid'; | ||||||
|  | 
 | ||||||
|  |     memdb = new Database(':memory:'); | ||||||
|  |     initializeDatabase(memdb); | ||||||
|  |     await Migrator.migrateToLatest(memdb); | ||||||
|  | 
 | ||||||
|  |     // Inyectar DB
 | ||||||
|  |     (CommandService as any).dbInstance = memdb; | ||||||
|  | 
 | ||||||
|  |     // Limpiar tablas
 | ||||||
|  |     memdb.exec(`DELETE FROM response_queue;`); | ||||||
|  |     memdb.exec(`DELETE FROM user_preferences;`); | ||||||
|  |     memdb.exec(`DELETE FROM users;`); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   async function runCmd(msg: string) { | ||||||
|  |     return await CommandService.handle({ | ||||||
|  |       sender: SENDER, | ||||||
|  |       groupId: '123@g.us', | ||||||
|  |       message: msg, | ||||||
|  |       mentions: [] | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function getPref(): { freq: string; time: string | null } | null { | ||||||
|  |     const row = memdb.prepare(`SELECT reminder_freq AS freq, reminder_time AS time FROM user_preferences WHERE user_id = ?`).get(SENDER) as any; | ||||||
|  |     if (!row) return null; | ||||||
|  |     return { freq: String(row.freq), time: row.time ? String(row.time) : null }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   it('configurar daily guarda preferencia y responde confirmación', async () => { | ||||||
|  |     const res = await runCmd('/t configurar daily'); | ||||||
|  |     expect(res).toHaveLength(1); | ||||||
|  |     expect(res[0].recipient).toBe(SENDER); | ||||||
|  |     expect(res[0].message).toContain('✅ Recordatorios: diario'); | ||||||
|  | 
 | ||||||
|  |     const pref = getPref(); | ||||||
|  |     expect(pref).not.toBeNull(); | ||||||
|  |     expect(pref!.freq).toBe('daily'); | ||||||
|  |     expect(pref!.time).toBe('08:30'); // default
 | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('configurar weekly guarda preferencia y responde confirmación', async () => { | ||||||
|  |     const res = await runCmd('/t configurar weekly'); | ||||||
|  |     expect(res).toHaveLength(1); | ||||||
|  |     expect(res[0].message).toContain('semanal (lunes 08:30)'); | ||||||
|  | 
 | ||||||
|  |     const pref = getPref(); | ||||||
|  |     expect(pref).not.toBeNull(); | ||||||
|  |     expect(pref!.freq).toBe('weekly'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('configurar off guarda preferencia y responde confirmación', async () => { | ||||||
|  |     const res = await runCmd('/t configurar off'); | ||||||
|  |     expect(res).toHaveLength(1); | ||||||
|  |     expect(res[0].message).toContain('apagado'); | ||||||
|  | 
 | ||||||
|  |     const pref = getPref(); | ||||||
|  |     expect(pref).not.toBeNull(); | ||||||
|  |     expect(pref!.freq).toBe('off'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('configurar con opción inválida devuelve uso correcto y no escribe en DB', async () => { | ||||||
|  |     const res = await runCmd('/t configurar foo'); | ||||||
|  |     expect(res).toHaveLength(1); | ||||||
|  |     expect(res[0].message).toContain('Uso: /t configurar daily|weekly|off'); | ||||||
|  | 
 | ||||||
|  |     const pref = getPref(); | ||||||
|  |     expect(pref).toBeNull(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('upsert idempotente: cambiar de daily a off actualiza la fila existente', async () => { | ||||||
|  |     await runCmd('/t configurar daily'); | ||||||
|  |     let pref = getPref(); | ||||||
|  |     expect(pref!.freq).toBe('daily'); | ||||||
|  | 
 | ||||||
|  |     await runCmd('/t configurar off'); | ||||||
|  |     pref = getPref(); | ||||||
|  |     expect(pref!.freq).toBe('off'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,170 @@ | |||||||
|  | import { describe, it, beforeEach, expect } from 'bun:test'; | ||||||
|  | import { Database } from 'bun:sqlite'; | ||||||
|  | import { initializeDatabase } from '../../../src/db'; | ||||||
|  | import { Migrator } from '../../../src/db/migrator'; | ||||||
|  | import { TaskService } from '../../../src/tasks/service'; | ||||||
|  | import { RemindersService } from '../../../src/services/reminders'; | ||||||
|  | import { ResponseQueue } from '../../../src/services/response-queue'; | ||||||
|  | 
 | ||||||
|  | function toIso(dt: Date): string { | ||||||
|  |   return dt.toISOString().replace('T', ' ').replace('Z', ''); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | describe('RemindersService', () => { | ||||||
|  |   let memdb: Database; | ||||||
|  |   const USER = '34600123456'; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     process.env.NODE_ENV = 'test'; | ||||||
|  |     process.env.TZ = 'Europe/Madrid'; | ||||||
|  | 
 | ||||||
|  |     memdb = new Database(':memory:'); | ||||||
|  |     initializeDatabase(memdb); | ||||||
|  |     // Migraciones para user_preferences
 | ||||||
|  |     return Migrator.migrateToLatest(memdb).then(() => { | ||||||
|  |       // Inyectar DB en servicios
 | ||||||
|  |       (TaskService as any).dbInstance = memdb; | ||||||
|  |       (RemindersService as any).dbInstance = memdb; | ||||||
|  |       (ResponseQueue as any).dbInstance = memdb; | ||||||
|  | 
 | ||||||
|  |       // Limpiar tablas entre tests por seguridad
 | ||||||
|  |       memdb.exec(`DELETE FROM response_queue;`); | ||||||
|  |       memdb.exec(`DELETE FROM user_preferences;`); | ||||||
|  |       memdb.exec(`DELETE FROM users;`); | ||||||
|  |       memdb.exec(`DELETE FROM tasks;`); | ||||||
|  |       memdb.exec(`DELETE FROM task_assignments;`); | ||||||
|  | 
 | ||||||
|  |       // Asegurar usuario
 | ||||||
|  |       memdb.exec(` | ||||||
|  |         INSERT INTO users (id, first_seen, last_seen) | ||||||
|  |         VALUES ('${USER}', '${toIso(new Date())}', '${toIso(new Date())}') | ||||||
|  |         ON CONFLICT(id) DO NOTHING; | ||||||
|  |       `);
 | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   function insertPref(freq: 'daily' | 'weekly' | 'off', time: string = '08:30', last: string | null = null) { | ||||||
|  |     memdb.prepare(` | ||||||
|  |       INSERT INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) | ||||||
|  |       VALUES (?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', 'now')) | ||||||
|  |       ON CONFLICT(user_id) DO UPDATE SET | ||||||
|  |         reminder_freq = excluded.reminder_freq, | ||||||
|  |         reminder_time = excluded.reminder_time, | ||||||
|  |         last_reminded_on = excluded.last_reminded_on, | ||||||
|  |         updated_at = excluded.updated_at | ||||||
|  |     `).run(USER, freq, time, last);
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function countQueued(): number { | ||||||
|  |     const row = memdb.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; | ||||||
|  |     return Number(row?.c || 0); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function getLastReminded(): string | null { | ||||||
|  |     const row = memdb.prepare(`SELECT last_reminded_on AS d FROM user_preferences WHERE user_id = ?`).get(USER) as any; | ||||||
|  |     return row?.d ?? null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   it('envía recordatorio diario cuando hora alcanzada y no enviado hoy', async () => { | ||||||
|  |     insertPref('daily', '08:30', null); | ||||||
|  |     // Lunes 2025-09-08 09:30 Europe/Madrid ≈ 07:30Z
 | ||||||
|  |     const now = new Date('2025-09-08T07:30:00.000Z'); | ||||||
|  | 
 | ||||||
|  |     // Crear 1 tarea asignada al usuario
 | ||||||
|  |     TaskService.createTask( | ||||||
|  |       { description: 'Tarea A', due_date: '2025-09-10', group_id: null, created_by: USER }, | ||||||
|  |       [{ user_id: USER, assigned_by: USER }] | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await RemindersService.runOnce(now); | ||||||
|  | 
 | ||||||
|  |     expect(countQueued()).toBe(1); | ||||||
|  |     expect(getLastReminded()).toBe('2025-09-08'); // YYYY-MM-DD en TZ
 | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('no duplica recordatorio diario en el mismo día', async () => { | ||||||
|  |     insertPref('daily', '08:30', null); | ||||||
|  |     const now = new Date('2025-09-08T07:40:00.000Z'); // ≥ 08:30 local
 | ||||||
|  | 
 | ||||||
|  |     TaskService.createTask( | ||||||
|  |       { description: 'Tarea B', due_date: '2025-09-11', group_id: null, created_by: USER }, | ||||||
|  |       [{ user_id: USER, assigned_by: USER }] | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await RemindersService.runOnce(now); | ||||||
|  |     expect(countQueued()).toBe(1); | ||||||
|  | 
 | ||||||
|  |     // Segunda ejecución el mismo día
 | ||||||
|  |     await RemindersService.runOnce(new Date('2025-09-08T08:00:00.000Z')); | ||||||
|  |     expect(countQueued()).toBe(1); // sin incremento
 | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('no envía diario antes de la hora configurada', async () => { | ||||||
|  |     insertPref('daily', '08:30', null); | ||||||
|  |     const now = new Date('2025-09-08T05:00:00.000Z'); // 07:00 local < 08:30
 | ||||||
|  | 
 | ||||||
|  |     TaskService.createTask( | ||||||
|  |       { description: 'Tarea C', due_date: '2025-09-12', group_id: null, created_by: USER }, | ||||||
|  |       [{ user_id: USER, assigned_by: USER }] | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await RemindersService.runOnce(now); | ||||||
|  |     expect(countQueued()).toBe(0); | ||||||
|  |     expect(getLastReminded()).toBeNull(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('no envía si el usuario no tiene tareas; no marca last_reminded_on', async () => { | ||||||
|  |     insertPref('daily', '08:30', null); | ||||||
|  |     const now = new Date('2025-09-08T07:40:00.000Z'); // ≥ 08:30 local
 | ||||||
|  | 
 | ||||||
|  |     await RemindersService.runOnce(now); | ||||||
|  |     expect(countQueued()).toBe(0); | ||||||
|  |     expect(getLastReminded()).toBeNull(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('weekly: solo envía en lunes y a la hora, no en martes', async () => { | ||||||
|  |     insertPref('weekly', '08:30', null); | ||||||
|  | 
 | ||||||
|  |     // Crear 1 tarea
 | ||||||
|  |     TaskService.createTask( | ||||||
|  |       { description: 'Tarea W', due_date: '2025-09-15', group_id: null, created_by: USER }, | ||||||
|  |       [{ user_id: USER, assigned_by: USER }] | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // Lunes (Mon) 2025-09-08 09:00 local (07:00Z) -> antes de hora: no envía
 | ||||||
|  |     await RemindersService.runOnce(new Date('2025-09-08T07:00:00.000Z')); | ||||||
|  |     expect(countQueued()).toBe(0); | ||||||
|  |     expect(getLastReminded()).toBeNull(); | ||||||
|  | 
 | ||||||
|  |     // Lunes 09:30 local (07:30Z) -> envía
 | ||||||
|  |     await RemindersService.runOnce(new Date('2025-09-08T07:30:00.000Z')); | ||||||
|  |     expect(countQueued()).toBe(1); | ||||||
|  |     expect(getLastReminded()).toBe('2025-09-08'); | ||||||
|  | 
 | ||||||
|  |     // Martes 2025-09-09 ≥ hora -> no debe enviar (weekly solo lunes)
 | ||||||
|  |     await RemindersService.runOnce(new Date('2025-09-09T07:40:00.000Z')); | ||||||
|  |     expect(countQueued()).toBe(1); // sin incremento
 | ||||||
|  |     // last_reminded_on permanece en lunes
 | ||||||
|  |     expect(getLastReminded()).toBe('2025-09-08'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('incluye “… y X más” cuando hay más de 10 tareas (tope de listado)', async () => { | ||||||
|  |     insertPref('daily', '08:30', null); | ||||||
|  |     const now = new Date('2025-09-08T07:40:00.000Z'); | ||||||
|  | 
 | ||||||
|  |     // Crear 12 tareas asignadas al usuario
 | ||||||
|  |     for (let i = 0; i < 12; i++) { | ||||||
|  |       TaskService.createTask( | ||||||
|  |         { description: `Tarea ${i + 1}`, due_date: '2025-09-20', group_id: null, created_by: USER }, | ||||||
|  |         [{ user_id: USER, assigned_by: USER }] | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await RemindersService.runOnce(now); | ||||||
|  |     expect(countQueued()).toBe(1); | ||||||
|  | 
 | ||||||
|  |     const row = memdb.prepare(`SELECT message FROM response_queue LIMIT 1`).get() as any; | ||||||
|  |     const msg: string = String(row?.message || ''); | ||||||
|  |     expect(msg.includes('… y 2 más')).toBe(true); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
					Loading…
					
					
				
		Reference in New Issue