# Execução parcial — `MG-6` Sincronização e lembretes no Cloud Scheduler

**Story:** `docs/stories/MG-6-jobs-scheduler.md` · **Data:** 07/08/2026
**Ambiente:** testes puros + integração nos emuladores. Suíte do Firebase: **132**
casos, 11 arquivos.

**Resultado: ⚠️ parcial — 4 de 6 tasks de backend.** `BE-3` (lembrete de prazo) e
`BE-4` (push por FCM) seguem abertas, e com elas as `FE-*` e as `OPS-*`: o push
depende de app registrado no projeto real, que é `MG-7`.

| ID | O que verifica |
| -- | -------------- |
| MG-6-01 | placar novo da fonte é aplicado |
| MG-6-02 | horário novo é aplicado — é o que reabre o palpite |
| MG-6-03 | duas concorrentes **não duplicam efeito** (o invariante) |
| MG-6-03b | com sobreposição real, a segunda é **ignorada** (o mecanismo) |
| MG-6-04 | lock abandonado não trava a sincronização para sempre |
| MG-6-05 | fonte fora do ar não sobrescreve dado bom |
| MG-6-06 | jogo que a fonte não conhece mais permanece intacto |
| MG-6-07 | nada mudou: nenhuma escrita |
| MG-6-08 | o lock é por competição — uma não bloqueia a outra |

Mais os **13 casos do adaptador** da fonte real, portados sem alterar um caso
(cota de 10 req/min, cache de 5 min, payload malformado, timeout, 403, 429).

## A trava em processo deixa de existir — e o teste teve de mudar de alvo

O defeito de 07/08 (`PP-10-11`) foi resolvido no NestJS com um `Map` em memória:
duas sincronizações simultâneas liam o placar antigo e as duas gravavam correção,
deixando o histórico do jogo duplicado e o aviso de recálculo saindo duas vezes.

**Em Functions essa trava não existe:** cada invocação é um processo novo, e duas
podem estar em máquinas diferentes. O lugar passou a ser um documento, tomado em
transação — o único jeito de duas máquinas concordarem sobre quem chegou primeiro.

Meu primeiro teste falhou afirmando que "uma das duas é ignorada", e a razão é
instrutiva: com a fonte falsa instantânea, a primeira execução **termina e solta o
lock** antes de a segunda tentar de novo (transação do Firestore repete em
contenção). As duas rodaram em sequência — e isso é **legítimo**: a segunda leu o
estado já novo e não achou nada para mudar.

O teste estava afirmando o **mecanismo** quando o que importa é o **invariante**.
Ficaram os dois casos:

- `MG-6-03` afirma o invariante: **uma** alteração de placar no total, independente
  de as duas terem rodado em paralelo ou em sequência.
- `MG-6-03b` força sobreposição real com uma fonte lenta e aí sim exige que a
  segunda seja ignorada — provando que o lock bloqueia.

`MG-6-04` cobre o outro lado do lock: **validade**. Function que morre no meio
(timeout, erro fatal) deixaria o lock preso e a sincronização pararia **em
silêncio** — que é exatamente o modo de falha que esta story existe para eliminar.

## Acoplamento que o teste expôs

`sincronizar()` chamava `getFirestore()` no corpo, dependendo do app **global** do
Admin SDK — que só existe quando o `index.ts` inicializa. O teste, que usa app
nomeado, quebrou com "The default Firebase app does not exist". A instância passou a
entrar por parâmetro, com o `getFirestore()` como padrão. Função exercitável sem
estado global é melhor por si; aqui foi o teste que cobrou.

## Uma decisão que evita dois caminhos de apuração

A sincronização **não** calcula ponto nem mexe em palpite: ela escreve o jogo, e
quem reage são os gatilhos de `MG-5` (apuração) e `MG-3` (sigilo). Se ela apurasse,
existiriam dois caminhos para a mesma regra e um deles envelheceria — e a correção
manual do admin, que entra pela mesma porta, ficaria com comportamento diferente da
sincronização.

`MG-6-07` protege o custo disso: escrita idêntica não acontece, então os gatilhos não
acordam à toa. No serviço em que custo é por operação, "escrever igual" não é
inofensivo.

## O que fecha quando `MG-6` fechar

`PP-10 OPS-1` — a task 🔴 de "hospedagem que não hiberne", aberta desde o começo do
projeto. O Cloud Scheduler não dorme. Falta o deploy e a verificação do agendamento
no projeto real.
