diff --git a/docs/changelog/2026-08-24-debt-flakes-c100.md b/docs/changelog/2026-08-24-debt-flakes-c100.md new file mode 100644 index 00000000..3f478d14 --- /dev/null +++ b/docs/changelog/2026-08-24-debt-flakes-c100.md @@ -0,0 +1,16 @@ +# DEBT-FLAKES-C100 — fix flakes pré-existentes do C100 (Closes #553) + +Duas flaky tests do pipeline de sugestões de campanha (E11) e do maquinário de +lease de consentimento, ambas pré-existentes ao PR C100: + +1. **campaignSuggestions silence review** — amostra de 10 municípios insuficiente + sob carga paralela (P(fail) nominal 0,17%). Amostra ampliada para 30 + (P(fail) ≈ 7e-9) com mensagem de diagnóstico. + +2. **testDatabaseLease concurrent ensures** — TOCTOU find→create em + `ensureLeasedConsent` permitia 2 creates sob advisory lock extremo. + Tornado idempotente via `INSERT … ON CONFLICT (key) DO NOTHING` + + find, eliminando a corrida independente do lock. + +Mudanças: `tests/helpers/testDatabaseLease.ts`, +`tests/int/campaignSuggestions.int.spec.ts`. diff --git a/tests/helpers/testDatabaseLease.ts b/tests/helpers/testDatabaseLease.ts index da79bfa2..e5109ee9 100644 --- a/tests/helpers/testDatabaseLease.ts +++ b/tests/helpers/testDatabaseLease.ts @@ -457,6 +457,16 @@ export const ensureLeasedConsent = async ( // hitting the UNIQUE constraint. Autocommit is safe: we hold the // exclusive lease, so no live renamer can be mid-flight. await purgeOrphanedConsentRenames((query) => payload.db.drizzle.execute(query)) + // Idempotent upsert: INSERT … ON CONFLICT (key) DO NOTHING eliminates the + // TOCTOU race between the find and the create — even if two ensures slip + // past the advisory lock under extreme load, the UNIQUE consent_key_idx + // guarantees exactly one row. The subsequent find always returns the + // committed row (created by this INSERT or a prior one). + await payload.db.drizzle.execute(sql` + INSERT INTO "consent" ("key", "text", "updated_at", "created_at") + VALUES (${consentKey}, ${fixtureConsentText}, now(), now()) + ON CONFLICT ("key") DO NOTHING + `) const existing = await payload.find({ collection: 'consent', where: { key: { equals: consentKey } }, @@ -464,16 +474,10 @@ export const ensureLeasedConsent = async ( sort: 'id', depth: 0, }) - if (existing.docs[0]) return existing.docs[0] - - return payload.create({ - collection: 'consent', - data: { - key: consentKey, - text: fixtureConsentText, - }, - depth: 0, - }) + if (!existing.docs[0]) { + throw new Error(`ensureLeasedConsent: consent key "${consentKey}" not found after upsert.`) + } + return existing.docs[0] }) export const withLeasedConsent = async ( diff --git a/tests/int/campaignSuggestions.int.spec.ts b/tests/int/campaignSuggestions.int.spec.ts index ba7a9abc..d634425f 100644 --- a/tests/int/campaignSuggestions.int.spec.ts +++ b/tests/int/campaignSuggestions.int.spec.ts @@ -276,9 +276,11 @@ describe('loadMunicipalitySuggestions (E11)', () => { // A quiet município cannot be staged — quietness means NO artifact-anchored // pattern fires — so claim through the allocator until one qualifies - // (measured: 205 of 435 with zeroed rows). + // (measured: 205 of 435 with zeroed rows → P(fail in 30) ≈ 0.53^30 ≈ 7e-9). + // 30 attempts keeps the flake probability negligible even under parallel load + // where the effective quiet rate may dip below the nominal 47%. let municipality: Municipality | null = null - for (let attempt = 0; attempt < 10; attempt += 1) { + for (let attempt = 0; attempt < 30; attempt += 1) { const candidate = await fixtures.getMunicipality() await normalizeMunicipality(candidate, { priority: 'alta' }) fixtures.touchMunicipality(candidate.id) @@ -289,7 +291,7 @@ describe('loadMunicipalitySuggestions (E11)', () => { break } } - if (!municipality) throw new Error('Nenhum município silencioso em 10 alocações.') + if (!municipality) throw new Error('Nenhum município silencioso em 30 alocações.') const author = await fixtures.createCampaignUser('coordinator') await fixtures.createMunicipalityUpdate({