From ecff2a564371ede4dfbe88d0b7aa25db57ff5cdc Mon Sep 17 00:00:00 2001 From: borja Date: Wed, 19 Nov 2025 15:35:26 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1adir=20reintentos=20en=20fondo?= =?UTF-8?q?=20para=20webhook=20y=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- src/http/bootstrap.ts | 29 ++++----- tests/unit/services/webhook-manager.test.ts | 70 +++++++++++++++++++++ 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/src/http/bootstrap.ts b/src/http/bootstrap.ts index 814c445..d7ad7fc 100644 --- a/src/http/bootstrap.ts +++ b/src/http/bootstrap.ts @@ -9,22 +9,20 @@ import { MaintenanceService } from '../services/maintenance'; export async function startServices(_db: Database): Promise { // Exponer la DB globalmente vía locator para servicios que lo usen. try { setDb(_db); } catch {} - await WebhookManager.registerWebhook(); - // Add small delay to allow webhook to propagate - await new Promise(resolve => setTimeout(resolve, 1000)); - const isActive = await WebhookManager.verifyWebhook(); - if (!isActive) { - console.error('❌ Webhook verification failed - retrying in 2 seconds...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - const isActiveRetry = await WebhookManager.verifyWebhook(); - if (!isActiveRetry) { - console.error('❌ Webhook verification failed after retry'); - process.exit(1); - } + + // Iniciar aseguramiento automático del webhook en background (reintentos con backoff) + try { + WebhookManager.startAutoEnsure(); + } catch (e) { + console.error('⚠️ Failed to start webhook auto-ensure loop:', e); } - // Initialize groups - critical for operation - await GroupSyncService.checkInitialGroups(); + // Initialize groups - critical for operation (no abortar si falla) + try { + await GroupSyncService.checkInitialGroups(); + } catch (e) { + console.error('⚠️ Initial groups check failed (will rely on schedulers to recover):', e); + } // Start groups scheduler (periodic sync of groups) try { @@ -73,6 +71,9 @@ export async function startServices(_db: Database): Promise { } export function stopServices(): void { + try { + WebhookManager.stopAutoEnsure(); + } catch {} try { ResponseQueue.stopCleanupScheduler(); } catch {} diff --git a/tests/unit/services/webhook-manager.test.ts b/tests/unit/services/webhook-manager.test.ts index ae23d2d..e88351f 100644 --- a/tests/unit/services/webhook-manager.test.ts +++ b/tests/unit/services/webhook-manager.test.ts @@ -80,4 +80,74 @@ describe('WebhookManager', () => { } }).toThrow(); }); + + test('should retry registration in background until success', async () => { + // Acelerar reintentos + process.env.WEBHOOK_RETRY_INITIAL_DELAY_MS = '10'; + process.env.WEBHOOK_RETRY_MAX_DELAY_MS = '20'; + process.env.WEBHOOK_RETRY_DEGRADED_INTERVAL_MS = '50'; + + const originalFetch: any = globalThis.fetch; + let setCalls = 0; + + const fetchSpy = mock(async (input: any, init?: any) => { + const url = typeof input === 'string' ? input : input.url; + + // Self-test of our own webhook endpoint + if (url === process.env.WEBHOOK_URL) { + return new Response('ok', { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + + // Register webhook endpoint (first fails, then succeeds) + if (typeof url === 'string' && url.includes('/webhook/set/')) { + setCalls++; + if (setCalls < 2) { + return new Response('unavailable', { status: 503, statusText: 'Service Unavailable' }); + } + const body = JSON.stringify({ + id: 'test-id', + url: process.env.WEBHOOK_URL, + enabled: true, + events: ['APPLICATION_STARTUP'] + }); + return new Response(body, { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + + // Verify webhook endpoint (returns enabled) + if (typeof url === 'string' && url.includes('/webhook/find/')) { + const body = JSON.stringify({ + enabled: true, + url: process.env.WEBHOOK_URL, + events: ['APPLICATION_STARTUP'] + }); + return new Response(body, { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + + return new Response('not found', { status: 404 }); + }); + + // @ts-ignore + globalThis.fetch = fetchSpy; + + try { + // Asegurar que no haya un lazo previo corriendo + try { (WebhookManager as any).stopAutoEnsure(); } catch {} + + WebhookManager['startAutoEnsure'](); + + // Esperar a que el lazo logre activar el webhook (o agote timeout) + const deadline = Date.now() + 2000; + while (WebhookManager['autoEnsureActive'] && Date.now() < deadline) { + await new Promise(res => setTimeout(res, 20)); + } + + expect(WebhookManager['autoEnsureActive']).toBe(false); + expect(fetchSpy).toHaveBeenCalled(); + expect(setCalls).toBeGreaterThan(1); + } finally { + try { WebhookManager['stopAutoEnsure'](); } catch {} + // @ts-ignore + globalThis.fetch = originalFetch; + } + }); });