teqo-backup/tests/unit/dossieInstitutionResearch.unit.spec.ts
Francisco Solla cb5854d17e feat(C187): dossiê Solla por instituição + boletim modelo (skill + builder) (Closes #1135)
Generaliza aditivamente o pipeline C186 (default município; specs verdes) via o
descritor scripts/lib/dossieUnit.mjs (municipality|institution), extrai o emit
A4/fit-guard para scripts/lib/buildPdf.mjs e a receita read-only para
scripts/lib/readOnlyExtract.mjs.

- Catálogo fail-closed src/lib/institutionCatalog.ts + institutionNameAliases.ts
  (seed UFBA/Correios/Enfermagem; escape one-off --slug/--name; dedup).
- Skill/command/subagente /dossie-solla-instituicao; eras A/B/C reusadas.
- Abrangência instituição × setor × rede nunca somada; era sem fonte vira página
  de lacuna explícita; lista capada declara "e mais N" (regra C188 no dono).
- Boletim modelo 1×A4 com fase junto ao valor, sem fontes, sem CTA; herda apenas
  fatos com fonte do dossiê (incl. falas do acervo read-only).
- Sem schema/migration/DB write; artefatos gitignored.
2026-09-17 19:25:13 -03:00

128 lines
4.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
DOSSIER_RESEARCH_CHECKLIST_IDS,
dossierChecklistForEra,
dossierResearchReceipt,
mergeDossierResearch,
normalizeDossierResearchInput,
} from '../../scripts/lib/dossieResearch.mjs'
import { INSTITUTION_UNIT } from '../../scripts/lib/dossieUnit.mjs'
// C187: the institution branch of the shared research contract. Same eras,
// institution slug field and `instituicao|setor|rede` spheres; the C186
// municipality behaviour is untouched (see dossieResearch.unit.spec.ts).
const validItem = (id: string, overrides: Record<string, unknown> = {}) => ({
id,
answer: 'Entrega documentada.',
sphere: 'instituicao',
sourceUrl: 'https://ufba.test/ato',
sourceDate: '2024-02-01',
...overrides,
})
const eraResearch = (era: 'A' | 'B' | 'C', overrides: Record<string, unknown> = {}) => ({
institutionSlug: 'ufba',
era,
researchedAt: '2026-09-17T10:00:00.000Z',
items: dossierChecklistForEra(era, INSTITUTION_UNIT).map((item) => validItem(item.id)),
...overrides,
})
describe('institution checklist', () => {
it('exposes an institution checklist id per era (distinct ids)', () => {
const ids = [
...dossierChecklistForEra('A', INSTITUTION_UNIT),
...dossierChecklistForEra('B', INSTITUTION_UNIT),
...dossierChecklistForEra('C', INSTITUTION_UNIT),
].map((row) => row.id)
expect(new Set(ids).size).toBe(ids.length)
expect(ids).toContain('era_a_vinculo')
expect(ids).toContain('era_c_parcerias')
// Municipality ids remain the C186 public surface.
expect(DOSSIER_RESEARCH_CHECKLIST_IDS).toContain('era_a_formacao')
})
})
describe('normalizeDossierResearchInput (institution unit)', () => {
it('requires institutionSlug and keeps institution items', () => {
const research = normalizeDossierResearchInput(eraResearch('C'), { unit: INSTITUTION_UNIT })
expect(research.institutionSlug).toBe('ufba')
expect(research.items.length).toBe(dossierChecklistForEra('C', INSTITUTION_UNIT).length)
expect(research.items.every((item) => item.sphere === 'instituicao')).toBe(true)
})
it('rejects the municipality slug field', () => {
expect(() =>
normalizeDossierResearchInput(
{ municipalitySlug: 'ilheus', era: 'C', researchedAt: '2026-09-17T10:00:00.000Z' },
{ unit: INSTITUTION_UNIT },
),
).toThrow(/institutionSlug/)
})
it('turns an invalid sphere into a gap with the institution vocabulary', () => {
const items = dossierChecklistForEra('A', INSTITUTION_UNIT).map((item) => validItem(item.id))
items[0] = validItem(items[0].id, { sphere: 'municipio' })
const research = normalizeDossierResearchInput(eraResearch('A', { items }), {
unit: INSTITUTION_UNIT,
})
expect(research.gaps).toContainEqual(
expect.objectContaining({
id: items[0].id,
reason: expect.stringMatching(/instituicao\/setor\/rede/),
}),
)
})
it('defaults the sphere to instituicao and keeps setor/rede', () => {
const research = normalizeDossierResearchInput(
eraResearch('B', {
items: [
validItem('era_b_sesab', { sphere: undefined }),
validItem('era_b_equipamentos', { sphere: 'setor' }),
validItem('era_b_programas', { sphere: 'rede' }),
],
}),
{ unit: INSTITUTION_UNIT },
)
const byId = Object.fromEntries(research.items.map((item) => [item.id, item.sphere]))
expect(byId.era_b_sesab).toBe('instituicao')
expect(byId.era_b_equipamentos).toBe('setor')
expect(byId.era_b_programas).toBe('rede')
})
it('builds an institution receipt with the institution research path', () => {
const receipt = dossierResearchReceipt(
normalizeDossierResearchInput(eraResearch('A'), { unit: INSTITUTION_UNIT }),
{ unit: INSTITUTION_UNIT },
)
expect(receipt.slug).toBe('ufba')
expect(receipt.researchPath).toBe('data/dossie-solla-instituicao/ufba.a.research.json')
expect(receipt.itemCount).toBeGreaterThan(0)
})
})
describe('mergeDossierResearch (institution unit)', () => {
it('merges the institution eras', () => {
const merged = mergeDossierResearch(
(['A', 'B', 'C'] as const).map((era) =>
normalizeDossierResearchInput(eraResearch(era), { unit: INSTITUTION_UNIT }),
),
{ unit: INSTITUTION_UNIT },
)
expect(merged.institutionSlug).toBe('ufba')
expect(merged.eras).toEqual(['A', 'B', 'C'])
})
it('fails closed when institution slugs disagree', () => {
const a = normalizeDossierResearchInput(eraResearch('A'), { unit: INSTITUTION_UNIT })
const b = normalizeDossierResearchInput(eraResearch('B', { institutionSlug: 'correios' }), {
unit: INSTITUTION_UNIT,
})
expect(() => mergeDossierResearch([a, b], { unit: INSTITUTION_UNIT })).toThrow(
/instituições diferentes/,
)
})
})