mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
125 lines
5 KiB
TypeScript
125 lines
5 KiB
TypeScript
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([])
|
||
})
|
||
})
|