# `MG-8 QA-1` — Mapa de cobertura da migração

**Story:** `docs/stories/MG-8-paridade.md` · **Data:** 07/08/2026
**Gerado por:** `python3 docs/cobertura.py` (rode antes de acreditar nesta tabela)

O risco escrito na própria story é este: *"é a story mais fácil de fingir que foi
feita"*. Por isso o mapa é **script**, não tabela escrita à mão — tabela à mão
envelhece em silêncio, e quem lê não tem como saber.

O critério do script é rigoroso de propósito: um caso conta como verificado
quando **o id dele aparece** num teste da stack nova (rule, Function, widget ou
device) ou como origem declarada de um caso `MG-*`. Citação de id é o que permite
comparar linha a linha com a execução do NestJS.

## Onde estamos

| | casos | |
| -- | -- | -- |
| verificados com o id na stack nova | **397** | 94% |
| reescritos, com justificativa | 22 | 5% |
| cancelados | 1 | — |
| **a verificar** | **0** | — |
| **total nos planos da fase 1** | **420** | |

**As 38 stories da fase 1 estão em 100%.** Rode `python3 docs/cobertura.py` antes
de acreditar nesta linha — é literalmente o motivo de o mapa ser script.

Chegar a zero levou dois dias e passou por três formas de trabalho bem
diferentes, e vale distinguir as três — porque só uma delas é "escrever teste":

1. **Reexecutar a regra na stack nova**, mantendo o id de origem. É o trabalho de
   verdade: 130 casos novos em sete arquivos de paridade.
2. **Etiquetar teste que já existia e passava.** Sem o id, ninguém consegue dizer
   se o caso do plano está coberto ou se o teste é sobre outra coisa. Não é
   fachada: é a diferença entre saber e achar — e é essa resposta que autoriza
   desligar o NestJS.
3. **Registrar o que deixou de existir**, com o que passou a valer no lugar. São os
   22 do grupo B. "Reescrito e registrado" só é honesto quando a justificativa está
   escrita ao lado; por isso ela mora neste documento, e o script a lê daqui.

### Grupo A — teste existe, etiqueta não

Casos de tela cobertos pelos **244 testes de widget**, que continuam passando e
não dependem do backend: eles falam com repositório falso. O que falta é o id no
nome do caso. Não há risco de regressão silenciosa aqui; há risco de contabilidade
errada — que é o que este mapa existe para evitar.

Em 07/08 esse grupo encolheu bastante: `PP-3` (contagem de prazo), `PP-4`
(rascunho), `AC-1` (tela de cadastro) e `AC-3` (cold start) tinham teste passando
**sem o id no nome**. Etiquetar não é trabalho de fachada: sem o id, ninguém
consegue dizer se o caso do plano está coberto ou se o teste que existe é sobre
outra coisa — e a resposta muda a decisão de desligar o NestJS.

### Grupo B — o mecanismo deixou de existir

Casos que testavam coisa que o Firebase passou a fazer, ou que deixou de existir na
arquitetura nova. O critério da story é explícito: **reescrever e registrar, nunca
descartar em silêncio**. O `docs/cobertura.py` lê esta tabela e conta estes casos à
parte — é por isso que ela precisa ficar aqui e não numa lista no código.

