From ad26dd517514132169271be1c245fa784d0d9a1c Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 29 Sep 2025 16:14:59 +0200 Subject: [PATCH] feat: guarda label en allowed_groups y actualiza en upsertGroups Co-authored-by: aider (openrouter/openai/gpt-5) --- src/server.ts | 4 +- src/services/group-sync.ts | 2 + tests/unit/server/discovery-label.test.ts | 92 +++++++++++++++++++ .../services/group-sync.label-update.test.ts | 34 +++++++ 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/unit/server/discovery-label.test.ts create mode 100644 tests/unit/services/group-sync.label-update.test.ts diff --git a/src/server.ts b/src/server.ts index a48ef9b..751d709 100644 --- a/src/server.ts +++ b/src/server.ts @@ -330,7 +330,7 @@ export class WebhookServer { .prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? LIMIT 1`) .get(remoteJid) as any; if (!exists) { - try { AllowedGroups.upsertPending(remoteJid, null, normalizedSenderId); } catch {} + try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {} try { Metrics.inc('unknown_groups_discovered_total'); } catch {} try { const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true'; @@ -347,7 +347,7 @@ export class WebhookServer { } } catch { // Si la tabla no existe por alguna razón, intentar upsert y retornar igualmente - try { AllowedGroups.upsertPending(remoteJid, null, normalizedSenderId); } catch {} + try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {} try { Metrics.inc('unknown_groups_discovered_total'); } catch {} try { const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true'; diff --git a/src/services/group-sync.ts b/src/services/group-sync.ts index 55ae482..b90383d 100644 --- a/src/services/group-sync.ts +++ b/src/services/group-sync.ts @@ -332,6 +332,8 @@ export class GroupSyncService { console.log('Added group:', group.id, 'result:', insertResult); added++; } + // Propagar subject como label a allowed_groups (no degrada estado; actualiza label si cambia) + try { (AllowedGroups as any).dbInstance = this.dbInstance; AllowedGroups.upsertPending(group.id, group.subject, null); } catch {} } return { added, updated }; diff --git a/tests/unit/server/discovery-label.test.ts b/tests/unit/server/discovery-label.test.ts new file mode 100644 index 0000000..1a1a5bf --- /dev/null +++ b/tests/unit/server/discovery-label.test.ts @@ -0,0 +1,92 @@ +import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { WebhookServer } from '../../../src/server'; +import { initializeDatabase } from '../../../src/db'; +import { ResponseQueue } from '../../../src/services/response-queue'; +import { GroupSyncService } from '../../../src/services/group-sync'; + +let testDb: Database; +let originalAdd: any; + +let simulatedQueue: any[] = []; +const SimulatedResponseQueue = { + async add(responses: any[]) { + simulatedQueue.push(...responses); + }, + clear() { simulatedQueue = []; }, + get() { return simulatedQueue; } +}; + +const createTestRequest = (payload: any) => + new Request('http://localhost:3007', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + +describe('WebhookServer - discovery guarda label del grupo si está en caché', () => { + const envBackup = process.env; + + beforeAll(() => { + testDb = new Database(':memory:'); + initializeDatabase(testDb); + originalAdd = (ResponseQueue as any).add; + }); + + afterAll(() => { + (ResponseQueue as any).add = originalAdd; + testDb.close(); + }); + + beforeEach(() => { + process.env = { + ...envBackup, + NODE_ENV: 'test', + GROUP_GATING_MODE: 'discover' + }; + SimulatedResponseQueue.clear(); + (ResponseQueue as any).add = SimulatedResponseQueue.add; + WebhookServer.dbInstance = testDb; + + // Limpiar tablas relevantes + testDb.exec('DELETE FROM response_queue'); + testDb.exec('DELETE FROM allowed_groups'); + testDb.exec('DELETE FROM users'); + + // Poblar caché con el nombre del grupo + GroupSyncService.activeGroupsCache.clear(); + GroupSyncService.activeGroupsCache.set('label-group@g.us', 'Proyecto Foo'); + }); + + afterEach(() => { + process.env = envBackup; + }); + + test('registra pending con label del grupo desde la caché', async () => { + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'label-group@g.us', + participant: '9999999999@s.whatsapp.net' + }, + message: { conversation: '/t n hola' } + } + }; + + const res = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(res.status).toBe(200); + + // No debe haber respuestas encoladas (retorno temprano) + expect(SimulatedResponseQueue.get().length).toBe(0); + + // Debe existir registro pending con label en allowed_groups + const row = testDb + .query(`SELECT status, label FROM allowed_groups WHERE group_id = 'label-group@g.us'`) + .get() as any; + expect(row).toBeDefined(); + expect(String(row.status)).toBe('pending'); + expect(String(row.label)).toBe('Proyecto Foo'); + }); +}); diff --git a/tests/unit/services/group-sync.label-update.test.ts b/tests/unit/services/group-sync.label-update.test.ts new file mode 100644 index 0000000..308be45 --- /dev/null +++ b/tests/unit/services/group-sync.label-update.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import Database from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { GroupSyncService } from '../../../src/services/group-sync'; + +describe('GroupSyncService - upsertGroups actualiza label en allowed_groups', () => { + const envBackup = process.env; + let memdb: Database; + + beforeEach(() => { + process.env = { ...envBackup, NODE_ENV: 'test', WHATSAPP_COMMUNITY_ID: 'comm-1' }; + memdb = new Database(':memory:'); + initializeDatabase(memdb); + (GroupSyncService as any).dbInstance = memdb; + + // Limpiar tablas + memdb.exec('DELETE FROM allowed_groups'); + memdb.exec('DELETE FROM groups'); + }); + + afterEach(() => { + memdb.close(); + process.env = envBackup; + }); + + it('propaga subject como label aunque no exista fila previa', async () => { + await (GroupSyncService as any).upsertGroups([{ id: 'gg@g.us', subject: 'Grupo GG', linkedParent: 'comm-1' }]); + + const row = memdb.query(`SELECT label, status FROM allowed_groups WHERE group_id = 'gg@g.us'`).get() as any; + expect(row).toBeDefined(); + expect(String(row.label)).toBe('Grupo GG'); + expect(String(row.status)).toBe('pending'); + }); +});