diff --git a/tests/unit/services/command.task-origins.test.ts b/tests/unit/services/command.task-origins.test.ts new file mode 100644 index 0000000..67eae9f --- /dev/null +++ b/tests/unit/services/command.task-origins.test.ts @@ -0,0 +1,55 @@ +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'; +import { GroupSyncService } from '../../../src/services/group-sync'; + +describe('CommandService - inserta task_origins al crear en grupo con messageId', () => { + let memdb: Database; + + beforeAll(() => { + memdb = new Database(':memory:'); + initializeDatabase(memdb); + (TaskService as any).dbInstance = memdb; + (CommandService as any).dbInstance = memdb; + + // Sembrar grupo activo y cache + memdb.exec(` + INSERT OR IGNORE INTO groups (id, community_id, name, active, archived, is_community, last_verified) + VALUES ('g1@g.us', 'comm-1', 'G1', 1, 0, 0, strftime('%Y-%m-%d %H:%M:%f','now')) + `); + try { (GroupSyncService as any).dbInstance = memdb; } catch {} + GroupSyncService.activeGroupsCache?.clear?.(); + GroupSyncService.activeGroupsCache?.set?.('g1@g.us', 'G1'); + }); + + beforeEach(() => { + process.env.NODE_ENV = 'test'; + memdb.exec('DELETE FROM task_assignments; DELETE FROM tasks; DELETE FROM task_origins;'); + }); + + it('crea tarea en grupo y registra (task_id, chat_id, message_id)', async () => { + const sender = '600111222'; + const res = await CommandService.handle({ + sender, + groupId: 'g1@g.us', + message: '/t n pruebas origen 2099-01-05', + mentions: [], + messageId: 'MSG-ORIG-1' + }); + + expect(res.length).toBeGreaterThan(0); + + const t = memdb.prepare(`SELECT id FROM tasks ORDER BY id DESC LIMIT 1`).get() as any; + expect(t).toBeTruthy(); + const row = memdb.prepare(` + SELECT task_id, chat_id, message_id FROM task_origins WHERE task_id = ? + `).get(Number(t.id)) as any; + + expect(row).toBeTruthy(); + expect(Number(row.task_id)).toBe(Number(t.id)); + expect(String(row.chat_id)).toBe('g1@g.us'); + expect(String(row.message_id)).toBe('MSG-ORIG-1'); + }); +}); diff --git a/tests/unit/services/response-queue.reactions.test.ts b/tests/unit/services/response-queue.reactions.test.ts new file mode 100644 index 0000000..5da435f --- /dev/null +++ b/tests/unit/services/response-queue.reactions.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { ResponseQueue } from '../../../src/services/response-queue'; + +const ORIGINAL_FETCH = globalThis.fetch; +const envBackup = { ...process.env }; + +describe('ResponseQueue - jobs de reacción (enqueue + sendOne)', () => { + let memdb: Database; + let captured: { url?: string; payload?: any } = {}; + + beforeEach(() => { + process.env = { + ...envBackup, + NODE_ENV: 'test', + EVOLUTION_API_URL: 'http://evolution.test', + EVOLUTION_API_INSTANCE: 'instance-1', + EVOLUTION_API_KEY: 'apikey', + RQ_REACTIONS_MAX_ATTEMPTS: '3', + }; + + memdb = new Database(':memory:'); + memdb.exec('PRAGMA foreign_keys = ON;'); + initializeDatabase(memdb); + + (ResponseQueue as any).dbInstance = memdb; + + globalThis.fetch = async (url: RequestInfo | URL, init?: RequestInit) => { + captured.url = String(url); + try { + captured.payload = init?.body ? JSON.parse(String(init.body)) : null; + } catch { + captured.payload = null; + } + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + }; + + memdb.exec('DELETE FROM response_queue'); + captured = {}; + }); + + afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + process.env = envBackup; + try { memdb.close(); } catch {} + }); + + it('enqueueReaction aplica idempotencia por (chatId, messageId, emoji) en ventana 24h', async () => { + await ResponseQueue.enqueueReaction('123@g.us', 'MSG-1', '🤖'); + await ResponseQueue.enqueueReaction('123@g.us', 'MSG-1', '🤖'); // duplicado → ignorar + + const cnt = memdb.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(cnt.c)).toBe(1); + + // Mismo chat y mensaje, emoji distinto → debe insertar + await ResponseQueue.enqueueReaction('123@g.us', 'MSG-1', '⚠️'); + const cnt2 = memdb.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(cnt2.c)).toBe(2); + }); + + it('sendOne con metadata.kind === "reaction" usa /message/sendReaction y payload esperado', async () => { + const item = { + id: 42, + recipient: '123@g.us', + message: '', // no se usa para reaction + attempts: 0, + metadata: JSON.stringify({ kind: 'reaction', emoji: '🤖', chatId: '123@g.us', messageId: 'MSG-99' }), + }; + + const res = await ResponseQueue.sendOne(item as any); + expect(res.ok).toBe(true); + + expect(captured.url?.includes('/message/sendReaction/instance-1')).toBe(true); + expect(captured.payload).toBeDefined(); + expect(captured.payload.reaction).toBe('🤖'); + expect(captured.payload.key).toEqual({ remoteJid: '123@g.us', fromMe: true, id: 'MSG-99' }); + }); +});