| Caso | O que testava | O que vale agora |
| -- | -- | -- |
| `AC-1-02` | senha e hash nunca voltam na resposta | não existe resposta nossa com senha: a credencial nunca passa pelo nosso servidor |
| `AC-1-06` | campo fora do contrato é recusado | não há DTO nosso no cadastro; o Auth aceita e-mail e senha, e nada mais |
| `AC-2-01`, `AC-3-01` | login devolve access e refresh token | não há token nosso. O SDK guarda e renova a sessão, e `AC-3-02`/`AC-3-03` cobrem o efeito que interessa |
| `AC-2-03`, `AC-2-04` | limite de tentativas de login | passa a ser do Auth (`too-many-requests`), com política do Google — e o app já trata o código |
| `AC-2-05`, `AC-3-09` | sessões independentes por device | o Auth mantém sessão por instalação; não há sessão nossa para derrubar |
| `AC-2-09` | ausência de recuperação de senha | **deixou de valer**: o Auth a dá de graça, e `AC-4` voltou ao escopo |
| `AC-3-05`, `AC-3-06`, `AC-3-10` | rotação e revogação de refresh token | o SDK rotaciona; revogar é `revokeRefreshTokens` do Admin, fora do nosso caminho |
| `AC-3-07`, `AC-3-08` | logout invalida o access token na hora | `signOut()` limpa localmente; o token expira em 1 h e não há sessão nossa a invalidar |
| `CC-7-07`, `CC-7-08`, `PR-3-07` | paginação com faixa validada pela API | a lista de participantes é uma consulta com limite do SDK; não há mais faixa nossa para validar. O ranking pagina por limite, e o custo está medido |
| `CV-3-03` | encerrar antecipado fecha o prazo dos jogos abertos | **não portado de propósito**: o jogo é global, e mexer no `kickoffAt` fecharia o prazo dos outros bolões. A rule do palpite já recusa bolão encerrado (`CV-3-04`, verificado) |
| `PP-1-03` | rodada inteira em uma requisição | a leitura da rodada é uma consulta de jogos + uma dos meus palpites: 11 documentos, medido em `QA-5`. "Uma requisição" era propriedade da API, não do produto |
| `PP-10-10` | job desabilitado não roda | virou configuração do Cloud Scheduler: pausar o job é ação de console, não código |
| `PP-10-11` | trava de sincronização concorrente | virou lock em documento (`MG-6-03`/`MG-6-03b`) |
| `PP-8-10`, `PP-8-11`, `PP-8-12`, `PR-7-10` | permissão de notificação, deep link do push, iOS | dependem de FCM e de app registrado no projeto real (`MG-6 BE-4`) |
| `PR-3-11` | ranking sem N+1 | passa a medir **leituras**: 50 para uma página de 50 num bolão de 60 |

### Grupo C — falta reexecutar

O resto: casos de regra que existem igual nos dois backends e ainda não foram
reexecutados com o id na stack nova. É o trabalho de `QA-3`, e é onde ficam os
piores riscos por story:

- ✅ `PP-8` **saiu do zero em 08/08**, e a lição é sobre o mapa: ela estava em zero
  por falta do job, não por falta de push. O que travava era `BE-4` (entrega), e
  **decidir quem lembrar** não depende de entregar. Separados, `PP-8` foi a 8/12 —
  sobra permissão de notificação, deep link e iOS, que são device e credencial.
- ✅ `CV-2` (7/9) e `PR-6` (10/10) saíram do zero em 07/08. Em `CV-2` só sobraram
  os dois casos de tela. **Elas não eram difíceis: eram as que ninguém tinha
  olhado**, que é exatamente o que o mapa existe para revelar.
- ✅ `CC-5`, `CC-8`, `PR-1` e `PR-6` em **100%**. `CC-1`, `CC-4`, `CV-5`, `CV-7` e
  `PP-3` a um ou dois casos de fechar, e o que sobra neles é tela.
- ✅ Oito stories em **100%**: `CC-3`, `CC-5`, `CC-6`, `CC-8`, `CC-10`, `PP-6`,
  `PP-7` e `PR-1`.
- 🟡 O que sobra concentra-se em `AC-*` (mecanismo do Auth, grupo B) e em casos de
  tela sem teste correspondente.
- ✅ `PR-1` (cálculo do ponto) em 100% — portada com os 14 casos.

## O que já foi reexecutado com o id original

O arquivo `paridade-seguranca.spec.ts` traz **34 casos** de sigilo, prazo, papel,
autorização e convite mantendo o id de origem: `PP-7-05` continua sendo
`PP-7-05`. Foi o grupo escolhido primeiro de propósito — é o que a story aponta
como o primeiro a sumir quando "os testes passam" basta como prova.

Duas coisas mudaram de **meio** e estão registradas caso a caso: onde o NestJS
recusava com um `if` num service, hoje recusa **o banco** (rule) ou **a callable**.
Para quem chama a API direto, a diferença é grande: não existe mais resposta para
filtrar.

## `QA-5` — custo medido, não estimado

