diff --git a/src/services/command.ts b/src/services/command.ts index 31fe1a9..d5228a5 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -1025,13 +1025,20 @@ export class CommandService { const n = parseInt(raw || '8', 10); return Number.isFinite(n) && n > 0 ? n : 8; })(); + const MAX_FALLBACK_DIGITS = (() => { + const raw = (process.env.ONBOARDING_FALLBACK_MAX_DIGITS || '').trim(); + const n = parseInt(raw || '15', 10); + return Number.isFinite(n) && n > 0 ? n : 15; + })(); - type FailReason = 'non_numeric' | 'too_short' | 'invalid'; + type FailReason = 'non_numeric' | 'too_short' | 'too_long' | 'from_lid' | 'invalid'; const isDigits = (s: string) => /^\d+$/.test(s); - const plausibility = (s: string): { ok: boolean; reason?: FailReason } => { + const plausibility = (s: string, opts?: { fromLid?: boolean }): { ok: boolean; reason?: FailReason } => { if (!s) return { ok: false, reason: 'invalid' }; + if (opts?.fromLid) return { ok: false, reason: 'from_lid' }; if (!isDigits(s)) return { ok: false, reason: 'non_numeric' }; if (s.length < MIN_FALLBACK_DIGITS) return { ok: false, reason: 'too_short' }; + if (s.length >= MAX_FALLBACK_DIGITS) return { ok: false, reason: 'too_long' }; return { ok: true }; }; const incOnboardingFailure = (source: 'mentions' | 'tokens', reason: FailReason) => { @@ -1056,7 +1063,10 @@ export class CommandService { } const resolved = IdentityService.resolveAliasOrNull(norm); if (resolved) return resolved; - const p = plausibility(norm); + // detectar si la mención proviene de un JID @lid (no plausible aunque sea numérico) + const dom = String(j || '').split('@')[1]?.toLowerCase() || ''; + const fromLid = dom.includes('lid'); + const p = plausibility(norm, { fromLid }); if (p.ok) return norm; // conservar para copy JIT unresolvedAssigneeDisplays.push(norm); @@ -1080,7 +1090,7 @@ export class CommandService { } const resolved = IdentityService.resolveAliasOrNull(norm); if (resolved) return resolved; - const p = plausibility(norm); + const p = plausibility(norm, { fromLid: false }); if (p.ok) return norm; // conservar para copy JIT (preferimos el token limpio v) unresolvedAssigneeDisplays.push(v); diff --git a/tests/unit/services/command.onboarding-jit-lid.test.ts b/tests/unit/services/command.onboarding-jit-lid.test.ts new file mode 100644 index 0000000..e69a729 --- /dev/null +++ b/tests/unit/services/command.onboarding-jit-lid.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeAll, beforeEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { CommandService } from '../../../src/services/command'; +import { ResponseQueue } from '../../../src/services/response-queue'; +import { TaskService } from '../../../src/tasks/service'; + +describe('CommandService - JIT onboarding para menciones @lid y números demasiado largos', () => { + let memdb: Database; + + beforeAll(() => { + memdb = new Database(':memory:'); + initializeDatabase(memdb); + (CommandService as any).dbInstance = memdb; + (TaskService as any).dbInstance = memdb; + (ResponseQueue as any).dbInstance = memdb; + }); + + beforeEach(() => { + process.env.NODE_ENV = 'test'; + process.env.ONBOARDING_ENABLE_IN_TEST = 'true'; + process.env.CHATBOT_PHONE_NUMBER = '34600000000'; + memdb.exec('DELETE FROM response_queue'); + memdb.exec('DELETE FROM users'); + memdb.exec('DELETE FROM tasks'); + memdb.exec('DELETE FROM task_assignments'); + }); + + it('cuando la mención proviene de @lid, no se asigna y se devuelve un DM JIT al creador con wa.me', async () => { + const res = await CommandService.handle({ + sender: '34611111111', + groupId: '123@g.us', + message: '/t n Pedir cita @166348562894911', + mentions: ['166348562894911@lid'] + }); + + const toCreator = res.filter(r => r.recipient === '34611111111').map(r => r.message).join('\n'); + expect(toCreator).toMatch(/activar/i); + expect(toCreator).toMatch(/https:\/\/wa\.me\/34600000000/); + + // No se devuelve ningún mensaje dirigido al "número" opaco + const recipients = res.map(r => r.recipient); + expect(recipients).not.toContain('166348562894911'); + }); + + it('cuando el token @ lleva 15+ dígitos, no es plausible y devuelve DM JIT al creador', async () => { + const res = await CommandService.handle({ + sender: '34622222222', + groupId: '123@g.us', + message: '/t n Tarea prueba @123456789012345', + mentions: [] + }); + + const toCreator = res.filter(r => r.recipient === '34622222222').map(r => r.message).join('\n'); + expect(toCreator).toMatch(/activar/i); + expect(toCreator).toMatch(/https:\/\/wa\.me\/34600000000/); + + const recipients = res.map(r => r.recipient); + expect(recipients).not.toContain('123456789012345'); + }); +});