diff --git a/scripts/test-circuit-breaker.mjs b/scripts/test-circuit-breaker.mjs index 81b5d63..cf14f57 100644 --- a/scripts/test-circuit-breaker.mjs +++ b/scripts/test-circuit-breaker.mjs @@ -128,6 +128,16 @@ try { check('journal : lignes sans date signalées', logs.some(([, t]) => t.includes('1 sans date ignorées'))) } + // 3bis. Aucune ligne datée : jamais « 0 € vérifié » + { + const rows = [{ Id: 1, cout_eur: 30 }, { Id: 2, cout_eur: 1 }] + const { fetchJson } = fauxNocodb({ schema: ['Id', 'cout_eur'], rows }) + logs.length = 0 + const s = await checkBudget({ ...base, fetchJson }) + check('aucune date : verified=false, pas 0 € vérifié', s.verified === false && s.lignesSansDate === 2) + check('aucune date : avertissement « NON vérifié »', logs.some(([niv, t]) => niv === 'warn' && t.includes('NON vérifié'))) + } + // 4. Pagination : 2 500 lignes du mois à 0,01 € → 25 € → bloqué { const rows = Array.from({ length: 2500 }, (_, i) => ({ Id: i + 1, cout_eur: 0.01, CreatedAt: '2026-09-20T00:00:00Z' })) diff --git a/server/utils/circuitBreaker.ts b/server/utils/circuitBreaker.ts index b0454b9..73262d3 100644 --- a/server/utils/circuitBreaker.ts +++ b/server/utils/circuitBreaker.ts @@ -113,6 +113,15 @@ export async function checkBudget(config: { return { cumulEur: 0, blocked: false, warning: false, verified: false, lignesLues, lignesMois, lignesSansDate } } + // Des lignes, mais aucune datée : on ne sait pas ce qui relève du mois. + // Rendre « 0 € vérifié » reproduirait le fail-open silencieux d'avant AF5. + if (lignesLues > 0 && lignesSansDate === lignesLues) { + console.warn( + `[circuitBreaker] budget NON vérifié : ${lignesLues} lignes stats_usage sans timestamp, CreatedAt ni created_at`, + ) + return { cumulEur: 0, blocked: false, warning: false, verified: false, lignesLues, lignesMois, lignesSansDate } + } + const status: BudgetStatus = { cumulEur, blocked: cumulEur >= BUDGET_MAX_EUR,