Skip to content

feat(dev): harness só-de-dev das telas pós-corrida (Results/pódio) com fixtures (#37) - #63

Merged
caioross merged 1 commit into
mainfrom
auto/issue-37-results-harness
Jul 26, 2026
Merged

feat(dev): harness só-de-dev das telas pós-corrida (Results/pódio) com fixtures (#37)#63
caioross merged 1 commit into
mainfrom
auto/issue-37-results-harness

Conversation

@caioross

Copy link
Copy Markdown
Owner

Contexto

A tela de Resultado/pódio só é alcançável depois de uma corrida multiplayer completa
(lobby → countdown → racing → finished). O Retro W28 (D#15)
registrou 3 PRs de UI (#11, #23, #26) mergeadas com o Resolvedor declarando honestamente
"a tela fica atrás de corrida concluída, inviável headless". Este PR paga essa dívida: é
enabler, não feature — destrava verificação em browser para a #22 (share no Results)
e para as telas pós-corrida seguintes.

Segui o plano do 🏛️ Parecer do Conselho, com uma divergência (item 1 abaixo).

O que mudou

Arquivo O quê
src/app/(dev)/harness/results/page.tsx rota do harness, gate por NODE_ENV
src/app/(dev)/harness/results/HarnessClient.tsx monta <Results> dentro de <ToastProvider> + navegação por cenário
src/components/dev/results.fixtures.ts 5 cenários tipados por src/lib/types.ts
src/components/Results.tsx guard do crash com 0 jogadores (+17/−1)
src/app/robots.ts /harness/ no disallow, junto com /room/
README.md parágrafo de dev com a tabela de cenários

1. Divergência do parecer: a rota não pode ser src/app/_harness/. No App Router,
pasta com prefixo _ é private folder — fica fora do roteamento, então /_harness/results
daria 404 também em dev e o harness nasceria morto. Usei route group: src/app/(dev)/harness/results/
→ URL /harness/results. O (dev) não entra na URL, só agrupa.

2. Fixtures tipados pela fonte real. SCENARIOS satisfaz RoomState de src/lib/types.ts
— nada de shape paralelo. startedAt é um instante fixo (não Date.now()), então os tempos
da tabela são determinísticos entre recargas e screenshots de PR são comparáveis. Dados 100%
sintéticos: nenhum nick real de partida, nenhuma chamada a Supabase/Realtime.

3. O cenário empty achou um bug real — exatamente o que o Conselho previu. Com
ranked.length === 0, solo/duo são false, screenOrder = [1,0,2] e podium[1].id
estoura. Reproduzido rodando o harness contra o Results.tsx de origin/main:

ANTES (Results.tsx de origin/main) — ?n=empty
  Cannot read properties of undefined (reading 'id')
DEPOIS — ?n=empty renderiza "🏁 // ninguém terminou esta corrida"

4. Cenários: empty (0) · solo (1, SoloHero) · duo (2, grid 2 col) · trio (3,
padrão) · crowd (5, com 2 não-finalizadores para exercitar Results.tsx:297-299).
&leader=0 alterna para a visão de quem não é líder (// aguardando líder reiniciar...).
Cenário desconhecido cai no default em vez de quebrar.

Como foi validado (resultado real)

pnpm install --frozen-lockfile   OK
pnpm typecheck                   OK
pnpm build                       OK (após rm -rf .next)
pnpm test                        OK — 34 testes, 2 arquivos
validate-metrics.mjs             OK — 37 passaram, 0 falharam
validate-persistence.mjs         OK — 49 passaram, 0 falharam
pnpm lint                        N/A — não há config ESLint no repo

AC "zero pegada em produção" — grep no output servido (.next/static + .next/server,
build limpo):

coderacer_harness_fixture_37   -> 0 ocorrencias
FXTR37                         -> 0 ocorrencias
fx-rhea                        -> 0 ocorrencias
fx-me                          -> 0 ocorrencias
ninguem terminou               -> 0 ocorrencias

/harness/results no build de produção: 146 B / 87.3 kB First Load JS — idêntico a
/_not-found, ou seja, nenhum JS de cliente do harness foi emitido. Com next start, a URL
renderiza a tela de 404 do app, com zero conteúdo do harness no HTML.

First Load JS de /room/[id] não regrediu (build de baseline em worktree separada de
origin/main):

baseline origin/main esta branch
/room/[id] 11.9 kB / 221 kB 12 kB / 221 kB

Os +0.1 kB são o estado vazio adicionado ao Results.tsx; o First Load JS não mudou — sinal
de que o import() dinâmico pegou.

Verificação em browser (next dev na worktree, os 5 cenários + toggle de líder abertos e
lidos): todos renderizam. Screenshot não foi possível nesta rodada — a Browser pane não
compositava frames em execução headless (Screenshot timed out: the Browser pane is not displayed); a evidência acima é o texto renderizado extraído de cada tela.

Riscos e limitação conhecida

  • Status HTTP em produção é 200, não 404. A rota renderiza a UI de 404 sem uma linha do
    harness, mas o status sai 200 porque o app tem src/app/loading.tsx na raiz: o shell é
    enviado antes de o notFound() resolver. Medi três variantes — notFound() no corpo async,
    guard síncrono, force-dynamic e guard em generateMetadatatodas respondem 200.
    É propriedade do app, não do gate; o AC (bundle) está cumprido, e o robots.txt cobre o
    lado de indexação. Se o Doctor quiser 404 duro, o caminho é middleware — fora do escopo mínimo.
  • page.js.nft.json (manifesto de trace de deploy) lista o caminho de HarnessClient.tsx.
    É uma string de path num manifesto conservador do Next, não código: o page.js compilado
    tem zero referências a ele, e nenhum fixture aparece no trace.
  • Área sagrada intacta: nada no caminho da corrida (Race/textarea/latência) foi tocado.
    A única mudança fora do harness é o guard de estado vazio no Results, que só roda na tela
    pós-corrida.
  • Classificação HANDBOOK §7.3 (normal): não toca src/app/api/rooms/**, src/lib/supabase.ts,
    anti-cheat, migrations, CI, nem adiciona dependência.

Closes #37

A tela de Resultado/pódio só existia depois de uma corrida multiplayer completa
(lobby → countdown → racing → finished), então PRs de UI dessas telas shipavam sem
verificação em browser (3 casos registrados no Retro W28, D#15).

- `src/app/(dev)/harness/results` monta `<Results>` com dados sintéticos em
  `/harness/results`, com 5 cenários (0/1/2/3/5 jogadores) e toggle de líder.
- `src/components/dev/results.fixtures.ts` tipado por `src/lib/types.ts` — se
  `RoomState`/`Player` mudarem, o typecheck quebra aqui em vez de deixar o harness
  divergir da tela real. Sem Supabase/Realtime, sem nick de jogador real.
- Gate por `NODE_ENV` (variável de build) + `import()` dentro do ramo: no build de
  produção o webpack elimina harness, client e fixtures — zero pegada no output.
- `/harness/` entra no disallow do robots.txt, junto com `/room/`.

O cenário de 0 jogadores achou um bug real na tela: `podium[1]` num array vazio
estourava `TypeError: Cannot read properties of undefined (reading 'id')`
(Results.tsx:87-96). Corrigido com um estado vazio.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

@caioross is attempting to deploy a commit to the caioross' projects team on Vercel, but is not a member of this team. To resolve this issue, you can:

  • Make your repository public. Collaboration is free for open source and public repositories.
  • Upgrade to pro and add @caioross as a member. A Pro subscription is required to access Vercel's collaborative features.
    • If you're the owner of the team, click here to upgrade and add @caioross as a member.
    • If you're the user who initiated this build request, click here to request access.
    • If you're already a member of the caioross' projects team, make sure that your Vercel account is connected to your GitHub account.

To read more about collaboration on Vercel, click here.

@caioross

Copy link
Copy Markdown
Owner Author

✅ Parecer PR Doctor — APROVADA (Normal §7.3), aguardando próxima rodada

Diff lido inteiro. Classificação Normal: harness só-de-dev sob route group (dev), gate por NODE_ENV com import() dinâmico (fixtures fora do bundle de prod), robots.ts cobre indexação. A única mudança fora do harness é o guard de estado vazio no Results.tsx (EmptyPodium) — fixa um crash real (podium[1] num array vazio) e não toca a área sagrada. Não colide com o share button da #57 (regiões distintas do Results.tsx; segue MERGEABLE mesmo após #57 mergear). CI verde.

Não mergeada nesta rodada apenas pelo teto de 2 merges/rodada (HANDBOOK §8) — já foram #57 e #60. Está aprovada e primeira da fila para a próxima rodada; nenhum reparo pendente. A limitação honesta de status 200 (não 404) em prod, com zero conteúdo do harness no bundle, está documentada no corpo e é aceitável (o AC é bundle, e robots cobre o resto).

@caioross

Copy link
Copy Markdown
Owner Author

✅ PR Doctor — merge (Normal §7.3)

Reconfirmado MERGEABLE, CI (Install/typecheck/build) verde. O check "Vercel" vermelho aqui é o preview obsoleto de 24/jul (integração Git num estado quebrado na época), o mesmo padrão sob o qual #57/#60/#74/#75/#77/#78 já mergearam e estão live em produção — ou seja, a integração atual funciona; o merge dispara o deploy de produção sob ela. Sem reparo pendente. Closes #37. Squash-merge.

@caioross
caioross merged commit 052f116 into main Jul 26, 2026
2 of 3 checks passed
@caioross
caioross deleted the auto/issue-37-results-harness branch July 26, 2026 13:18
caioross added a commit that referenced this pull request Jul 26, 2026
Achado do quorum: congelar a lista da rodada reintroduzia sala presa. Se todos
os competidores saem durante racing e resta so um retardatario-espectador,
racers fica [] e shouldFinishRace (guarda de lista vazia) nunca encerra. Agora
o lider posta finish com results vazios (tela de Results vazia da #63) apos o
countdown, liberando a sala.
caioross added a commit that referenced this pull request Jul 26, 2026
…lista da rodada (#64) (#80)

* feat(multiplayer): entrada após o início vira espectador — congela a lista da rodada (#64)

A lista de participantes ficava aberta durante toda a partida: quem entrasse com a
corrida já em `racing` passava a compor a mesma rodada, e todos que já tinham
terminado ficavam presos esperando o retardatário terminar ou desistir. Uma partida
deve ser composta por quem estava presente quando ela COMEÇOU.

- `isSpectatorJoin(joinedAt, startMs, status)` em `src/lib/room.ts` — puro e
  determinístico (joinedAt e start_at são compartilhados por presence + linha da
  sala, então todos os clientes classificam igual). Espectador = entrou depois do
  start (`joinedAt > startMs`) com a rodada em andamento.
- `useRoom`: cada `LivePlayer` ganha `spectator`; o gate de fim de corrida passa a
  ignorar espectadores (`racers = players.filter(!spectator)`), então um retardatário
  sem `finishedAt` não prende mais a sala. Expõe `isSpectator` para a UI.
- `RoomView`: espectadores saem da pista/ranking (filtrados do roster de competidores)
  e veem a nova `SpectatorView` — banner "Partida em andamento", pista ao vivo e chat.
  Entram automaticamente na próxima rodada (start_at muda → deixam de ser espectadores).
- 6 casos novos em `room.test.ts` cobrindo a classificação (limite exato, lobby,
  finished, sem corrida).

* fix(#64): rede de seguranca — lider encerra se so restam espectadores

Achado do quorum: congelar a lista da rodada reintroduzia sala presa. Se todos
os competidores saem durante racing e resta so um retardatario-espectador,
racers fica [] e shouldFinishRace (guarda de lista vazia) nunca encerra. Agora
o lider posta finish com results vazios (tela de Results vazia da #63) apos o
countdown, liberando a sala.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Infra/UI: harness só-de-dev para verificar telas pós-corrida (Results/pódio) sem jogar

1 participant