# Testes

## Cobertura

| Componente | Tipo | Path | Status |
|---|---|---|---|
| `ExecucaoStrategyFactory::createFromMissao` (derivação `variante_separacao`) | TDD unit (sem DB) | `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Strategies/Execucao/ExecucaoStrategyFactoryVarianteSeparacaoTddTest.php` | ✅ pass (13 asserts) |
| `SeparacaoWmsStrategy` (ordenação por endereço + legado ao fim + hot path) | TDD unit (sem DB) | `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Strategies/Execucao/SeparacaoWmsStrategyTddTest.php` | ✅ pass (12 asserts) |
| `ResolverEnderecoProdutoLaboratorio` (batch produto→endereço + normalização) | TDD integration (dev_gx_goldie) | `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ResolverEnderecoProdutoLaboratorioTddTest.php` | ✅ pass (7 asserts) |
| `App\Laboratorios\ValidarEnderecoSeparacao` (gate de bipagem de endereço + extrato) | TDD integration (dev_gx_goldie) | `componentes/Missoes/Logistica/sinais/App/Testes/Laboratorios/ValidarEnderecoSeparacaoTddTest.php` | ✅ pass (17 asserts) |
| `GestaoMissoesSeparacao::enriquecerLocalizacaoWms` + `getConfiguracoesAdicionais` (snapshot na criação + cobertura US3) | TDD integration (dev_gx_goldie, via reflection no hook) | `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesSeparacaoEnderecoWmsTddTest.php` | ✅ pass (16 asserts) |

**Total: 5 arquivos, 65 asserts, 0 falhas.**

Todos os arquivos são scripts standalone (`php <caminho>.php`), sem PHPUnit, seguindo o
padrão TDD do Sinal (`Testes/<espelho>/*TddTest.php`) já usado no restante do módulo
`AlocacaoWms`/`Designacoes`/`MissaoLogistica`.

## Decisões de escopo dos testes

- **Estratégia + Factory são unit puro (sem bootstrap de framework/DB).** `SeparacaoWmsStrategy`
  não sobrescreve `enriquecerItens()` (herda o no-op de `BaseExecucaoStrategy`), então
  `ordenarCandidatosParaExecucao()` não toca o banco — os testes rodam só com
  `require vendor/autoload.php` + os arquivos de classe, provando por construção que a
  ordenação é 0-I/O (AC2.4/AC4.4). `ExecucaoStrategyFactory::createFromMissao()` só lê
  propriedades do objeto `$missao` em memória — idem.
- **`aplicarOrdenacao()` é `protected` e recebe `array &$itens` por referência.** Chamar via
  `ReflectionMethod::invoke()` não preserva a referência (bug conhecido de reflection com
  parâmetros by-ref) — os testes usam o método público `aplicarSequenciamento()` (que delega
  1:1 para `aplicarOrdenacao()`, ver cenário 9 do TDD) e o pipeline público
  `ordenarCandidatosParaExecucao()`, que exercitam exatamente o mesmo código de produção sem
  esse problema.
- **`ResolverEnderecoProdutoLaboratorio` e `ValidarEnderecoSeparacao` são integration reais**
  (dev_gx_goldie) porque envolvem Sondas/Models (`WmsMovimentoSonda`, `WmsLocalizacaoSonda`,
  `MissaoLogisticaItem`, `MissaoLogisticaExtrato`) — nada de mock de banco (anti-padrão
  proibido). `produto_id` fictício (ex.: `999880001`) é usado nos testes porque
  `wms_movimentos.produto_id` não tem FK para `produtos` — evita colisão com catálogo real do
  tenant sem precisar de fixture de produto.
- **`GestaoMissoesSeparacao::enriquecerLocalizacaoWms` é testado via
  `ReflectionClass::newInstanceWithoutConstructor()` + invocação direta do hook protegido**
  (mesmo padrão já usado em `GestaoMissoesAlocacaoWmsTddTest.php` para o Laboratório irmão de
  Alocação WMS). O construtor de `GestaoMissoesBase` auto-dispara `processarEnvio()` completo
  no `opcao='enviarMissoes'`, que depende de romaneio/pedido reais
  (`buscarItensDoPedido`/`recuperarItensSelecionados`) — pesado e frágil demais para um teste
  focado no hook novo. A reflection isola exatamente o código que o PRD pediu para cobrir
  (RN3/RN4/US3) sem arrastar o pipeline inteiro de criação de missão.

