teqo-backup/tests/unit/testMunicipalityAllocatorConventions.unit.spec.ts

125 lines
5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { readdirSync, readFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
// Miss #73 (2026-07-31): int specs pinning seeded municipality slugs
// (`getMunicipality('cairu')` et al.) and mutating them deadlocked
// `municipality_rels` under the parallel suite (Postgres 40P01), and e2e specs
// deep-linking `?municipio=cairu` raced the same row from two workers. The
// allocator (`getMunicipality()` / `claimMunicipality()`, Postgres sequence)
// hands out a unique row per claim; pinning is what breaks the contract.
//
// Rule 3 is the literal-regex sibling of the anchored-template guard (P3-C):
// `new RegExp(\`…\`)` was hardened there, but `/Cairu/i` literals dodged it —
// and catalog names are not prefix-unique (Conde/Condeúba, Barra ×4, Laje ×3).
const repoRoot = process.cwd()
const repoPath = (absolute: string) => relative(repoRoot, absolute)
const walkFiles = (root: string, extensions: readonly string[]): string[] =>
readdirSync(root, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext)))
.map((entry) => resolve(entry.parentPath, entry.name))
const specFiles = [
...walkFiles(resolve(repoRoot, 'tests/int'), ['.ts']),
...walkFiles(resolve(repoRoot, 'tests/e2e'), ['.ts']),
]
/** `getMunicipality(…)` called with a non-empty argument (a pinned slug). */
const FIXED_SLUG_CALL = /\bgetMunicipality\s*\(\s*[^)\s]/
const MUTATION_PATTERN = new RegExp(
[
'\\b(?:assignMunicipalityAdvisors(?:Record)?|touchMunicipality|createLeadership|createVotePledge|createCampaignDemand|createMunicipalityUpdate|setStateDeputyMunicipalitiesBatchRecord)\\s*\\(',
'payload\\.update\\(\\s*\\{\\s*collection:\\s*[\'"]municipality[\'"]',
].join('|'),
)
describe('municipality allocator discipline (miss #73)', () => {
it('never pins a fixed-slug municipality in a spec that mutates municipalities', () => {
const offenders: string[] = []
for (const file of specFiles) {
const source = readFileSync(file, 'utf8')
if (!FIXED_SLUG_CALL.test(source)) continue
if (!MUTATION_PATTERN.test(source)) continue
offenders.push(repoPath(file))
}
expect(
offenders,
'allocate with getMunicipality() (no slug) — read-only catalog lookups belong to getMunicipalityCatalogEntry',
).toEqual([])
})
it('never deep-links a hardcoded municipality slug in e2e specs', () => {
const offenders: string[] = []
for (const file of walkFiles(resolve(repoRoot, 'tests/e2e'), ['.ts'])) {
const lines = readFileSync(file, 'utf8').split('\n')
for (const [index, line] of lines.entries()) {
if (/municipio=[a-z0-9-]/.test(line)) offenders.push(`${repoPath(file)}:${index + 1}`)
}
}
expect(offenders, 'interpolate claimMunicipality().slug into wizard deep links').toEqual([])
})
it('anchors e2e name regexes that mention a catalog municipality', () => {
const offenders: string[] = []
const nameLiteral = /name:\s*\/([^/\n]+)\/i?\b/g
const catalogNames = municipalityCatalog
.map((entry) => entry.name)
.filter((name) => name.length >= 4)
for (const file of walkFiles(resolve(repoRoot, 'tests/e2e'), ['.ts'])) {
const source = readFileSync(file, 'utf8')
nameLiteral.lastIndex = 0
let match: RegExpExecArray | null
while ((match = nameLiteral.exec(source)) !== null) {
const pattern = match[1]!
if (pattern.startsWith('^')) continue
const hit = catalogNames.find((name) => pattern.toLowerCase().includes(name.toLowerCase()))
if (hit) {
const line = source.slice(0, match.index).split('\n').length
offenders.push(`${repoPath(file)}:${line} (unanchored, matches "${hit}")`)
}
}
}
expect(
offenders,
'anchor with ^ (or locate by href) — catalog names are not prefix-unique',
).toEqual([])
})
it('routes every claim through the shared allocator module (OPS46)', () => {
// The claims registry (campaignMunicipalityAllocator.ts) is what makes the
// purge-on-claim contract safe after the sequence wraps; a direct
// nextval/sequence reference anywhere else reintroduces the collision it
// exists to prevent.
const offenders: string[] = []
const allocatorModule = repoPath(
resolve(repoRoot, 'tests/helpers/campaignMunicipalityAllocator.ts'),
)
const scanned = [
...specFiles,
...walkFiles(resolve(repoRoot, 'tests/helpers'), ['.ts']),
].filter((file) => repoPath(file) !== allocatorModule)
for (const file of scanned) {
const source = readFileSync(file, 'utf8')
if (/\bnextval\s*\(|\bsetval\s*\(|campaign_fixture_municipality_alloc/.test(source)) {
offenders.push(repoPath(file))
}
}
expect(
offenders,
'claim through claimMunicipalityIndex — direct sequence access bypasses the claims registry',
).toEqual([])
})
})