teqo-backup/tests/unit/cityReportBatchSkill.unit.spec.ts
Francisco Solla 321ac7b872
Some checks failed
Deploy / preflight (push) Has been cancelled
Deploy / verify (push) Has been cancelled
Deploy / deploy staging (teqo-staging) (push) Has been cancelled
Deploy / requeue when main advanced (push) Has been cancelled
Deploy / deploy production (teqo-1313) (push) Has been cancelled
feat(relatorio-cidade): foca o relatório na mobilização do 2º turno
A federal foi decidida em 04/10; o documento de cidade passa a servir
inteiro à eleição de Lula. Saem conta eleitoral/cadeira de Solla,
concorrentes e pré-candidatos, emendas e entregas (sem fetch do Portal
da Transparência no builder), acervo de falas, panorama regional e
sinais/demandas; demografia, economia e transporte ficam como contexto
da visita.

A página 1 vira só mobilização: KPIs oficiais do artefato TSE 2026
(Lula/Δ, X₁/X₂, Solla), papel determinístico da cidade, quem mobiliza,
movimento local do 2º turno, riscos e o painel "O que pedir e falar"
(pedido Lula 13; guardrail de votos em jogo no lugar do defeso).

O checklist cai de 10 para 8 itens (emendas_web e quem_investe saem;
relacao_campo/disputa_local reancorados no 2º turno); listas
preCandidates/investments/emendasIndicators saem do contrato (arquivos
antigos seguem aceitos em replay). O snapshot enxuga para rede +
conjuntura + demografia (cityReportTerritory.mjs removido).

Prova: unit 5.232 verdes, int do snapshot sem PII e sem os grupos
aposentados, lint/format/typecheck/knip limpos e rebuild da fixture de
Cruz das Almas (9 seções, 16 páginas contra 21, página 1 fechando sem
fallback).
2026-10-10 16:57:20 -03:00

106 lines
4.7 KiB
TypeScript
Raw 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 { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
// OPS118: pins the batch contract of the relatorio-cidade skill and the
// researcher-only role of its subagent. The batch entry is prompt text (a
// comma-separated argument), so the contract lives in the SKILL.md prose — this
// guard fails the build if the batch/resolver/receipt/serialization wording is
// dropped, or if the subagent regains the ssh/build pipeline.
const repoRoot = process.cwd()
const read = (path: string): string => readFileSync(resolve(repoRoot, path), 'utf8')
const skill = read('.agents/skills/relatorio-cidade/SKILL.md')
const researcher = read('.opencode/agent/relatorio-cidade.md')
describe('skill /relatorio-cidade documents the batch contract', () => {
it('keeps the comma-separated batch entry and the single-city case', () => {
expect(skill).toContain('## Lote (várias cidades)')
expect(skill, 'batch example must be literal').toContain(
'/relatorio-cidade Ilheus, Itacare, Una',
)
expect(skill, 'single-city must remain valid').toContain('/relatorio-cidade Feira de Santana')
expect(skill, 'comma is the separator').toContain('vírgula')
expect(skill, 'N=1 is the preserved path').toContain('N=1')
})
it('pins the canonical slug resolution and the ambiguity policy', () => {
expect(skill).toContain('isMunicipalitySlug')
expect(skill).toContain('resolveMunicipalityName')
expect(skill, 'Salvador ambiguity must be explicit').toContain('salvador-ze-N')
expect(skill, 'ambiguous token is an isolated failure').toContain('ambíguo')
})
it('documents the staged pipeline with a serialized extraction', () => {
expect(skill).toContain('## Pipeline (etapas)')
expect(skill).toContain('serializada')
expect(skill, 'isolated failure must be visible as partial success').toContain(
'sucesso parcial',
)
expect(skill, 'summary lists status per entry').toContain('(ok|failed)')
})
})
describe('finite researcher receipt (OPS118)', () => {
it('declares the short receipt and its fields', () => {
expect(skill).toContain('## Recibo do researcher')
for (const field of [
'itemCount',
'gapCount',
'newsCount90d',
'weakSourceCount',
'failureReason',
]) {
expect(skill, `receipt must declare ${field}`).toContain(field)
}
expect(skill, 'receipt must never carry the body').toContain('corpo do `research.json`')
})
})
describe('skill /relatorio-cidade carries the 2º turno mobilization mission', () => {
it('documents the mission, the artifact and the role vocabulary', () => {
expect(skill).toContain('## Missão — 2º turno (Lula × Flávio — 25/10/2026)')
expect(skill, 'the new checklist item must be declared').toContain('segundo_turno')
expect(skill, 'the artifact build command is pinned').toContain('pnpm build:second-round')
expect(skill, 'the committed artifact path is pinned').toContain('bahia-second-round-2026.json')
expect(skill, 'the scenario is never a promise').toMatch(
/votos em jogo, não transferência garantida/i,
)
for (const role of ['prioridade', 'mobilizacao', 'defesa', 'expansao']) {
expect(skill, `role vocabulary must include ${role}`).toContain(role)
}
})
it('retires the mandate-era report blocks from the skill contract', () => {
for (const retired of ['emendas_web', 'quem_investe', 'preCandidates', 'emendasIndicators']) {
expect(skill, `skill must not reintroduce ${retired}`).not.toContain(retired)
expect(researcher, `researcher must not reintroduce ${retired}`).not.toContain(retired)
}
expect(skill, 'the refocus is explicit in the skill').toContain(
'fora de escopo desde o refactor',
)
})
it('makes the researcher research the local 2º turno movement, not the official numbers', () => {
expect(researcher).toContain('segundo_turno')
expect(researcher, 'official numbers come from the artifact').toMatch(
/números oficiais.*artefato/is,
)
})
})
describe('subagent .opencode/agent/relatorio-cidade.md is researcher-only', () => {
it('stays a subagent that writes research.json and returns the receipt', () => {
expect(researcher).toMatch(/^---\n[\s\S]*mode: subagent[\s\S]*\n---/)
expect(researcher).toContain('researcher')
expect(researcher).toContain('research.json')
expect(researcher).toContain('Recibo do researcher')
})
it('does not run the deterministic extract/build stages', () => {
expect(researcher, 'researcher must not run the extractor CLI').not.toContain('--municipality=')
expect(researcher, 'researcher must not run the builder CLI').not.toContain('--snapshot=')
})
})