## Gaps de cobertura (e justificativa)

- **`GestaoMissoesSeparacao::processarEnvio()` (merge de `cobertura_wms` na resposta final,
  fluxo E2E completo de criação de missão)** — não coberto por um teste ponta-a-ponta. Exigiria
  fixtures reais de romaneio + pedido + itens com relacionamento completo no
  `dev_gx_goldie` (fora do que existe pronto no banco de dev) só para exercitar código que já é
  testado nas duas metades relevantes: (1) `enriquecerLocalizacaoWms()` grava o snapshot
  correto por item — coberto; (2) o `array_merge($response, ['cobertura_wms' => ...])` em
  `processarEnvio()` é 3 linhas triviais sem lógica de negócio. Risco residual baixo.
- **`obterProximoItem` via `MissoesExecucao` real (satélite completo, incluindo
  `sp_reservar_item_da_lista` / claim `SKIP LOCKED`)** — não coberto ponta-a-ponta pelo satélite
  público (método é `private`, dispatch via Galaxia). O que o techplan pedia
  ("`obterProximoItem` numa missão WMS devolve fila por endereço sem I/O adicional") está coberto
  no nível equivalente: o cenário 9 do `ExecucaoStrategyFactoryVarianteSeparacaoTddTest`
  reproduz exatamente a chamada que `MissoesExecucao::ordenarCandidatosStrategy` faz
  (`createFromMissao($missao)` sem `$ordenacao` seguido de
  `ordenarCandidatosParaExecucao($candidatos, ...)`), e comprovadamente sem DB. Testar o satélite
  inteiro arrastaria a stored procedure de claim (`SKIP LOCKED`) e risco de deixar itens
  reservados/travados em dados reais — fora do escopo desta feature (a reserva em si não mudou).
- **`AcaoExtrato::ENDERECO_VALIDADO`** — não tem teste unit dedicado; verificado por inspeção
  (os 3 `match()` exaustivos — `descricao()`, `icon()`, `categoria()` — já cobrem o case novo, o
  enum quebraria em runtime/`php -l` não pegaria isso mas o teste de integração de
  `ValidarEnderecoSeparacao` chama `registrarExtrato()` com esse case e passa, provando que os
  três `match()` não estouram `UnhandledMatchError`).
- **Frontend (`endereco_wms.js`, `execucao_card.php`, `designar_unificado.js`)** — fora do escopo
  deste QA (JS/HTML, ver anti-padrão "testar CSS/visual"); coberto pelo smoke test manual descrito
  em `04-frontend.md`.
- **Migration** — não aplicável; o techplan e o `03-backend.md` confirmam **zero migration nova**
  nesta feature (tudo aditivo sobre colunas JSON existentes).

## Output da execução

