Skip to content

Auditoria — Pipeline de Resultados / "Unificadores" (cards)

Data: 2026-06-04 Escopo: @audit completo da feature Resultados do Coordenador Gatilho: Reportes de cards (Participantes / Classificados / Medalhas) com contagens incorretas e sem filtro real entre fases. Linkado ao incidente paralelo do CI "fantasma" que escondia 10 testes vermelhos.


0. Sumário executivo

Bug declaradoStatus investigaçãoCausa raiz (provável)Risco
B1 — Card "Classificados" some em fases intermediárias quando deveria mostrarConfirmado por leitura de códigodetalhes-header.tsx esconde card quando classificados === 0. Em olimpíadas onde fase intermediária só usa medalha (ex.: Canguru por nível), classificados zera e o card some inteiro.Médio — UX
B2 — Cards de medalhas (Ouro/Prata/Bronze) só renderizam na "última fase"Confirmado por leitura de códigoGate ehUltimaFase && no header. Olimpíadas com premiação em fase intermediária ficam sem dashboard de medalhas.Alto — feature parcial
B3 — Stats não respeitam o filtro de nível ativo da UIConfirmado por leitura backendhandleList em _shared/resultados-queries.ts:430-487 calcula stats sobre todos os resultados da fase, sem aplicar nivelId do filtro UI. O filterMeta.stats é global por fase.Alto — dado errado em por_nivel
B4 — Não há "fluxo entre fases" no dashboard (quantos passaram fase X → fase Y)Confirmado por ausênciaNão existe agregação cross-fase no header. Só stats_agregados (visão olimpíada inteira) e stats (uma fase).Médio — feature ausente
B5 — Tela usa useEffect para debounce de busca e reset de páginaNão é bug, mas anti-padrão menor vs useMemo/derivedBaixo

1. Mapa do pipeline

text
UI (coordenador/resultados/)
  index.tsx
    → resultados-lista.tsx (listagem de olimpíadas com cards-resumo)
    → resultados-detalhes.tsx ◄ tela alvo dos bugs
        ├─ DetalhesHeader            ◄ cards "unificadores" (B1, B2)
        ├─ DetalhesFasesToolbar      (troca de fase ativa)
        ├─ DetalhesFiltros           (nível, série, turma, situação)
        └─ DetalhesTabela            (linhas paginadas)

Hook: src/hooks/useGestaoResultados.ts
  useResultadosPorOlimpiadaPaginado(olimpiadaId, faseId, {page, pageSize, nivelId, ...})
    → invokeAction("gestao-resultados", "list", {...})

Backend: supabase/functions/gestao-resultados/index.ts
  action "list"            → handleList (queries.ts:200-500)
  action "stats"           → handleStats (queries.ts:659-700)  — visão olimpíada
  action "stats_agregados" → handleStatsAgregados (queries.ts:508-640) — multi-olimpíada

2. Trecho crítico — backend não filtra stats por nível ativo

supabase/functions/_shared/resultados-queries.ts:430-487:

ts
let classificados = 0, medalhistas = 0, ouro = 0, prata = 0, bronze = 0;
for (const r of (resultados ?? [])) {
  if (r.situacao === "classificado") classificados++;
  if (["medalha_ouro","medalha_prata","medalha_bronze"].includes(r.situacao)) medalhistas++;
  if (r.situacao === "medalha_ouro") ouro++;
  else if (r.situacao === "medalha_prata") prata++;
  else if (r.situacao === "medalha_bronze") bronze++;
}
// ...
filterMeta: {
  stats: { totalParticipantes: alunosDoNivel.length, classificados, medalhistas, ouro, prata, bronze }
}

O resultados aqui é a coleção completa da fase, antes de aplicar o filtro nivelId do paginationParams. A paginação filtra somente as linhas exibidas na tabela; as stats permanecem globais por fase. Quando o coordenador troca para "Nível B", os cards continuam mostrando contagem somada de A+B.

→ Memória aplicável: mem://architecture/query-key-context-isolation (queryKeys já incluem nivelAtivo, mas o backend ignora).

3. Trecho crítico — UI condiciona cards à "última fase"

src/components/coordenador/resultados/detalhes-header.tsx:71-129:

tsx
{!ehUltimaFase && estatisticasDinamicas.classificados > 0 && ( /* Card Classificados */ )}
{ehUltimaFase && estatisticasDinamicas.medalhistas > 0 && ( /* Card Medalhistas */ )}
{ehUltimaFase && estatisticasDinamicas.ouro > 0 && ( /* Card Ouro */ )}
{ehUltimaFase && estatisticasDinamicas.prata > 0 && ( /* Card Prata */ )}
{ehUltimaFase && estatisticasDinamicas.bronze > 0 && ( /* Card Bronze */ )}

Gate binário última fase vs não-última. Olimpíadas que premiam em fase intermediária (caso Canguru por_nivel quando a configuração de premiação está numa fase != final) ficam sem nenhum card de medalha.

