# Testes — Fase 3 (tradutor `alocacao` real + fixes M1/n1/m1/m4 do review da Fase 2)

Escopo testado: passo 16 (parcial — só `alocacao`) do `02-techplan.md`, conforme
implementado e reportado em `10-backend-fase3.md`. O dev já havia escrito/atualizado
`AlocacaoOrdemTradutorTddTest.php` (novo, 23 asserts), `ReceberOrdemLaboratorioTddTest.php`
(reescrito, 59 asserts), `TraducaoOrdemFactoryTddTest.php` e `WmsOrdemSondaTddTest.php`
(m4). Este documento cobre a **auditoria** dessa suíte, o **fechamento de 1 gap real**
encontrado (m1 sem cobertura dedicada), a **validação fim-a-fim via HTTP real**
(`tests/integration/IntegracaoWms/ReceberOrdemApiTest.php`, que ainda refletia a
Fase 2 e quebrou com a tradução real — corrigido/expandido aqui) e o **pin nominal**
da baseline pré-existente de 12 falhas + 1 fatal na suíte `AlocacaoWms`.

Todos os arquivos rodam com `php <caminho>.php` (sem flags, sem phpunit).

## Cobertura

| Componente | Tipo | Path | Status |
|---|---|---|---|
| `AlocacaoOrdemTradutor` (getOrdemTipo, traduzir: itens válidos/sessão livre/produto desconhecido) | TDD integração (DB) — já escrito pelo dev | `componentes/.../IntegracaoWms/Testes/Tradutores/AlocacaoOrdemTradutorTddTest.php` | ✅ pass (23/23) |
| `TraducaoOrdemFactory`/`OrdemTipo` (n1: `alocacao` resolve tradutor; demais 3 continuam lançando) | unit puro — já escrito pelo dev | `componentes/.../IntegracaoWms/Testes/Tradutores/TraducaoOrdemFactoryTddTest.php` | ✅ pass (18/18) |
| `WmsOrdemSonda::buscarPorMissaoId` (m4: filtro opcional `block_empresa_id`) | TDD integração (DB) — já escrito pelo dev | `componentes/.../IntegracaoWms/Testes/Sondas/WmsOrdemSondaTddTest.php` | ✅ pass (37/37) |
| `ReceberOrdemLaboratorio` — fluxo completo (M1 endpoint×URL, n1 tradução real, dedup rejeitada/aceita) | TDD integração (DB) — já escrito pelo dev | `componentes/.../IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php` | ✅ pass (59/59 antes da minha adição) |
| **`ReceberOrdemLaboratorio::criarOrdem()` — m1 (colisão de corrida → `ja_processada`, não erro genérico)** | TDD integração (DB), **NOVO — gap fechado nesta rodada** | `componentes/.../IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php` (seção 10, via Reflection) | ✅ pass (+8 asserts, 59→67) |
| `POST /api/webhooks/wms-7` E2E: `alocacao` **com** tradutor → aceita → missão real (tipo_missao_id=14, referencia_tipo/id, configuracoes.origem/operacao_tipo) → `wms_ordens.status='traduzida'` | integration (HTTP real via `InboundHandler`), **NOVO — GRUPO D** | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO D1) | ✅ pass |
| Reenvio pós-aceite via HTTP → `ja_processada`, não cria 2ª missão | integration, **NOVO — GRUPO D2** | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO D2) | ✅ pass |
| **`ReportarOrdemLaboratorio::reportar()` — primeiro teste REAL do ciclo completo** (missão nasce via tradutor real → movimento confirmado `enviado` → `reportar()` → `status='reportada'`) | integration, **NOVO — GRUPO D3** | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO D3) | ✅ pass |
| B2/B3 — tipo sem tradutor (`movimentacao`) segue rejeitado (Fase 2 usava `alocacao` p/ isso — **corrigido** pois `alocacao` agora tem tradutor) | integration — ajustado nesta rodada | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO B2/B3) | ✅ pass |
| Cross-tenant via HTTP (2 empresas, mesmo `external_ref`, sem colisão) | integration — ajustado nesta rodada (ver "Bugs encontrados" #1) | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO C) | ✅ pass |
| `GestaoMissoesAlocacaoWms` — 3 pontos de extensão opt-in (desvio #1 do backend) — não-regressão | TDD integração (DB) — já existia, não editado | `componentes/Missoes/Logistica/sinais/Designacoes/Testes/Laboratorios/GestaoMissoesAlocacaoWmsTddTest.php` | ✅ pass (30/30, idêntico) |
| Baseline `IntegracaoWms` (demais 7 arquivos: Config/Provedores/Strategies/Validators/Webhooks) — não-regressão | unit + TDD integração — já existiam | ver "Output da execução" | ✅ pass (161/161) |
| Baseline `AlocacaoWms` (27+2 arquivos, domínio irmão só *lido*) — pin da baseline | TDD integração — já existiam | ver "Pin da baseline" | ⚠️ 12 fail + 1 fatal **pré-existentes**, confirmados alheios |
| **BUG NOVO**: `WebhookEndpointSonda::findByChannel()` depende da sessão ambiente (não do HMAC) | integration, documentado via `expectFixed()` | `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (GRUPO E) | 🔴 bug documentado (não corrigido — fora do escopo do Tester) |

## Gaps de cobertura (e justificativa)

- **`separacao`/`movimentacao`/`inventario` tradutores concretos** — não existem
  ainda (Fase 3 só pluga `alocacao`, por design do techplan/backend). Coberto
  apenas o "continuam rejeitados com motivo" (já testado em `TraducaoOrdemFactoryTddTest`,
  `ReceberOrdemLaboratorioTddTest` seção 6, `ReceberOrdemApiTest` B2/B3/GRUPO C).
- **Endereço sugerido no item da missão** (desvio #4 do backend) — não há campo
  de dados para isso hoje (fora do escopo desta entrega); nada a testar além do
  que `OrdemIdentidadeValidatorTddTest` já cobre (existência do endereço antes
  da tradução).
- **`configuracoes.gatilho`/`gerado_automaticamente` mislabeled** (desvio #5 do
  backend, cosmético/sem impacto) — não testado deliberadamente: é dado
  morto/só-escrita (backend confirmou via grep que nada lê essa chave); testar
  seria fixar comportamento de um valor sabidamente incorreto.
- **`ReportarOrdemLaboratorio` com push 'enviado' só no canal WMS (não ERP)** —
  já coberto por fixture manual em `ReportarOrdemLaboratorioTddTest.php` (passo
  5b, não tocado nesta rodada); não dupliquei via E2E real (GRUPO D3 cobre o
  canal ERP) por não agregar cobertura nova (a lógica de "qualquer um dos dois
  canais" já está testada isoladamente).
- **Concorrência real (multi-processo) para o m1** — não reproduzida com
  threads/processos de verdade (impraticável de forma determinística num
  script CLI single-thread); a cobertura nova usa Reflection para invocar
  `criarOrdem()` isoladamente no exato estado pós-corrida (linha vencedora já
  inserida), que é o que importa (o comportamento do método, não o timing).
  Mesmo padrão já usado no repo (`GestaoMissoesAlocacaoWmsTddTest`).

## Pin da baseline — suíte `AlocacaoWms` (domínio irmão, só *lido* por esta feature)

O backend (`10-backend-fase3.md`) reportou 12 falhas + 1 fatal pré-existentes,
não relacionadas a esta feature. **Investigação desta rodada, lendo cada teste
que falha (sem `git stash` — usei `git worktree add` num commit anterior apenas
para comparar arquivos, removido ao final) e confirmando a causa raiz:**

### 1. `ConfirmarAlocacaoValidatorTddTest.php` — 7 pass / **12 fail**

**Confirmado pré-existente e alheio a esta feature** (nenhum arquivo tocado por
mim ou pela Fase 3 está envolvido). Causa raiz **investigada e mais precisa**
do que a suspeita original do backend ("provavelmente aprovação de gestor"):

O teste busca dinamicamente uma missão fixture via
`SELECT ... WHERE status IN ('pendente','em_andamento') AND lider_id=1 AND block=22 LIMIT 1`
— **sem `ORDER BY` e sem filtrar `operacao_tipo`**. Hoje essa query retorna a
missão `id=2893` (`configuracoes.operacao_tipo = "movimentacao"`), não uma
missão de alocação como o teste pressupõe (docblock cita `missao_id = 2723`,
mas a query nunca fixou esse id). `ConfirmarAlocacaoValidator` delega a
validação específica ao `OperacaoAdapterFactory::createFromConfig()`, que
corretamente resolve `MovimentacaoAdapter` para essa missão — e o
`MovimentacaoAdapter` exige `localizacao_codigo_origem` (não
`localizacao_codigo`), daí todas as 12 falhas em cascata ("Endereço de origem
é obrigatório." em vez do erro esperado sobre `localizacao_codigo`).
**Confirmado isolando a chamada**: `AlocacaoAdapterTddTest.php` (37/37) prova
que o `AlocacaoAdapter` em si funciona corretamente; `ConfirmarAlocacaoValidator.php`
e `AlocacaoAdapter.php` não têm diff nesta sessão. A missão `id=2893` (e outras
7 do mesmo `block=22`/`lider_id=1` com `operacao_tipo` diferente de `alocacao`)
são resíduo de fixtures de **outra feature não commitada** (refatoração
multi-tipo de operação — `MovimentacaoAdapter`/`InventarioProdutosAdapter`/
`InventarioEnderecoAdapter`, todos visíveis como novos/alterados no `git status`
antes desta sessão começar), que passou a poluir a busca não-determinística
deste teste. **Não é causado por nada desta feature (Integração WMS)** — é
uma fragilidade pré-existente do próprio `ConfirmarAlocacaoValidatorTddTest.php`
(fixture dinâmica sem filtro suficiente) exposta pelo acúmulo de dados de um
domínio vizinho. Nem o Validator nem o Adapter foram tocados por mim.

### 2. `CronEstornoSmokeTddTest.php` — 0 pass / **2 fail**

**Confirmado pré-existente.** O teste roda o cron real
(`crons/wms_alocacao_integracao.php`) via `exec()` (subprocesso PHP separado).
Esse subprocesso falha com
`RuntimeException: Provedor WMS não registrado: 'galpao'` — confirmado lendo
`crons/wms_alocacao_integracao.php` (nenhuma chamada a `IntegracaoWms::bootstrap()`
existe no arquivo, `grep` confirma) e `WmsProviderRegistry::resolver()` (array
estático `self::$registro`, populado **somente** por `IntegracaoWms::bootstrap()`,
sem fallback). O cron nunca chamou esse bootstrap — gap de infra da Fase 1,
não desta feature. `crons/wms_alocacao_integracao.php` tem diff nesta sessão
(+107/-23), mas 100% do diff é do fluxo de estorno/aprovação de gestor (seção
"Processar fila de ESTORNO ERP/WMS" + guard da coluna `aprovacao_gestor`) —
confirmei que a ausência do bootstrap não é uma remoção recente (não há
`bootstrap` em nenhum lado do diff).

### 3. `HookEstornoTddTest.php` — **1 fatal** (0 pass / 0 fail — aborta antes do 1º assert)

**Mesma causa raiz do item 2**, confirmada lendo o arquivo: instancia
`ErpAlocacaoWmsHookStrategy()->estornar(...)` diretamente (sem passar por
`Api\Bootstrap::ensure()`/`IntegracaoWms::bootstrap()`), que internamente chama
`WmsProviderFactory::build('galpao')` → `WmsProviderRegistry::resolver('galpao')`
→ `RuntimeException` não capturada (fatal, script aborta na linha 38, antes de
qualquer `check()`).

### Resultado do pin

| Arquivo | Antes (baseline do backend) | Depois (nesta rodada) | Causa confirmada |
|---|---|---|---|
| `ConfirmarAlocacaoValidatorTddTest.php` | 12 fail | 12 fail (idêntico) | Fixture dinâmica sem filtro `operacao_tipo`/`ORDER BY`, poluída por resíduo de feature paralela (multi-tipo de operação) |
| `CronEstornoSmokeTddTest.php` | 2 fail | 2 fail (idêntico) | `crons/wms_alocacao_integracao.php` nunca chama `IntegracaoWms::bootstrap()` |
| `HookEstornoTddTest.php` | 1 fatal | 1 fatal (idêntico) | Mesma causa acima, via chamada direta do Hook Strategy |
| Demais 26 arquivos AlocacaoWms | pass | pass (idêntico) | — |

**Nota sobre contagem total**: o backend reportou "348 PASS / 12 FAIL" para 27
arquivos; nesta rodada a suíte tem **29 arquivos** (396 PASS "brutos" pela minha
contagem automatizada + 13 de 2 arquivos com formato de saída diferente = **409
PASS / 14 FAIL** + 1 fatal). A diferença de arquivos/asserts **não é desta
feature** — é o trabalho paralelo não commitado (`aprovação de gestor`/operação
multi-tipo) continuando a evoluir no mesmo branch entre o relatório do backend
e esta auditoria. **O que importa para este pin — os MESMOS 3 arquivos falhando
pelos MESMOS motivos — está confirmado idêntico.** Nenhum arquivo de produção
do domínio `AlocacaoWms` foi tocado por mim ou pela Fase 3.

## Contagem total (domínio desta feature — `IntegracaoWms`)

| Categoria | Arquivos | Asserções | Passing | Failing (bugs reais) | Failing (documentado/`expectFixed`) |
|---|---|---|---|---|---|
| TDD do sinal (`componentes/.../IntegracaoWms/Testes/`) — 12 arquivos (inclui +8 do m1 nesta rodada) | 12 | 336 | 336 | 0 | 0 |
| Integration (`tests/integration/IntegracaoWms/`) — 3 arquivos | 3 | 143 | 142 | 0 | 1 |
| **Total desta feature** | **15** | **479** | **478** | **0** | **1** |

> O único "failing" é o bug de infraestrutura do GRUPO E (`expectFixed()`),
> documentado deliberadamente — não é uma quebra de teste nem uma regressão.
> `exit code` da suíte é `0` (falhas documentadas ≤ bugs contabilizados).

## Output da execução

### TDD do sinal — suíte completa (12 arquivos)
```
componentes/.../Testes/Config/IntegracaoWmsConfigSondaTddTest.php        -> PASS=21  FAIL=0
componentes/.../Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php   -> PASS=67  FAIL=0   (+8 nesta rodada — seção 10, m1)
componentes/.../Testes/Laboratorios/ReportarOrdemLaboratorioTddTest.php  -> PASS=18  FAIL=0
componentes/.../Testes/Provedores/GalpaoProviderParidadeTddTest.php      -> PASS=35  FAIL=0
componentes/.../Testes/Provedores/WmsProviderRegistryTddTest.php         -> PASS=27  FAIL=0
componentes/.../Testes/Sondas/WmsOrdemSondaTddTest.php                   -> PASS=37  FAIL=0
componentes/.../Testes/Strategies/AlocacaoHookStrategyTddTest.php        -> PASS=41  FAIL=0
componentes/.../Testes/Tradutores/AlocacaoOrdemTradutorTddTest.php       -> PASS=23  FAIL=0
componentes/.../Testes/Tradutores/TraducaoOrdemFactoryTddTest.php        -> PASS=18  FAIL=0
componentes/.../Testes/Validators/OrdemEnvelopeValidatorTddTest.php      -> PASS=15  FAIL=0
componentes/.../Testes/Validators/OrdemIdentidadeValidatorTddTest.php    -> PASS=20  FAIL=0
componentes/.../Testes/Webhooks/WmsPayloadNormalizerTddTest.php          -> PASS=14  FAIL=0
----
TOTAL: PASS=336  FAIL=0
```

### `ReceberOrdemLaboratorioTddTest.php` — seção 10 (NOVA — m1), trecho relevante
```
--- 10. m1: colisão de corrida em criarOrdem() (veredito idempotente, não erro genérico) ---
[PASS] fixture m1: linha "vencedora" da corrida pré-inserida
[PASS] m1: colisão UNIQUE em criarOrdem() → ja_processada=true (não erro genérico)
[PASS] m1: id devolvido é o da linha VENCEDORA (a que realmente existe)
[PASS] m1: ok reflete o veredito da vencedora (rejeitada → ok=false)
[PASS] m1: motivo é o da linha vencedora (não recalculado a partir do payload da perdedora)
[PASS] m1: exatamente 1 linha física em wms_ordens (UNIQUE realmente impediu a duplicata da "corrida")
[PASS] m1: falha de banco REAL (sem corrida) → ja_processada=false (erro genérico é o correto aqui)
[PASS] m1: falha de banco real (sem corrida) → id=null

[CLEANUP] Linhas de teste removidas.

====================================================
RESULTADO: PASS=67  FAIL=0
TODOS OS TESTES PASSARAM
```

### Integration — `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php` (reescrito nesta rodada)
```
====================================================
INTEGRATION — IntegracaoWms: receiver ponta-a-ponta (dev_gx_goldie)
====================================================

--- GRUPO A: canal seedado 'wms-7' deve estar imediatamente alcançável ---
[PASS] pré-condição: webhook_endpoints.block do canal 'wms-7' está populado pela Migration 003
[NOTE] POST /api/webhooks/wms-7 (assinatura HMAC VÁLIDA, canal seedado pela Migration 003):
[NOTE]   → HTTP 200 | body={"received":true,"channel":"wms-7","request_id":"...","ok":true,"ordem_id":280,"status":"aceita","motivo":null}
[PASS] GRUPO A: POST assinado corretamente para o canal seedado NÃO deveria dar 404 (canal existe na tabela)
[NOTE] Fase 3: 'alocacao' agora tem tradutor — resposta esperada é 200/aceita (ver GRUPO D para asserções completas do fluxo).

--- GRUPO B: fluxo normal do receiver ---
-- B1. version='v99' (desconhecida) --
[PASS] B1: version='v99' → HTTP 422
[PASS] B1: corpo rico → ok=false
[PASS] B1: corpo rico → status="rejeitada"
[PASS] B1: corpo rico → motivo menciona a versão recebida
[PASS] B1: corpo mantém "received"=true (compat com o formato antigo)
[PASS] B1: corpo mantém "channel"
[PASS] B1: NADA gravado em wms_ordens (tenant nem foi resolvido nesse ponto)

-- B2. Envelope válido, tipo='movimentacao' (sem tradutor) --
[PASS] B2: HTTP 422 (ok=false → status 422 pelo fix do InboundHandler)
[PASS] B2: corpo rico → ok=false
[PASS] B2: motivo menciona falta de tradutor
[PASS] B2: ordem_id retornado no corpo
[PASS] B2: linha gravada em wms_ordens
[PASS] B2: linha gravada com status="rejeitada"
[PASS] B2: linha gravada na empresa correta (7, resolvida pelo canal wms-7)

-- B3. Reenvio do MESMO external_ref (dedup) --
[PASS] B3: reenvio → HTTP 422 (a rejeição original é preservada, não é sucesso)
[PASS] B3: reenvio → status="ja_processada" no corpo
[PASS] B3: reenvio NÃO duplica linha em wms_ordens (ainda 1 só)

-- B4. Assinatura HMAC inválida --
[PASS] B4: assinatura HMAC inválida → HTTP 401
[PASS] B4: corpo → error.code = "hmac_mismatch"
[PASS] B4: assinatura inválida → ReceberOrdemLaboratorio NUNCA executa (nada gravado)

-- B5. Canal inexistente (nunca registrado) --
[PASS] B5: canal nunca registrado → HTTP 404 (comportamento correto — "mantém verde" do techplan)
[PASS] B5: corpo → error.code = "unknown_channel"

--- GRUPO C: cross-tenant via HTTP (2 canais, mesmo external_ref) ---
[PASS] cross-tenant: POST em wms-7 processado normalmente (422 — sem tradutor)
[PASS] cross-tenant: POST em wms-99941 processado normalmente (422 — sem tradutor)
[PASS] cross-tenant: linha da empresa 7 existe (mesmo external_ref da empresa X)
[PASS] cross-tenant: linha da empresa de teste existe
[PASS] cross-tenant: tipo da empresa 7 é o que ELA enviou ("movimentacao")
[PASS] cross-tenant: tipo da empresa X é o que ELA enviou ("inventario"), sem se confundir com a 7
[PASS] cross-tenant: exatamente 2 linhas para o mesmo external_ref (uma por empresa, sem colisão)

--- GRUPO D: Fase 3 — alocacao real via HTTP + ciclo de report ---
-- D1. Envelope válido, tipo='alocacao' (COM tradutor, Fase 3) --
[PASS] D1: alocacao COM tradutor (Fase 3) → HTTP 200
[PASS] D1: ok=true
[PASS] D1: status='aceita'
[PASS] D1: motivo=null (aceite limpo)
[PASS] D1: ordem_id retornado no corpo
[PASS] D1: linha existe em wms_ordens
[PASS] D1: wms_ordens.status='traduzida'
[PASS] D1: missao_logistica_id vinculado
[PASS] D1: gravado na empresa correta (7, resolvida pelo canal wms-7)
[PASS] D1: missão existe em missao_logistica
[PASS] D1: tipo_missao_id=14 (TipoMissao::ALOCACAO_WMS)
[PASS] D1: referencia_tipo='WMS_ORDEM'
[PASS] D1: referencia_id aponta de volta para a ordem
[PASS] D1: configuracoes.origem='wms'
[PASS] D1: configuracoes.operacao_tipo='alocacao'

-- D2. Reenvio pós-aceite via HTTP (não-regressão: não cria 2ª missão) --
[PASS] D2: reenvio via HTTP → status="ja_processada"
[PASS] D2: reenvio via HTTP preserva ok=true (não é um novo aceite)
[PASS] D2: mesmo ordem_id da 1ª vez
[PASS] D2: reenvio via HTTP NÃO cria uma 2ª missão (ainda exatamente 1)

-- D3. Ciclo de report REAL (movimento confirmado 'enviado' → 'reportada') --
[PASS] D3: fixture — movimento confirmado criado para a missão REAL (id=3340)
[PASS] D3: ReportarOrdemLaboratorio::reportar() → true (avança o ciclo)
[PASS] D3: wms_ordens.status avançou para 'reportada'
[PASS] D3: reportada_em preenchido

--- GRUPO E: bug encontrado — WebhookEndpointSonda depende da sessão ambiente ---
[BUG ] BUG: canal HMAC-válido deveria ser encontrado independente da sessão ambiente (um webhook externo real não carrega sessão Genesis nenhuma)
       └─ WebhookEndpointSonda::findByChannel() (Api/Sondas, Fase 1 — fora do escopo desta feature) tem customerControl=true + companyControl=true, ... Resultado real: HTTP 404 | body={"error":{"code":"unknown_channel",...}}.

[CLEANUP] Linhas de teste removidas (wms_ordens, missões/itens criados, wms_movimentos). webhook_endpoints.block('wms-7') permanece populado (estado correto pós-fix).

====================================================
RESULTADO: PASS=54  FAIL=1  (dos quais BUGS=1)
ATENÇÃO: 1 bug(s) documentado(s) via expectFixed() — ver 11-testes-fase3.md.
SUÍTE CONCLUÍDA (bugs conhecidos documentados acima, se houver).
```
Exit code: `0`.

### Não-regressão — demais integration/TDD reexecutados
```
tests/integration/IntegracaoWms/ConfigIntegracaoTest.php     -> PASS=47   FAIL=0
tests/integration/IntegracaoWms/Fase2MigrationsTest.php      -> PASS=41   FAIL=0
componentes/.../Designacoes/Testes/.../GestaoMissoesAlocacaoWmsTddTest.php -> PASS=30 FAIL=0  (idêntico à baseline pré-Fase 3 — os 3 pontos de extensão opt-in não mudam comportamento)
```

## Bugs encontrados

### 1. [NOVO, não corrigido — reportar ao dono de `Api/Sondas`] `WebhookEndpointSonda::findByChannel()` depende da sessão PHP ambiente, não da requisição autenticada

**Onde**: `componentes/Missoes/Plataforma/sinais/Api/Sondas/WebhookEndpointSonda.php:38-45`
(`customerControl=true`, `companyControl=true`) + `controladores/_Controladores/ModelInteligencia.php`
(`customerQuery()`/`companyQuery()`, injetam `(block=:bcustomerid OR block=:bcustomerid2=7) AND block_empresa_id=:bcompanyid`
onde os dois parâmetros vêm de `getSession('galaxiaUserInfo')`/`SessionGalaxia`).

**Problema**: o `InboundHandler` chama `WebhookEndpointSonda::findByChannel($channel)`
**antes** de instanciar qualquer Lab — nesse momento, a única "identidade" que a
query usa é a **sessão PHP ambiente** do processo que atende a requisição, não
nada do HMAC/payload da própria chamada. Um webhook HTTP real de um WMS externo
**nunca carrega uma sessão Genesis** (não há login, não há cookie compartilhado
com o WMS) — na prática, sem uma sessão pré-alinhada ao tenant do canal, a
linha nunca é encontrada, mesmo com HMAC 100% válido.

**Reprodução determinística** (`tests/integration/IntegracaoWms/ReceberOrdemApiTest.php`,
GRUPO E): com a sessão ambiente apontando para uma empresa arbitrária que não é
dona de nenhum canal, `POST /api/webhooks/wms-7` com assinatura HMAC **correta**
retorna `404 unknown_channel` — apesar de o canal existir, estar ativo e a
assinatura bater. Reproduzi também o caso "sessão vazia" (nenhuma
`galaxiaUserInfo` setada — o estado mais próximo de uma requisição HTTP
anônima real): **nem o canal `wms-7` é encontrado** (`bcompanyid` cai no
fallback hardcoded `'12'`, que não bate com `block_empresa_id=7`).

**Por que os testes anteriores "passavam" apesar disso**: tanto a Fase 2 quanto
esta rodada (antes do fix de teste) fixam a sessão ambiente para **empresa 7**
logo no topo do arquivo — que **coincide** com o tenant do canal principal sob
teste (`wms-7`) E com a constante hardcoded `bcustomerid2=7` do framework
(usada para o "tenant mestre"/dev). Isso mascarava o problema para o cenário
"feliz" (mesma empresa o tempo todo) — só apareceu quando o GRUPO C
(cross-tenant, empresa 99941) tentou usar um SEGUNDO canal sem realinhar a
sessão ambiente, e a 2ª chamada retornou 404 em vez de 422.

**Impacto**: é uma questão de **disponibilidade/roteamento**, não de
segurança/vazamento cross-tenant (nenhum dado de uma empresa aparece pra
outra — o pior caso é "não encontra o canal", nunca "encontra o canal errado").
Mas é potencialmente **bloqueante para produção multi-tenant de verdade**: se
o mecanismo de sessão ambiente do servidor de fato não carrega nenhum tenant
default para requisições anônimas (precisaria confirmar em ambiente real
Apache/PHP-FPM, fora do alcance deste Tester rodando via CLI), qualquer canal
de uma empresa que não seja a "mestre" (7) pode nunca ser alcançado por um WMS
real — o próprio smoke test documentado em `10-backend-fase3.md`/Fase 2
("Como testar" via `curl`) merece ser **re-executado de verdade contra o
servidor** (não só documentado) antes do primeiro WMS real ir ao ar, para
confirmar se o ambiente de produção tem algum bootstrap de sessão default que
mascare isto (não encontrei nenhum no código — `Bootstrap::ensure()` da API
não popula `$_SESSION`).

**Correção sugerida (não aplicada — fora do escopo do Tester; arquivo é da
Plataforma/Fase 1, não desta feature)**: `WebhookEndpointSonda::findByChannel()`
deveria rodar com `customerControl=false`/`companyControl=false` (como
`WmsOrdemSonda`/`IntegracaoWmsConfigSonda` já fazem, isolamento **explícito**
via `WHERE channel = :ch AND active = :ac`) — a autenticação real do canal já é
o HMAC comparado contra o `secret` daquela linha especificamente, não a sessão.
Isso é consistente com o padrão que o próprio `10-backend-fase3.md`/M1 já
apontou: "a sessão não deveria ser a fonte-da-verdade do tenant para um
webhook". Reportar ao dev responsável por `Api/Sondas` (Plataforma, Fase 1).

**Workaround aplicado só no teste** (para conseguir validar a garantia que
realmente importa — isolamento de dados entre tenants, não o roteamento): GRUPO
C agora alinha a sessão ambiente ao tenant de cada canal via `comoTenant()`
antes de cada chamada — isso NÃO corrige o bug, só permite o teste chegar ao
código de negócio para provar que, uma vez o canal encontrado, os dados não
vazam entre empresas (o que continua verdadeiro).

### 2. [Sem impacto — corrigido no meu próprio teste] Assert `motivo=null` com `??` mascarava o valor real

Durante a escrita do GRUPO D, um assert usava `$resD1->body['motivo'] ?? 'x' === null`
— `??` trata chave presente-mas-`null` como "ausente" (gotcha clássico de PHP,
`isset()` retorna `false` para valores `null`), então o assert falhava mesmo com
o comportamento de produção correto. Troquei para
`array_key_exists('motivo', $resD1->body) && $resD1->body['motivo'] === null`.
**Bug do meu teste, não do código de produção** — corrigido antes da entrega
(não requer ação do dev backend).

## Rastreabilidade — fixes do review da Fase 2 (09-review-fase2.md)

| Fix | Teste que FALHARIA sem o fix | Resultado |
|---|---|---|
| **M1** (tenant via endpoint autenticado, não URL crua) | `ReceberOrdemLaboratorioTddTest.php` seções 1b (endpoint×URL diverge → fail-closed) e 1c (endpoint sozinho é fonte da verdade) | ✅ pass |
| **n1** (bloco morto vira tradução real) | `ReceberOrdemLaboratorioTddTest.php` seção 7 (`alocacao` com tradutor → aceita de verdade) + `ReceberOrdemApiTest.php` GRUPO D | ✅ pass |
| **m1** (colisão de corrida → `ja_processada`, não erro genérico) | `ReceberOrdemLaboratorioTddTest.php` seção 10 (**novo nesta rodada** — via Reflection em `criarOrdem()`) | ✅ pass |
| **m4** (`buscarPorMissaoId` com `empresaId` errado → `null`) | `WmsOrdemSondaTddTest.php` (filtro opcional `block_empresa_id`) | ✅ pass |

## Não-regressão confirmada

- Reenvio do mesmo `external_ref` pós-aceite → `ja_processada` sem 2ª missão:
  `ReceberOrdemLaboratorioTddTest.php` seção 9 (chamada direta ao Lab) **e**
  `ReceberOrdemApiTest.php` GRUPO D2 (via HTTP real) — dupla cobertura.
- Tipo sem tradutor (`separacao`/`movimentacao`/`inventario`) → rejeitada com
  motivo: `ReceberOrdemLaboratorioTddTest.php` seção 6 + `ReceberOrdemApiTest.php`
  B2/B3/GRUPO C.
- Cross-tenant (ordem da empresa A não visível/afetando empresa B):
  `WmsOrdemSondaTddTest.php` (isolamento a nível de Sonda) + `ReceberOrdemApiTest.php`
  GRUPO C (via HTTP, após o workaround de sessão — ver bug #1).
- `InboundHandler` backward-compat com outros `target_lab` (2º parâmetro extra
  não quebra construtores de 1 parâmetro): confirmado por inspeção de código
  (`new $targetLab($payload, $endpoint)` — PHP ignora argumentos extras não
  declarados) + nenhum outro `target_lab` registrado hoje em `webhook_endpoints`
  além de `ReceberOrdemLaboratorio` (conferido via `SELECT DISTINCT target_lab
  FROM webhook_endpoints` no `dev_gx_goldie` — só 1 valor). Não escrevi um
  Lab-dummy artificial para este teste porque não há um 2º consumidor real do
  contrato hoje; o raciocínio é de linguagem (PHP), não de dado, e já está
  documentado no comentário do próprio `InboundHandler.php:70-75`.

## `php -l`

Sem erros nos 2 arquivos tocados nesta rodada:
- `componentes/Missoes/Logistica/sinais/IntegracaoWms/Testes/Laboratorios/ReceberOrdemLaboratorioTddTest.php`
- `tests/integration/IntegracaoWms/ReceberOrdemApiTest.php`

## Resíduo no banco

Confirmado zero resíduo de `__test_*`/`ZZ-TEST*` em `wms_ordens`,
`wms_movimentos`, `wms_localizacoes`, `webhook_endpoints`, `integracao_wms_config`
e `missao_logistica` (ligadas às ordens de teste) após a execução completa da
suíte desta feature. **Observação à parte** (não é resíduo desta rodada): 2
linhas antigas em `missao_logistica` (`id=2776/2777`, `descricao='__test_sugestao_alocacao_wms'`,
`data_cadastro='2026-06-18'`) já existiam no banco antes desta sessão — não
usam nenhum prefixo/padrão desta feature e não foram tocadas por mim.