```
=== SeparacaoWmsStrategyTddTest.php ===
====================================================
TDD: SeparacaoWmsStrategy — ordenação por endereço WMS
====================================================
--- 1. Cenário: Herança ---
✅ [PASS] SeparacaoWmsStrategy estende SeparacaoExecucaoStrategy
✅ [PASS] SeparacaoWmsStrategy estende BaseExecucaoStrategy (via SeparacaoExecucaoStrategy)
--- 2. Cenário: Ordena por código do endereço (natural sort) ---
✅ [PASS] Natural sort: A01-P1-N1, A03-P2-N2, A03-P2-N10 (não string sort puro)
--- 3. Cenário: Itens 'legado' sempre ao FIM, agrupados (AC4.2) ---
✅ [PASS] wms (por endereço) primeiro, legado (por nome) ao fim, sem intercalar
--- 4. Cenário: Ausência de origem_localizacao trata como legado (default seguro) ---
✅ [PASS] Item sem origem_localizacao cai no grupo legado (fim)
--- 5. Cenário: Desempate por nome A-Z quando endereço é igual ---
✅ [PASS] Mesmo endereço -> desempate alfabético (Abacaxi antes de Zebra)
--- 6. Cenário: Desempate final por id ASC ---
✅ [PASS] Endereço e nome iguais -> desempate por id ASC
--- 7. Cenário: dados_adicionais como STRING JSON crua (fetch sem decode) ---
✅ [PASS] Decodifica dados_adicionais em string JSON corretamente antes de ordenar
--- 8. Cenário: candidatos como array associativo (não stdClass) ---
✅ [PASS] Formato array associativo também é suportado (painel vs execução híbrida)
--- 9. Cenário: aplicarSequenciamento (Painel) e ordenarCandidatosParaExecucao (Execução) concordam ---
✅ [PASS] Sequenciamento visual (Painel) usa exatamente os mesmos critérios da fila de Execução (nenhum item pulado nesse conjunto)
--- 10. Cenário: ordenarCandidatosParaExecucao() (pipeline público, hot path) ---
✅ [PASS] Pipeline completo: wms por endereço, legado no fim, pulado por último (finalizarOrdenacao)
--- 11. Cenário: candidatos vazio -> retorna [] sem erro ---
✅ [PASS] Lista vazia não quebra o pipeline
====================================================
RESULTADO GERAL: ✅ SUCESSO
Passou: 12
Falhou: 0
====================================================
EXIT=0

=== ExecucaoStrategyFactoryVarianteSeparacaoTddTest.php ===
====================================================
TDD: ExecucaoStrategyFactory — variante SEPARACAO:WMS via configuracoes
====================================================
--- 1. variante_separacao='WMS' (JSON string, como vem do banco) -> SeparacaoWmsStrategy ---
✅ [PASS] createFromMissao() sem $ordenacao + configuracoes.variante_separacao=WMS -> SeparacaoWmsStrategy
--- 2. configuracoes SEM a chave -> default atual (SeparacaoEnderecamentoStrategy), regressão zero ---
✅ [PASS] Sem variante_separacao -> Strategy default (AC1.4/AC8)
✅ [PASS] Strategy default NÃO é SeparacaoWmsStrategy
--- 3. configuracoes vazio/null -> default atual ---
✅ [PASS] configuracoes=null -> Strategy default
✅ [PASS] configuracoes='' -> Strategy default
--- 4. valor não reconhecido (typo/valor antigo) -> fallback silencioso pro default ---
✅ [PASS] variante_separacao='wms' (lowercase, não é literal exato) -> fallback default
✅ [PASS] variante_separacao='ANDAR' (valor não mapeado) -> fallback default
--- 5. configuracoes já como array PHP (não string JSON) -> também funciona (defensivo) ---
✅ [PASS] configuracoes como array PHP com variante_separacao=WMS -> SeparacaoWmsStrategy
--- 6. $ordenacao explícito continua tendo prioridade sobre configuracoes (callers pré-existentes) ---
✅ [PASS] $ordenacao='rota' explícito vence configuracoes.variante_separacao=WMS (SEPARACAO:ROTA)
--- 7. Outro tipo de missão (CARREGAMENTO) ignora variante_separacao mesmo se presente ---
✅ [PASS] Tipo CARREGAMENTO não é afetado pela chave variante_separacao (aditivo/isolado)
--- 8. Tipo/missão inválidos -> null (comportamento pré-existente preservado) ---
✅ [PASS] tipo_missao_id ausente -> null
✅ [PASS] tipo_missao_id desconhecido -> null
--- 9. Hot path combinado: variante persiste do início ao fim SEM query nova (AC1.3) ---
✅ [PASS] Missão WMS -> fila ordenada por endereço (3,2) + legado ao fim (1), tudo derivado só de $missao->configuracoes
====================================================
RESULTADO GERAL: ✅ SUCESSO
Passou: 13
Falhou: 0
====================================================
EXIT=0

=== ResolverEnderecoProdutoLaboratorioTddTest.php (dev_gx_goldie) ===
====================================================
INTEGRATION — ResolverEnderecoProdutoLaboratorio (dev_gx_goldie)
====================================================
--- Mapa batch (com + sem + histórico) ---
[PASS] produto com alocação -> código normalizado (trim+upper) no mapa
[PASS] produto sem alocação NÃO aparece no mapa (RN4: fallback é do consumidor)
[PASS] produto com 2 alocações -> mapa traz a alocação MAIS RECENTE (C09, não B01)
--- resolverMapa([]) ---
[PASS] lista de produto_ids vazia -> []
--- resolverMapa filtra apenas os IDs pedidos (não vaza todo o mapa do tenant) ---
[PASS] resolverMapa([X]) só traz X, mesmo com outros produtos alocados no tenant
--- normalizar() — ponto único de normalização (reusado por ValidarEnderecoSeparacao) ---
[PASS] normalizar('  a03-p2-n1  ') === 'A03-P2-N1'
[PASS] normalizar() é idempotente para código já normalizado
====================================================
RESULTADO: PASS=7  FAIL=0
TODOS OS TESTES PASSARAM
EXIT=0

=== ValidarEnderecoSeparacaoTddTest.php (dev_gx_goldie) ===
====================================================
INTEGRATION — ValidarEnderecoSeparacao (dev_gx_goldie)
====================================================
--- Validações de input ---
[PASS] item_id ausente -> status=no
[PASS] código ausente -> status=no
[PASS] item inexistente -> status=no
--- Item origem=legado: libera direto (AC5.6), sem exigir bip ---
[PASS] item legado -> status=ok mesmo com código arbitrário
[PASS] item legado -> endereco_confirmado=true
--- Item origem=wms: endereço inativo/inexistente -> erro (AC5.4) ---
[PASS] endereço inativo -> status=no
[PASS] endereço inativo -> endereco_confirmado=false
[PASS] endereço inexistente no catálogo -> status=no
--- Item origem=wms: mismatch com snapshot esperado -> erro, NÃO libera (AC5.3) ---
[PASS] endereço ATIVO mas diferente do esperado -> status=no (mismatch)
[PASS] mismatch -> endereco_confirmado=false
[PASS] mismatch -> devolve endereco_esperado (snapshot do item) para o feedback
--- Item origem=wms: match (normalizado) -> libera + registra extrato (US5.8) ---
[PASS] match normalizado (espaço/minúsculo) -> status=ok
[PASS] match -> endereco_confirmado=true
[PASS] match -> registra exatamente 1 entrada ENDERECO_VALIDADO no extrato
[PASS] extrato aponta referencia_id = item_id bipado
[PASS] extrato.payload.endereco_bipado normalizado (trim+upper)
--- Alias de payload: 'endereco' funciona igual a 'codigo' ---
[PASS] payload com chave 'endereco' (alias de 'codigo') -> status=ok
====================================================
RESULTADO: PASS=17  FAIL=0
TODOS OS TESTES PASSARAM
EXIT=0

=== GestaoMissoesSeparacaoEnderecoWmsTddTest.php (dev_gx_goldie) ===
====================================================
INTEGRATION — GestaoMissoesSeparacao::enriquecerLocalizacaoWms (dev_gx_goldie)
====================================================
--- 1. Sem variante_separacao=WMS -> no-op (AC1.4/AC8) ---
[PASS] data sem variante_separacao -> itens retornados IDÊNTICOS (no-op)
[PASS] variante_separacao != 'WMS' -> no-op
--- 2. variante_separacao=WMS -> snapshot endereco_wms/origem_localizacao (RN3/RN4) ---
[PASS] produto com alocação -> origem_localizacao=wms
[PASS] produto com alocação -> endereco_wms normalizado (trim+upper)
[PASS] produto sem alocação -> origem_localizacao=legado (fallback RN4)
[PASS] produto legado -> NÃO grava endereco_wms
[PASS] dados_adicionais pré-existentes (produto_nome) são preservados, não sobrescritos
--- 3. Cobertura calculada (US3, AC3.1-3.3) ---
[PASS] coberturaWms populado após a variante WMS rodar
[PASS] cobertura.total = 2 (todos os itens)
[PASS] cobertura.sem_endereco = 1 (só o produto sem alocação)
[PASS] cobertura.itens_sem_endereco lista o produto sem endereço
--- 4. coberturaWms fica null quando a variante WMS não rodou (instância nova) ---
[PASS] nova instância (sem chamar o hook WMS) -> coberturaWms = null por padrão
--- 5. Itens vazios -> no-op mesmo com variante_separacao=WMS ---
[PASS] itens=[] -> retorna [] sem estourar (guard empty($itens))
--- 6. getConfiguracoesAdicionais(): variante_separacao só é gravada quando pedida ---
[PASS] data['variante_separacao']='WMS' -> configs['variante_separacao']='WMS'
[PASS] data sem variante_separacao -> chave AUSENTE em configs (default = andar, AC1.4)
[PASS] valor != 'WMS' (literal exato) -> chave AUSENTE em configs
====================================================
RESULTADO: PASS=16  FAIL=0
TODOS OS TESTES PASSARAM
EXIT=0
```

