import { describe, it, expect, beforeAll, beforeEach } from 'bun:test'; import { Database } from 'bun:sqlite'; import { initializeDatabase } from '../../../src/db'; import { TaskService } from '../../../src/tasks/service'; import { CommandService } from '../../../src/services/command'; function ymdInTZ(d: Date, tz: string = 'Europe/Madrid'): string { const parts = new Intl.DateTimeFormat('en-GB', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(d); const get = (t: string) => parts.find(p => p.type === t)?.value || ''; return `${get('year')}-${get('month')}-${get('day')}`; } function addDaysToYMD(ymd: string, days: number, tz: string = 'Europe/Madrid'): string { const [Y, M, D] = ymd.split('-').map(n => parseInt(n, 10)); const base = new Date(Date.UTC(Y, (M || 1) - 1, D || 1)); base.setUTCDate(base.getUTCDate() + days); return ymdInTZ(base, tz); } describe('CommandService - parser de fechas (hoy/mañana y formatos YYYY/YY-MM-DD)', () => { let memdb: Database; beforeAll(() => { memdb = new Database(':memory:'); initializeDatabase(memdb); TaskService.dbInstance = memdb; CommandService.dbInstance = memdb; }); beforeEach(() => { process.env.NODE_ENV = 'test'; process.env.TZ = 'Europe/Madrid'; memdb.exec('DELETE FROM task_assignments; DELETE FROM tasks;'); }); it('interpreta "hoy" como due_date de hoy en la TZ configurada', async () => { const sender = '600111222'; const ctx = { sender, groupId: `${sender}@s.whatsapp.net`, // DM message: '/t n prueba hoy', mentions: [] as string[], }; await CommandService.handle(ctx); const row = memdb.prepare(`SELECT id, due_date FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; const todayYMD = ymdInTZ(new Date(), process.env.TZ); expect(row).toBeTruthy(); expect(String(row.due_date)).toBe(todayYMD); }); it('interpreta "mañana" (con o sin acento) como due_date de mañana', async () => { const sender = '600111222'; const ctx = { sender, groupId: `${sender}@s.whatsapp.net`, // DM message: '/t n prueba mañana', mentions: [] as string[], }; await CommandService.handle(ctx); const row = memdb.prepare(`SELECT id, due_date FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; const todayYMD = ymdInTZ(new Date(), process.env.TZ); const tomorrowYMD = addDaysToYMD(todayYMD, 1, process.env.TZ); expect(row).toBeTruthy(); expect(String(row.due_date)).toBe(tomorrowYMD); }); it('elige la última fecha futura cuando hay varias (mezcla YYYY-MM-DD y tokens)', async () => { const sender = '600111222'; const ctx = { sender, groupId: `${sender}@s.whatsapp.net`, // DM // contiene pasado (2020-01-01), hoy (futuro válido) y una futura explícita, y termina con "mañana" message: '/t n mezcla 2020-01-01 hoy 2099-01-01 mañana', mentions: [] as string[], }; await CommandService.handle(ctx); const row = memdb.prepare(`SELECT id, due_date FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; const todayYMD = ymdInTZ(new Date(), process.env.TZ); const tomorrowYMD = addDaysToYMD(todayYMD, 1, process.env.TZ); expect(row).toBeTruthy(); expect(String(row.due_date)).toBe(tomorrowYMD); }); it('acepta formato YY-MM-DD y normaliza a YYYY-MM-DD (pivot en 2000s)', async () => { const sender = '600111222'; const ctx = { sender, groupId: `${sender}@s.whatsapp.net`, message: '/t n con corto 25-12-19', mentions: [] as string[], }; await CommandService.handle(ctx); const row = memdb.prepare(`SELECT id, due_date FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; expect(row).toBeTruthy(); expect(String(row.due_date)).toBe('2025-12-19'); }); it('acepta formato YY-MM-DD con ceros y futuro lejano seguro (30-01-05 → 2030-01-05)', async () => { const sender = '600111222'; const ctx = { sender, groupId: `${sender}@s.whatsapp.net`, message: '/t n con corto 30-01-05', mentions: [] as string[], }; await CommandService.handle(ctx); const row = memdb.prepare(`SELECT id, due_date FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; expect(row).toBeTruthy(); expect(String(row.due_date)).toBe('2030-01-05'); }); it('rechaza formatos no permitidos y no establece due_date', async () => { const sender = '600111222'; const invalids = [ '/t n invalida 25/12/30', // separador no permitido '/t n invalida 2025/12/01', // separador no permitido '/t n invalida 2025-2-01', // mes 1 dígito '/t n invalida 2025-02-3', // día 1 dígito '/t n invalida 2025-13-01', // mes inválido '/t n invalida 2025-00-10', // mes inválido '/t n invalida 2025-02-30', // día inválido calendario '/t n invalida 25-12', // dos partes no permitido '/t n invalida 12-25', // dos partes no permitido '/t n invalida 2025-1-1', // sin padding ]; for (const msg of invalids) { memdb.exec('DELETE FROM task_assignments; DELETE FROM tasks;'); await CommandService.handle({ sender, groupId: `${sender}@s.whatsapp.net`, message: msg, mentions: [] as string[], }); const row = memdb.prepare(`SELECT id, due_date FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; expect(row).toBeTruthy(); expect(row.due_date).toBeNull(); } }); });