Iniciativa Cidadã Independente · Transparência Parlamentar
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/)
})