*(Os testes de integração emitem `PHP Warning` de ruído pré-existente do bootstrap
(`session_name()`/`session_id()` já ativa, chaves `cacheControl`/`returnData` não definidas em
`ModelInteligencia`) — mesmo padrão observado em todos os `TddTest.php` de integração já
existentes no repo (ex.: `ConsultarEnderecoLaboratorioTddTest`, `WmsMovimentoSondaMapaLocTddTest`);
não são falhas, foram omitidos do output acima para legibilidade.)*

## Limpeza de dados de teste

Todos os testes de integração usam `try/finally` para deletar as linhas criadas
(`missao_logistica`, `missao_logistica_itens` — cascade —, `missao_logistica_extrato` —
cascade —, `wms_movimentos`, `wms_localizacoes`), com `produto_id`/`codigo` prefixados
(`999880xxx`, `__TEST_...`) para nunca colidir com catálogo real. Verificado após a
execução completa da suíte: **zero linhas remanescentes** em `dev_gx_goldie` associadas a
estes testes (uma linha órfã de uma execução anterior interrompida por um bug do próprio
teste — FK duplicada por reuso acidental de `produto_id` entre dois itens — foi identificada
e removida manualmente durante o desenvolvimento; o bug foi corrigido no teste antes da
entrega final).

