import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { Database } from 'bun:sqlite'; import { initializeDatabase } from '../../../src/db'; import { CommandService } from '../../../src/services/command'; import { TaskService } from '../../../src/tasks/service'; import { GroupSyncService } from '../../../src/services/group-sync'; let memDb: Database; const testContextBase = { sender: '1234567890', groupId: 'test-group@g.us', mentions: [] as string[], }; beforeEach(() => { memDb = new Database(':memory:'); initializeDatabase(memDb); (CommandService as any).dbInstance = memDb; (TaskService as any).dbInstance = memDb; (GroupSyncService as any).dbInstance = memDb; GroupSyncService.activeGroupsCache.clear(); }); test('listar grupo por defecto con /t ver en grupo e incluir “… y X más”', async () => { // Insert group and cache it as active memDb.exec(` INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('test-group@g.us', 'test-community', 'Test Group', 1) `); GroupSyncService.activeGroupsCache.clear(); GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); // Crear 12 tareas sin asignados en el grupo for (let i = 1; i <= 12; i++) { TaskService.createTask({ description: `Task ${i}`, due_date: '2025-12-31', group_id: 'test-group@g.us', created_by: '1234567890', }); } const responses = await CommandService.handle({ sender: '1234567890', groupId: 'test-group@g.us', mentions: [], message: '/t ver' }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); expect(responses[0].message).toContain('Test Group'); // Debe indicar que hay 2 más (límite 10) expect(responses[0].message).toContain('… y 2 más'); // Debe mostrar “sin responsable” expect(responses[0].message).toContain('sin responsable'); }); test('listar “mis” por defecto en DM con /t ver', async () => { // Insert groups and cache them memDb.exec(` INSERT OR REPLACE INTO groups (id, community_id, name, active) VALUES ('test-group@g.us', 'test-community', 'Test Group', 1), ('group-2@g.us', 'test-community', 'Group 2', 1) `); GroupSyncService.activeGroupsCache.clear(); GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); GroupSyncService.activeGroupsCache.set('group-2@g.us', 'Group 2'); // Crear 2 tareas asignadas al usuario en distintos grupos const t1 = TaskService.createTask({ description: 'G1 Task', due_date: '2025-11-20', group_id: 'test-group@g.us', created_by: '1111111111', }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); const t2 = TaskService.createTask({ description: 'G2 Task', due_date: '2025-11-25', group_id: 'group-2@g.us', created_by: '2222222222', }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); const responses = await CommandService.handle({ sender: '1234567890', // Contexto de DM: usar un JID que NO sea de grupo groupId: '1234567890@s.whatsapp.net', mentions: [], message: '/t ver' }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); const msg = responses[0].message; expect(msg).toContain('Test Group'); expect(msg).toContain('Group 2'); expect(msg).toMatch(/- `\d{4}` G1 Task/); expect(msg).toMatch(/- `\d{4}` G2 Task/); }); test('completar tarea: camino feliz, ya completada y no encontrada', async () => { // Insertar grupo y cache memDb.exec(` INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('test-group@g.us', 'test-community', 'Test Group', 1) `); GroupSyncService.activeGroupsCache.clear(); GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); const taskId = TaskService.createTask({ description: 'Completar yo', due_date: '2025-10-10', group_id: 'test-group@g.us', created_by: '1111111111', }); // 1) Camino feliz let responses = await CommandService.handle({ sender: '1234567890', groupId: 'test-group@g.us', mentions: [], message: `/t x ${taskId}` }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); expect(responses[0].message).toMatch(/^✅ `\d{4}` _completada_/); // 2) Ya completada responses = await CommandService.handle({ sender: '1234567890', groupId: 'test-group@g.us', mentions: [], message: `/t x ${taskId}` }); expect(responses.length).toBe(1); expect(responses[0].message).toContain('_Ya estaba completada_'); // 3) No encontrada responses = await CommandService.handle({ sender: '1234567890', groupId: 'test-group@g.us', mentions: [], message: `/t x 999999` }); expect(responses.length).toBe(1); expect(responses[0].message).toContain('no encontrada'); }); test('ver sin en grupo activo: solo sin dueño y paginación', async () => { // Insertar grupo y cachearlo como activo memDb.exec(` INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('test-group@g.us', 'test-community', 'Test Group', 1) `); GroupSyncService.activeGroupsCache.clear(); GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); // 12 tareas sin dueño (para provocar “… y 2 más” con límite 10) for (let i = 1; i <= 12; i++) { TaskService.createTask({ description: `Unassigned ${i}`, due_date: '2025-12-31', group_id: 'test-group@g.us', created_by: '9999999999', }); } // 2 tareas asignadas (no deben aparecer en "ver sin") TaskService.createTask({ description: 'Asignada 1', due_date: '2025-11-01', group_id: 'test-group@g.us', created_by: '1111111111', }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); TaskService.createTask({ description: 'Asignada 2', due_date: '2025-11-02', group_id: 'test-group@g.us', created_by: '1111111111', }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); const responses = await CommandService.handle({ sender: '1234567890', groupId: 'test-group@g.us', mentions: [], message: '/t ver sin' }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); const msg = responses[0].message; expect(msg).toContain('Test Group'); expect(msg).toContain('sin responsable'); expect(msg).toContain('… y 2 más'); expect(msg).not.toContain('Asignada 1'); expect(msg).not.toContain('Asignada 2'); }); test('ver sin por DM devuelve instrucción', async () => { const responses = await CommandService.handle({ sender: '1234567890', // DM: no es un JID de grupo groupId: '1234567890@s.whatsapp.net', mentions: [], message: '/t ver sin' }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); expect(responses[0].message).toContain('Este comando se usa en grupos'); }); test('ver todos en grupo: “Tus tareas” + “Sin dueño (grupo actual)” con paginación en la sección sin dueño', async () => { // Insertar grupo y cachearlo como activo memDb.exec(` INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('test-group@g.us', 'test-community', 'Test Group', 1) `); GroupSyncService.activeGroupsCache.clear(); GroupSyncService.activeGroupsCache.set('test-group@g.us', 'Test Group'); // Tus tareas (2 asignadas al usuario) TaskService.createTask({ description: 'Mi Tarea 1', due_date: '2025-10-10', group_id: 'test-group@g.us', created_by: '2222222222', }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); TaskService.createTask({ description: 'Mi Tarea 2', due_date: '2025-10-11', group_id: 'test-group@g.us', created_by: '2222222222', }, [{ user_id: '1234567890', assigned_by: '2222222222' }]); // 12 sin dueño en el grupo (provoca “… y 2 más” en esa sección) for (let i = 1; i <= 12; i++) { TaskService.createTask({ description: `Sin dueño ${i}`, due_date: '2025-12-31', group_id: 'test-group@g.us', created_by: '9999999999', }); } const responses = await CommandService.handle({ sender: '1234567890', groupId: 'test-group@g.us', mentions: [], message: '/t ver todos' }); expect(responses.length).toBe(1); const msg = responses[0].message; expect(msg).toContain('Tus tareas'); expect(msg).toContain('Test Group'); expect(msg).toContain('sin responsable'); expect(msg).toContain('… y 2 más'); // paginación en la sección “sin dueño” }); test('ver todos por DM: “Tus tareas” + nota instructiva para ver sin dueño desde el grupo', async () => { // 2 tareas asignadas al usuario en cualquier grupo (no importa para este test) TaskService.createTask({ description: 'Mi Tarea A', due_date: '2025-11-20', group_id: 'group-1@g.us', created_by: '1111111111', }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); TaskService.createTask({ description: 'Mi Tarea B', due_date: '2025-11-21', group_id: 'group-2@g.us', created_by: '1111111111', }, [{ user_id: '1234567890', assigned_by: '1111111111' }]); const responses = await CommandService.handle({ sender: '1234567890', groupId: '1234567890@s.whatsapp.net', // DM mentions: [], message: '/t ver todos' }); expect(responses.length).toBe(1); const msg = responses[0].message; expect(msg).toContain('Tus tareas'); expect(msg).toContain('ℹ️ Para ver tareas sin responsable'); }); afterEach(() => { try { memDb.close(); } catch {} }); describe('CommandService', () => { test('should ignore non-tarea commands', async () => { const responses = await CommandService.handle({ ...testContextBase, message: '/othercommand' }); expect(responses).toEqual([]); }); test('acepta alias /t y responde con formato compacto', async () => { const responses = await CommandService.handle({ ...testContextBase, message: '/t n Test task' }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); // Debe empezar con "📝 `0001` " expect(responses[0].message).toMatch(/^📝 `\d{4}` /); // Debe mostrar la descripción en texto plano (sin cursiva) expect(responses[0].message).toContain(' Test task'); expect(responses[0].message).not.toContain('_Test task_'); // No debe usar el texto antiguo "Tarea creada" expect(responses[0].message).not.toMatch(/Tarea \d+ creada/); }); test('should return error response on failure', async () => { // Forzar error temporalmente const original = TaskService.createTask; (TaskService as any).createTask = () => { throw new Error('forced'); }; const responses = await CommandService.handle({ ...testContextBase, message: '/tarea nueva Test task' }); expect(responses.length).toBe(1); expect(responses[0].recipient).toBe('1234567890'); expect(responses[0].message).toBe('Error processing command'); // Restaurar (TaskService as any).createTask = original; }); });