TS219 linhas12.829 bytes
apps/worker/tests/tramitacao.ts
SHA-256
b11143c0addc1092f4a4a8582e95bc2356036167cdbf0428aaf6deb8fe742829
Somente leiturafonte-0531042d9f3e
import assert from 'node:assert/strict'
import { test } from 'node:test'
import Database from 'better-sqlite3'
import { readFileSync } from 'node:fs'
import { app } from '../src/index'
import { SqliteD1 } from '../src/runtime/node/d1-sqlite'
import type { Env, IdsScore } from '../src/types'
import { classificarTramitacao, resumirAutorias, regularizarEfetividade, percentisEfetividade } from '../src/services/proposicoes'
import { avaliarAutorias, corteProposicoes, esperaRetryAfter } from '../src/services/processos'
import { getRawDimensions } from '../src/services/legis'
import { getRankingSnapshot, persistRanking } from '../src/services/ranking'
import { snapshotsComparaveis } from '../src/services/newsletter-data'
import { apurarScores, REGUA_ANO } from '../src/services/premiacao'
const corte = '2026-10-29'
const pl = { id: 1, identificacao: 'PL 1/2023', dataApresentacao: '2023-02-02' }
const situacao = (sigla: string, inicio = '2024-02-01', descricao = sigla) => ({ sigla, inicio, descricao })
const historico = (...situacoes: ReturnType<typeof situacao>[]) => ({ ...pl, autuacoes: [{ numero: 1, situacoes }] })
const avaliar = (p: Record<string, unknown>, data = corte) => classificarTramitacao(p, data)
const resumo = (...processos: Record<string, unknown>[]) => resumirAutorias(processos.map((processo) => ({ processo, tramitacao: avaliar(processo) })), corte)
test('Retry-After aceita segundos, data HTTP e ausência, sem ignorar a limitação da fonte', () => {
const agora = Date.parse('2026-10-29T14:00:00Z')
assert.equal(esperaRetryAfter('120', agora), 120000)
assert.equal(esperaRetryAfter('Thu, 29 Oct 2026 14:02:00 GMT', agora), 120000)
assert.equal(esperaRetryAfter(null, agora), 60000)
})
test('campos reais da listagem: normaGerada prevalece sobre aprovação e tramitando=Não', () => {
const t = avaliar({ ...pl, siglaTipoDeliberacao: 'APROVADA_NO_PLENARIO', dataDeliberacao: '2024-02-01', normaGerada: 'Lei nº 15.230 de 02/10/2025', tramitando: 'Não' })
assert.equal(t.etapa, 'norma')
assert.equal(t.pontos, 4)
})
test('remessa à Câmara reconhecida no campo situacaoAtual mesmo com processo encerrado', () => {
assert.equal(avaliar({ ...pl, situacaoAtual: 'REMETIDA À CÂMARA DOS DEPUTADOS', dataSituacaoAtual: '2024-03-14', tramitando: 'Não' }).etapa, 'camara')
})
test('destinação oficial: comissão terminativa → Câmara; plenário → sanção/promulgação', () => {
for (const [destino, pontos] of [['CAMARA', 2], ['SANCAO', 3], ['PROMULGACAO', 3]] as const) {
assert.equal(avaliar({ ...pl, deliberacao: { siglaTipo: 'APROVADA_EM_COMISSAO_TERMINATIVA', siglaDestino: destino, data: '2025-01-01' } }).pontos, pontos)
}
assert.equal(avaliar({ ...pl, deliberacao: { siglaTipo: 'DEVOLVIDO_CD_ACAO_JUD', siglaDestino: 'CAMARA', data: '2025-01-01' } }).pontos, 0)
})
test('movimentos administrativos, ementa, requerimento incidental e transformada em indicação não pontuam', () => {
for (const dados of [
{ tramitando: 'Sim' }, { situacaoAtual: 'REMETIDA À COMISSÃO', dataSituacaoAtual: '2024-01-01' },
{ siglaTipoDeliberacao: 'TRANSF_PROJ_LEI_SEN', dataDeliberacao: '2024-01-01' },
{ siglaTipoDeliberacao: 'DEFERIDO_CDIR', dataDeliberacao: '2024-01-01' },
{ situacaoAtual: 'NÃO APROVADA', dataSituacaoAtual: '2024-01-01' },
{ situacaoAtual: 'APROVADO O REQUERIMENTO', dataSituacaoAtual: '2024-01-01' },
{ objetivo: 'aprovado', ementa: 'Altera lei sancionada', normaGerada: {} },
]) assert.equal(avaliar({ ...pl, ...dados }).pontos, 0)
})
test('parecer favorável comprovado pontua instrução; parecer contrário não equivale a aprovação', () => {
const base = { ...pl, autuacoes: [{ numero: 1, informesLegislativos: [{ siglaSituacaoIniciada: 'APRVPAR', data: '2024-02-01 10:00:00', descricao: 'Aprovado parecer pela aprovação do projeto.' }] }] }
assert.equal(avaliar(base).pontos, 1)
base.autuacoes[0].informesLegislativos[0].descricao = 'Aprovado parecer pela rejeição do projeto.'
assert.equal(avaliar(base).pontos, 0)
assert.equal(avaliar(historico(situacao('APRVPAR'))).pontos, 0)
})
test('corte histórico impede atribuir norma futura a ano passado e preserva aprovação anterior ao arquivo', () => {
const p = { ...historico(situacao('RMCD', '2024-03-01'), situacao('TNJR', '2026-02-01')),
situacaoAtual: 'AO ARQUIVO', dataSituacaoAtual: '2026-03-01',
normaGerada: { codigo: 1, dataAssinatura: '2026-02-01', descricao: 'Lei nº 1' } }
assert.equal(avaliar(p, '2024-12-31').pontos, 2)
assert.equal(avaliar(p).pontos, 4)
assert.equal(avaliar(p, '2023-12-31').pontos, 0)
})
test('autuação incidental não aprova a matéria; datas ausentes/futuras não pontuam', () => {
assert.equal(avaliar({ ...pl, autuacoes: [{ numero: 2, situacoes: [situacao('TNJR')] }] }).pontos, 0)
const t = avaliar({ ...pl, normaGerada: { codigo: 1 }, siglaSituacaoAtual: 'RMCD', dataSituacaoAtual: '2027-02-01' })
assert.equal(t.pontos, 0)
assert.equal(t.evidenciasSemData, 1)
assert.equal(avaliar(historico(situacao('TNJR', '2024-02-31'))).pontos, 0)
assert.equal(corteProposicoes(undefined, '2028-01-01'), '2027-01-31')
})
test('veto parcial com norma é norma; veto total sem norma não ganha pontos de lei', () => {
assert.equal(avaliar({ ...pl, situacaoAtual: 'TRANSFORMADA EM NORMA JURÍDICA COM VETO PARCIAL', dataSituacaoAtual: '2024-01-01' }).pontos, 4)
assert.equal(avaliar({ ...historico(situacao('RMSAN')), situacaoAtual: 'VETADA', dataSituacaoAtual: '2024-03-01' }).pontos, 3)
})
test('requerimentos/indicações/MPV não geram efetividade e não diluem seu denominador', () => {
const r = resumo(historico(situacao('TNJR')), { ...pl, identificacao: 'RQS 1/2023', situacaoAtual: 'APROVADA', dataSituacaoAtual: '2024-02-01' }, { ...pl, identificacao: 'MPV 1/2023' })
assert.equal(r.autoriasTotal, 3)
assert.equal(r.autoriasAprovadas, 1)
assert.equal(r.tramitacao.normativas, 1)
assert.equal(r.tramitacao.proporcao, 1)
assert.equal(r.produtividadePonderada, 2.9)
})
test('cada projeto tem no máximo quatro pontos; as contagens por etapa são exclusivas', () => {
const r = resumo(historico(situacao('PRONDEPLEN'), situacao('APRVD'), situacao('RMCD'), situacao('RMSAN'), situacao('TNJR')))
assert.equal(r.tramitacao.pontosPonderados, 6)
assert.equal(Object.values(r.tramitacao.porEtapa).reduce((a, b) => a + b), 1)
assert.equal(r.tramitacao.porEtapa.norma, 1)
})
test('regularização usa propostas efetivas, preserva ordenação e reduz instabilidade de uma única lei', () => {
const uma = resumo(historico(situacao('TNJR'))).tramitacao
const muitas = resumo(...Array.from({ length: 100 }, () => historico(situacao('TNJR')))).tramitacao
assert.ok(regularizarEfetividade(uma, 0.2) < regularizarEfetividade(muitas, 0.2))
assert.ok(regularizarEfetividade(uma, 0.2) > 0.2)
assert.equal(regularizarEfetividade(resumo().tramitacao, 0.2), 0)
const etapas = ['PRONDEPLEN', 'APRVD', 'RMSAN', 'TNJR']
const taxas = etapas.map((s) => regularizarEfetividade(resumo(historico(situacao(s))).tramitacao, 0.2))
assert.deepEqual(taxas, [...taxas].sort((a, b) => a - b))
const misto = resumo(pl, { ...pl, identificacao: 'PEC 1/2023' }).tramitacao
assert.ok(misto.amostraEfetiva < 2)
})
test('percentis empíricos suportam maioria de zeros, empates, referência e ausência de propostas', () => {
assert.deepEqual(percentisEfetividade([0, 0, 0, 1], [true, true, true, true], [true, true, true, true]), [37.5, 37.5, 37.5, 87.5])
assert.deepEqual(percentisEfetividade([0, 0], [true, true], [true, true]), [0, 0])
assert.deepEqual(percentisEfetividade([0, 1, 2], [true, true, false], [false, true, true]), [0, 50, 100])
})
function ambiente(): Env {
const cache = new Map<string, unknown>()
return { LEGIS_BASE_URL: 'https://legis.test', ADMIN_SECRET: 'test', SENADO_CACHE: {
get: async (k: string) => cache.get(k) ?? null,
put: async (k: string, v: string) => { cache.set(k, JSON.parse(v)) },
delete: async (k: string) => { cache.delete(k) },
} } as unknown as Env
}
test('coleta integrada: deduplica, filtra janela, reaproveita detalhe entre autores e não lê cache v2', async () => {
const env = ambiente()
const original = globalThis.fetch
let detalhes = 0
globalThis.fetch = (async (input) => {
const u = new URL(String(input))
if (u.pathname.endsWith('/processo')) {
assert.equal(u.searchParams.get('dataInicioApresentacao'), '2023-02-01')
return Response.json([pl, pl, { ...pl, id: 2, dataApresentacao: '2022-01-01' }, { ...pl, id: 3, dataApresentacao: '2028-01-01' }])
}
detalhes++
return Response.json({ ...historico(situacao('RMCD')), documento: { dataApresentacao: pl.dataApresentacao } })
}) as typeof fetch
try {
const [a, b] = await Promise.all([avaliarAutorias(env, '1'), avaliarAutorias(env, '2')])
assert.equal(a.length, 1)
assert.equal(b[0].tramitacao.pontos, 2)
assert.equal(detalhes, 1)
assert.equal((await avaliarAutorias(env, '1', { inicio: '2025-01-01', fim: '2025-12-31' })).length, 0)
assert.equal((await avaliarAutorias(env, '1', { inicio: '2023-02-01', fim: '2023-12-31' }))[0].tramitacao.pontos, 0)
} finally { globalThis.fetch = original }
})
test('falha em detalhe impede getRawDimensions de produzir falso zero/cache parcial', async () => {
const env = ambiente()
const original = globalThis.fetch
globalThis.fetch = (async (input) => String(input).includes('/processo?') ? Response.json([pl]) : Response.json({ erro: 'falha' })) as typeof fetch
try { await assert.rejects(getRawDimensions(env, '1'), /formato de resposta inválido/) }
finally { globalThis.fetch = original }
})
function score(codigo = '1'): IdsScore {
return {
senadorCod: codigo, nome: 'Senador de teste', partido: 'PXX', uf: 'SP', bloco: 'PXX',
idsTotal: 50, dimProdutividade: 50, dimEfetividade: 50, dimParticipacao: 50,
dimFiscalizacao: 50, dimCeap: 50, dimTransparencia: 50, autoriasTotal: 1, autoriasAprovadas: 1,
votacaoPresentes: 1, votacoesTotal: 1, relatoriasTotal: 1, discursosTotal: 1, apartesTotal: 0,
ceapTotalAno: 0, mesesAtivos: 10, metodologiaVersao: 'v5',
tramitacao: resumo(historico(situacao('TNJR'))).tramitacao, efetividadeAjustada: 0.4,
}
}
test('migração idempotente e round trip SQLite preservam versão e decomposição; bind é imutável', async () => {
const db = new Database(':memory:')
const env = ambiente()
env.SENADO_DB = new SqliteD1(db) as unknown as D1Database
db.exec(readFileSync(new URL('../src/db/schema.sql', import.meta.url), 'utf8'))
try {
for (let i = 0; i < 2; i++) {
const res = await app.request('/admin/migrate', { method: 'POST', headers: { 'X-Admin-Secret': 'test' } }, env)
const body = await res.json() as { results: { ok: boolean }[] }
assert.ok(body.results.every((r) => r.ok))
}
await persistRanking(env, [score('1'), score('2')])
const snap = await getRankingSnapshot(env)
assert.deepEqual(snap?.scores.map((s) => s.senadorCod).sort(), ['1', '2'])
assert.equal(snap?.metodologiaVersao, 'v5')
assert.deepEqual(snap?.scores[0].tramitacao, score().tramitacao)
assert.equal(snap?.scores[0].efetividadeAjustada, 0.4)
const api = await app.request('/api/ranking', undefined, env)
assert.equal((await api.json() as { metodologiaVersao: string }).metodologiaVersao, 'v5')
db.exec("UPDATE ranking_snapshots SET metodologia_versao='v4', tramitacao_json=NULL, efetividade_ajustada=NULL")
const legado = await getRankingSnapshot(env)
assert.equal(legado?.scores[0].tramitacao, undefined)
assert.equal(snapshotsComparaveis(snap!.scores, legado!.scores), false)
assert.equal(snapshotsComparaveis(snap!.scores, snap!.scores), true)
} finally { db.close() }
})
test('batch SQLite desfaz todos os inserts se qualquer instrução falhar', async () => {
const db = new Database(':memory:')
db.exec('CREATE TABLE teste (id INTEGER PRIMARY KEY)')
const d1 = new SqliteD1(db)
const stmt = d1.prepare('INSERT INTO teste VALUES (?)')
try {
await assert.rejects(d1.batch([stmt.bind(1), stmt.bind(1)]))
assert.equal((db.prepare('SELECT COUNT(*) AS n FROM teste').get() as { n: number }).n, 0)
} finally { db.close() }
})
test('premiação usa proporção ponderada e rejeita mistura de metodologias', () => {
const ctx = { edicao: 'leg57-2025', legislatura: 57, ano: 2025, recorte: '2025', snapshot: 'teste', congelado: true, regua: { ...REGUA_ANO, minLinhas: 1 } }
const p = apurarScores([score()], ctx)
assert.equal(p.metodologiaVersao, 'v5')
assert.equal(p.categorias.find((c) => c.id === 'efetividade')?.vencedor.indicador.valor, 100)
assert.throws(() => apurarScores([score(), { ...score('2'), metodologiaVersao: 'v4' }], ctx), /mistura versões/)
})