## Bugs encontrados

Nenhum bug de código de produção encontrado. As correções necessárias foram todas nos
próprios scripts de teste (uso de `ReflectionMethod::invoke()` para método `protected` com
parâmetro por referência — corrigido trocando para os métodos públicos equivalentes; e
`produto_id` duplicado entre dois itens de teste colidindo com a constraint
`uq_itens_missao_produto` — corrigido usando `produto_id` distinto por item). Nada foi
alterado em `componentes/Constelacoes/...` ou `componentes/Missoes/...` fora de `Testes/`.

## Ciclo 1 — pós-correções

Escopo: regressão dos 5 arquivos já existentes contra as 3 correções de `03-backend.md`
("Correções pós-review (ciclo 1)": M1 — `ValidarEnderecoSeparacao` migrado pra
`MissaoLogisticaItemSonda` [Sonda V2]; M2 — `App.php` sem `atualizarPagina()` no ramo
`ValidarEnderecoSeparacao`; M3 — `MissaoPresenter::montarJsonFrontendBase()` expõe
`origem_localizacao`/`endereco_wms` no topo de cada item) + cobertura nova pros 2 pontos que
o ciclo 1 introduziu no backend.

### Cobertura (ciclo 1)

| Componente | Tipo | Path | Status |
|---|---|---|---|
| `SeparacaoWmsStrategy` (regressão) | TDD unit (sem DB) | `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Strategies/Execucao/SeparacaoWmsStrategyTddTest.php` | ✅ pass (12 asserts) |
| `ExecucaoStrategyFactory::createFromMissao` (regressão) | TDD unit (sem DB) | `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Strategies/Execucao/ExecucaoStrategyFactoryVarianteSeparacaoTddTest.php` | ✅ pass (13 asserts) |
| `ResolverEnderecoProdutoLaboratorio` (regressão) | TDD integration (dev_gx_goldie) | `componentes/Missoes/Logistica/sinais/AlocacaoWms/Testes/Laboratorios/ResolverEnderecoProdutoLaboratorioTddTest.php` | ✅ pass (7 asserts) |
| `App\Laboratorios\ValidarEnderecoSeparacao` (regressão — agora lê via Sonda V2, M1) | TDD integration (dev_gx_goldie) | `componentes/Missoes/Logistica/sinais/App/Testes/Laboratorios/ValidarEnderecoSeparacaoTddTest.php` | ✅ pass (17 asserts) |
| `GestaoMissoesSeparacao::enriquecerLocalizacaoWms` (regressão) | TDD integration (dev_gx_goldie) | `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesSeparacaoEnderecoWmsTddTest.php` | ✅ pass (16 asserts) |
| `MissaoLogisticaItemSonda::buscarPorId` (NOVO — Sonda criada no M1) | TDD integration (dev_gx_goldie) | `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Sondas/MissaoLogisticaItemSondaTddTest.php` | ✅ pass (12 asserts) |
| `MissaoPresenter::montarJsonFrontendBase` — `origem_localizacao`/`endereco_wms` no topo (NOVO — M3) | TDD unit (sem DB) | `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Presenters/MissaoPresenterEnderecoWmsTddTest.php` | ✅ pass (14 asserts) |