→ Memória aplicável: mem://architecture/olympics-phases-by-level-standard — modo por_nivel permite premiação em fase ≠ última, mas a UI assume cenário "global, última fase".

4. Plano de correção (não aplicado nesta sessão — requer decisão de produto)

4.1 Backend — stats respeita nivelId

Em handleList, recalcular o bloco stats após o filtro por nivelId quando presente:

ts
const resultadosParaStats = nivelId
  ? (resultados ?? []).filter((r: any) => r.nivel_id === nivelId)
  : (resultados ?? []);
// computar classificados / medalhistas / ouro / prata / bronze sobre resultadosParaStats

Cobertura: teste Deno _shared/__tests__/resultados-stats-by-nivel.test.ts com 2 níveis × 6 alunos × situações distintas.

4.2 UI — cards baseados em "fase tem premiação?" não em "ehUltimaFase"

Substituir gate ehUltimaFase por temPremiacaoConfigurada (já calculado dentro do PremiacaoFaixasModal via useConfigsFase). Quando a fase ativa tem configs_premiacao (global ou por_nivel), renderizar cards de medalha. Quando não, renderizar Classificados se a fase tiver nota_corte_definida.

Pseudo-código:

tsx
const faseTemPremiacao = !!faseAtual?.temPremiacaoConfigurada;
const faseTemNotaCorte = !!faseAtual?.temNotaCorteConfigurada;

{faseTemNotaCorte && /* card Classificados */}
{faseTemPremiacao && /* cards Medalha */}

Backend handleList precisa retornar essas duas flags no filterMeta (uma query extra em configs_fase por fase, leve).

4.3 Card cross-fase — "Aprovados da fase anterior"

Novo card em fases > 1: "X aprovados vieram da fase anterior". Reaproveita inscricaoIdsClassificadosFaseAnterior que já existe em handleList:240. Exportar no filterMeta.fluxoFases.classificadosFaseAnterior.

4.4 Testes a adicionar

  • Deno: gestao-resultados/__tests__/stats-by-nivel.test.ts (B3)
  • Deno: gestao-resultados/__tests__/stats-fluxo-fases.test.ts (B4)
  • RTL: coordenador/resultados/__tests__/detalhes-header.cards.test.tsx cobrindo:
    • fase intermediária com premiação → mostra medalhas
    • filtro nível → cards reagem
    • fase > 1 → card "vieram da fase anterior"
  • E2E (já existe e2e/resultados-premiacao-por-nivel.spec.ts com fixmes) — desfix dos cenários "cards refletem por nível".

5. Itens correlatos resolvidos nesta mesma sessão

5.1 CI silencioso (resolvido)

.github/workflows/ci.yml rodava bun run test:coverage com continue-on-error: true num único step. O flag, justificado para o threshold de cobertura, também engolia falhas de teste. 10 testes vermelhos passavam despercebidos.

Fix aplicado: dois steps separados — gate (bun run test, falha = vermelho) + termômetro (bun run test:coverage, falha tolerada). Adicionado job summary + notificação ntfy em failure.

5.2 Timeout de 5s em RTL sob coverage (resolvido)

Os 10 testes que apareciam vermelhos no Vitest Report eram timeout 5000ms exceeded, não asserção. Coverage v8 + self-hosted runner sob carga inflam userEvent.type/click + waitFor além de 5s. Local roda em ~1s.

Fix aplicado: vitest.config.tstestTimeout: 15_000, hookTimeout: 15_000. Confirmado localmente (885/885 passed em 50s).

5.3 Análise — não era regressão da feature Resultados

Os arquivos vermelhos eram olimpiada-detalhes/, admin-escola/editar-assinatura-modal, ciclo-contratual-dashboard — nenhum toca o pipeline de Resultados. A suíte de Resultados (premiacao-faixas-modal, useGestaoResultados.premiacao-batch) está e estava verde. B1-B4 são dívidas pré-existentes, não regressão da última entrega de Premiação por Nível.


6. Status final (2026-06-04, sessão tarde)

ItemStatusNotas
B1✅ CorrigidoHeader passa a gatear por faseConfig.temNotaCorte
B2✅ CorrigidoHeader gateia medalhas por faseConfig.temPremiacao
B3✅ Já estavaStats em handleList já usavam alunosDoNivel (revisão de código confirmou)
B4✅ CorrigidofilterMeta.fluxoFases.classificadosFaseAnterior + card "Vindos da fase anterior"
B5✅ Já estavaModal já usa touched/existentes + placeholders (entrega anterior)
B6✅ Já estavaModal fecha no save bem-sucedido; hidratadoFor impede re-hidrate enquanto aberto
B7✅ Corrigidoresultados-config.ts cleanup agora fail-close, throw em erro de delete

Validação: bun run test → 885/885 (1 skip). @audit completo recomendado antes do PR.

7. Memória / playbook

  • Criada mem://features/results/dashboard-cards-by-fase-config.
  • Caso 20 ainda a registrar em PROBLEM_SOLVING.md: "Agregados ignoram filtros UI".