Medido com um contador em volta do Firestore, rodando os fluxos de verdade contra
o emulador (`custo.spec.ts`). O Firestore cobra por **documento**, então é
documento que se conta.

| fluxo | leituras | escritas |
| -- | -- | -- |
| criar bolão (10 jogos) | 13 | 2 |
| entrar por convite | 14 | 1 |
| abrir a rodada no app (10 jogos) | 11 | 0 |
| palpitar na rodada (10 jogos) | 0 | 10 |
| ranking geral (página de 50, bolão de 60) | 50 | 0 |
| ranking da rodada (60 participantes) | 72 | 0 |
| extrato da rodada | 22 | 0 |
| quem não palpitou (60 participantes) | 82 | 0 |
| encerrar o bolão | 70 | 1 |

**O requisito não funcional está cumprido:** o ranking geral custa uma leitura por
**linha da página**, não por participante nem por palpite. Somar na leitura seria
60 × 10 × 38 documentos por abertura de tela — o free tier acabaria antes do
returno do campeonato. É o que justifica a classificação materializada.

O que **cresce com o bolão** é a cobrança da rodada (82 para 60 participantes) e o
encerramento (70). São ações raras — o admin abre a cobrança uma vez por rodada, e
encerra uma vez por temporada. Com o free tier em 50 mil leituras/dia, um bolão de
60 pessoas gasta por rodada, no pior caso, algo como:

- 60 pessoas × (11 abrir a rodada + 22 extrato + 50 ranking) ≈ **5.000 leituras**
- \+ 82 da cobrança + 72 do ranking da rodada ≈ 5.150

Uma rodada por semana, com folga de 10× para o participante que abre o app várias
vezes ao dia. **Cabe.**

## As duas decisões do dono, respondidas em 07/08

| Decisão | Resposta | O que muda |
| -- | -- | -- |
| *Email enumeration protection* | **já estava ligada** | `AC-2-02` sai de bloqueado. O login não distingue senha errada de e-mail inexistente, e o app já tratava os dois com a mesma mensagem — `traduzirAuth` mapeia `user-not-found`, `wrong-password` e `invalid-credential` para o mesmo texto. |
| Identity Platform | **ligado** | O mínimo de 8 caracteres passa a ser do **servidor**, não só do app. `AC-1-05` deixa de ser lacuna conhecida. |

**As duas se verificam por configuração, não por teste automatizado.** O emulador
do Auth não aplica política de senha nem proteção de enumeração — ele aceita
qualquer coisa de propósito, para o teste não depender de nuvem. Verificar isso na
suíte exigiria falar com o projeto real, que é justamente o que a story proíbe.
Ficam registradas aqui, com data, e conferidas na verificação em device.

## O que falta para `MG-8` fechar

- `QA-3`: reexecutar o resto do grupo C. `PP-3`, `CV-5` e `PP-4` são os maiores
  buracos; `PP-8` fica bloqueado até `MG-6 BE-3`/`BE-4`.
- `QA-4`: **uma divergência registrada** — a primeira, abaixo.

## `QA-4` — divergências encontradas

### `CV-4-01` — quem saía continuava no ranking

**Encontrada em 08/08, ao reexecutar o caso. Corrigida no mesmo dia.**

No NestJS o ranking era **somado na leitura** a partir de quem estava ativo: sair
do bolão tirava a pessoa da lista na hora. No Firestore ele é agregado
materializado, e a classificação só era reescrita na **apuração** — quem saía
continuava aparecendo até o próximo jogo terminar, o que pode ser semana que vem
ou nunca (bolão encerrado não tem próximo jogo).

Eram dois defeitos empilhados, e o segundo é o que dá o susto:

1. A mudança de vínculo não disparava recálculo. Resolvido no gatilho que já
   cuidava do índice — mudou o vínculo, recalcula.
2. **O recálculo não apagava linha de quem saiu.** Ele reescrevia a de cada
   participante ativo, e o que não é reescrito **fica**. Agregado materializado só
   está correto quando alguém cuida do que sobra.

É exatamente o tipo de coisa que a migração introduz sem avisar: a regra não
mudou, o lugar mudou, e o novo lugar tem uma propriedade que o antigo não tinha —
memória.