**Total ciclo 1: 7 arquivos, 91 asserts, 0 falhas** (5 arquivos de regressão do ciclo 0 + 2
arquivos novos). M2 (`App.php` sem `atualizarPagina()`) não tem teste dedicado — ver "Gaps"
abaixo.

### Decisões de escopo (ciclo 1)

- **`MissaoLogisticaItemSondaTddTest.php`**: já existia no working tree ao início desta
  sessão (arquivo untracked, aparentemente preparado numa passada anterior interrompida) —
  revisado, executado e mantido como está; cobre exatamente o que M1 introduziu:
  `buscarPorId()` caminho feliz (todos os campos relevantes, incluindo
  `dados_adicionais.endereco_wms`/`origem_localizacao` decodificáveis), item inexistente
  (`null`, não lança) e isolamento de tenant (`customerControl` — item de outro `block` não é
  visível). Não duplica `ValidarEnderecoSeparacaoTddTest.php`: aquele cobre o Laboratório
  INTEIRO (orquestração + regras de negócio de bipagem via a Sonda), este cobre só o método
  público da Sonda em isolamento — granularidade diferente, ambos com valor.
- **`MissaoPresenterEnderecoWmsTddTest.php` (novo, unit sem DB)**: até o ciclo 1 não havia
  nenhum teste que chamasse `MissaoPresenter::montarJsonFrontendBase()` diretamente (o único
  teste do Presenter layer, `CarregamentoPresenterCubagemTddTest.php`, replica a lógica de
  decode de `cubagem` fora do Presenter real, por depender de Satélites/DB nos outros ramos do
  método). Descoberta desta sessão: o único ponto de I/O do método
  (`ProdutoCodigoBarraSonda`/`ProdutoModel`, para produtos sem `dados_adicionais.codigos_barras`
  no snapshot) só roda para `$idsSemSnapshot` não-vazio — bastou montar todo item de teste já
  com `codigos_barras` preenchido pra rodar o método real, sem mock e sem bootstrap de
  framework/DB (mesmo espírito 0-I/O do `SeparacaoWmsStrategyTddTest.php`). Cobre exatamente o
  que M3 mudou: item `wms` com `endereco_wms` exposto no topo do array (mantendo também dentro
  de `dados_adicionais`, não-regressão), item `legado` (`endereco_wms=null`), item de snapshot
  antigo sem as chaves novas (`null`/`null`, AC8), múltiplos itens sem vazamento de valor entre
  eles, e `dados_adicionais` como string JSON crua (fetch sem decode). Não duplica
  `GestaoMissoesSeparacaoEnderecoWmsTddTest.php` (que cobre a GRAVAÇÃO do snapshot na criação
  da missão): este cobre a LEITURA/exposição do snapshot já gravado, para o JSON consumido
  pelo front — camada diferente do pipeline.

### Gaps de cobertura (ciclo 1)

- **M2 (`App.php` — remoção de `->atualizarPagina()` no ramo `ValidarEnderecoSeparacao`)** —
  não tem teste automatizado dedicado. `App::start()` é um dispatcher pesado (Galaxia route,
  `showOnView`, etc.) sem teste de integração pré-existente pra esse satélite no repo — criar
  um exigiria montar um `GalaxiaRoute` completo só pra inspecionar se `atualizarPagina()` foi
  chamado ou não, desproporcional pra uma remoção de 1 chamada de método fluente. Risco
  residual baixo: `ValidarEnderecoSeparacaoTddTest.php` já prova que o `response` do
  Laboratório (o payload que `App.php` embrulha) está correto em todos os cenários; o que M2
  mudou é só o wrapper de transporte (reload vs JSON limpo), documentado no
  `03-backend.md`/`04-frontend.md` e verificável por inspeção de código (`php -l` + leitura do
  diff, já feita nesta sessão) — comportamento confirmado também pelo smoke test manual do
  `04-frontend.md` ciclo 1 (item 1, "sem reload, sem toast por scan").
- **Frontend (`endereco_wms.js`, `endereco_wms.php`, `execucao_card.php`, `item.php`)** — fora
  do escopo deste QA (JS/HTML), mesmo critério do ciclo 0; coberto pelo smoke test manual em
  `04-frontend.md` ciclo 1 (6 passos, incluindo M2 e M3).

### Output da execução (ciclo 1)

```
########## 1/7 SeparacaoWmsStrategyTddTest ##########
EXIT=0
====================================================
RESULTADO GERAL: ✅ SUCESSO
Passou: 12
Falhou: 0
====================================================

########## 2/7 ExecucaoStrategyFactoryVarianteSeparacaoTddTest ##########
EXIT=0
====================================================
RESULTADO GERAL: ✅ SUCESSO
Passou: 13
Falhou: 0
====================================================

########## 3/7 ResolverEnderecoProdutoLaboratorioTddTest (dev_gx_goldie) ##########
EXIT=0
[PASS] normalizar('  a03-p2-n1  ') === 'A03-P2-N1'
[PASS] normalizar() é idempotente para código já normalizado
====================================================
RESULTADO: PASS=7  FAIL=0
TODOS OS TESTES PASSARAM

########## 4/7 ValidarEnderecoSeparacaoTddTest (dev_gx_goldie) — confirma M1 (leitura via Sonda V2) ##########
EXIT=0
--- Validações de input ---
[PASS] item_id ausente -> status=no
[PASS] código ausente -> status=no
[PASS] item inexistente -> status=no
--- Item origem=legado: libera direto (AC5.6), sem exigir bip ---
[PASS] item legado -> status=ok mesmo com código arbitrário
[PASS] item legado -> endereco_confirmado=true
--- Item origem=wms: endereço inativo/inexistente -> erro (AC5.4) ---
[PASS] endereço inativo -> status=no
[PASS] endereço inativo -> endereco_confirmado=false
[PASS] endereço inexistente no catálogo -> status=no
--- Item origem=wms: mismatch com snapshot esperado -> erro, NÃO libera (AC5.3) ---
[PASS] endereço ATIVO mas diferente do esperado -> status=no (mismatch)
[PASS] mismatch -> endereco_confirmado=false
[PASS] mismatch -> devolve endereco_esperado (snapshot do item) para o feedback
--- Item origem=wms: match (normalizado) -> libera + registra extrato (US5.8) ---
[PASS] match normalizado (espaço/minúsculo) -> status=ok
[PASS] match -> endereco_confirmado=true
[PASS] match -> registra exatamente 1 entrada ENDERECO_VALIDADO no extrato
[PASS] extrato aponta referencia_id = item_id bipado
[PASS] extrato.payload.endereco_bipado normalizado (trim+upper)
--- Alias de payload: 'endereco' funciona igual a 'codigo' ---
[PASS] payload com chave 'endereco' (alias de 'codigo') -> status=ok
====================================================
RESULTADO: PASS=17  FAIL=0
TODOS OS TESTES PASSARAM

########## 5/7 GestaoMissoesSeparacaoEnderecoWmsTddTest (dev_gx_goldie) ##########
EXIT=0
[PASS] data sem variante_separacao -> chave AUSENTE em configs (default = andar, AC1.4)
[PASS] valor != 'WMS' (literal exato) -> chave AUSENTE em configs
====================================================
RESULTADO: PASS=16  FAIL=0
TODOS OS TESTES PASSARAM

########## 6/7 MissaoLogisticaItemSondaTddTest (dev_gx_goldie) — NOVO, cobre a Sonda do M1 ##########
EXIT=0
====================================================
INTEGRATION — MissaoLogisticaItemSonda (dev_gx_goldie)
====================================================
--- buscarPorId: caminho feliz ---
[PASS] buscarPorId(item existente) não retorna null
[PASS] buscarPorId(item existente) retorna objeto
[PASS] buscarPorId: id bate com o item inserido
[PASS] buscarPorId: produto_id bate
[PASS] buscarPorId: missao_logistica_id bate
[PASS] buscarPorId: quantidade bate
[PASS] buscarPorId: status bate
[PASS] buscarPorId: dados_adicionais.origem_localizacao decodificável e correto
[PASS] buscarPorId: dados_adicionais.endereco_wms decodificável e correto
--- buscarPorId: item inexistente -> null ---
[PASS] buscarPorId(id inexistente) retorna null (não estoura)
--- buscarPorId: isolamento de tenant (customerControl) ---
[PASS] buscarPorId(item de outro tenant) retorna null (customerControl aplicado)
[PASS] sanity: item de outro tenant continua null nesta mesma sessão (não é flakiness)
====================================================
RESULTADO: PASS=12  FAIL=0
TODOS OS TESTES PASSARAM

########## 7/7 MissaoPresenterEnderecoWmsTddTest (sem DB) — NOVO, cobre o M3 ##########
EXIT=0
====================================================
TDD: MissaoPresenter::montarJsonFrontendBase — origem_localizacao/endereco_wms no topo (M3)
====================================================
--- 1. Item origem=wms com endereco_wms -> exposto no TOPO do item (M3) ---
✅ [PASS] origem_localizacao=wms exposto no topo do item
✅ [PASS] endereco_wms exposto no topo do item
✅ [PASS] origem_localizacao CONTINUA presente dentro de dados_adicionais (não regressão do ciclo 0)
--- 2. Item origem=legado (sem endereco_wms) -> origem exposta, endereco null ---
✅ [PASS] origem_localizacao=legado exposto no topo
✅ [PASS] chave endereco_wms presente mesmo quando ausente do snapshot (legado)
✅ [PASS] endereco_wms ausente -> null (legado nunca grava endereço, RN4)
--- 3. Item de missão SEM a variante WMS (snapshot antigo, sem as chaves novas) -> ambos null, sem quebrar ---
✅ [PASS] chave origem_localizacao SEMPRE presente no array (mesmo null) -- contrato estável pro JS
✅ [PASS] missão sem a variante WMS -> origem_localizacao=null (regressão zero, AC8)
✅ [PASS] missão sem a variante WMS -> endereco_wms=null (regressão zero, AC8)
--- 4. Múltiplos itens -> cada um mantém seu próprio valor, sem vazar entre itens ---
✅ [PASS] item 10: endereco_wms próprio, correto
✅ [PASS] item 11 (legado): endereco_wms=null, não herdou do item 10
✅ [PASS] item 12: endereco_wms próprio, correto (não sobrescrito por item 10)
--- 5. dados_adicionais como STRING JSON crua (fetch sem decode) -> ainda decodifica e expõe ---
✅ [PASS] dados_adicionais como string JSON -> origem_localizacao decodificado e exposto no topo
✅ [PASS] dados_adicionais como string JSON -> endereco_wms decodificado e exposto no topo
====================================================
RESULTADO GERAL: ✅ SUCESSO
Passou: 14
Falhou: 0
====================================================
```

*(Mesmo ruído de `PHP Warning` pré-existente do bootstrap nos testes de integração, omitido
dos blocos 3-6 acima por legibilidade — ver nota no final da seção "Output da execução" do
ciclo 0.)*

### Limpeza de dados de teste (ciclo 1)

`MissaoLogisticaItemSondaTddTest.php` segue o mesmo padrão `try/finally` dos demais
(`produto_id` prefixado `777003`/`777004`, `block` sentinela `999703`, código de endereço
prefixado `__TEST_SONDA_ITEM_...`). Verificado após a execução completa da suíte do ciclo 1
(rodada 2x, incluindo a correção do bug do próprio teste unit novo — ver "Bugs encontrados"
abaixo): **zero linhas remanescentes** em `dev_gx_goldie` (`missao_logistica_itens`,
`missao_logistica`, `wms_movimentos`, `wms_localizacoes`) associadas a qualquer teste desta
feature.

### Bugs encontrados (ciclo 1)

Nenhum bug de código de produção. As 3 correções do `03-backend.md` (M1/M2/M3) passaram em
toda a regressão sem exigir nenhum ajuste em `componentes/Constelacoes/...` ou
`componentes/Missoes/...`. O único problema encontrado foi no meu próprio teste novo
(`MissaoPresenterEnderecoWmsTddTest.php`, cenário 2): usei
`$resultado[...]['endereco_wms'] ?? 'sentinela'` para diferenciar "chave ausente" de "chave
null" — mas o operador `??` do PHP trata `null` e "ausente" como equivalentes, então a
asserção comparava contra o fallback errado. Corrigido trocando por
`array_key_exists('endereco_wms', ...)` (presença) + comparação direta (valor) como duas
asserções separadas — sem tocar em nenhum código de produção.
