test(canais): prova real do seam — envio pela tela com external_id do WhatsApp
Jornada revivida contra build FRESCO (o anterior era pre-4b e daria um diff vazio mentiroso). diff do gates.csv contra a baseline: vazio. Mensagem enviada pelo inbox chegou status=sent com external_id 3EB0644366757BD8B9CA71 devolvido pelo WhatsApp — o adapter fala com o WAHA real, nao so com fetch stubado. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01671t6LhFTTonLgwL47wBy3
@@ -41,7 +41,7 @@ O motivo é o acúmulo: quanto mais tarde o teste, mais causas possíveis para u
|
||||
| Task 4b (`resolveWahaChatId` → `adapter.resolveRecipient`) | ✅ `650a795` · +6/−2 linhas · 8✓ · suíte 1060✓ exit 0 |
|
||||
| Task 4c (pre-check de configuração → `adapter.isConfigured()`) | ✅ `f0acb82` · adapter ganhou `isConfigured()` + `codes` (3 casos novos, vermelhos antes) · 8✓ · suíte 1063✓ exit 0 |
|
||||
| Task 4d (envio → `adapter.send`) | ✅ `c5221cb` · `getWahaClient` fora do handler · 8✓ · suíte 1063✓ exit 0 · sabotagem do ramo de mídia vermelha no caso certo |
|
||||
| Task 4e (jornada + `gates.csv`) | ⛔ **não executada** — conduzida separadamente. Nada do refactor foi provado pela tela ainda |
|
||||
| Task 4e (jornada + `gates.csv`) | ✅ build refeito do HEAD (`BUILD_ID 4W3v83yHSysyCUIj3YQLD`, 15:20 > commits 15:04–15:12) · jornada **3✓/0✗** · `diff` do `gates.csv` contra a baseline **vazio** · envio manual pela tela chegou ao WAHA (`status='sent'`, `external_id='3EB0644366757BD8B9CA71'`) · evidência em `evidence/canais/task4/` |
|
||||
|
||||
A Task 0 gravou a foto do "antes" e produziu 2 instrumentos reutilizáveis
|
||||
(`tests/journeys/`, `scripts/provoke-agent-turn.ts`). A **Task 1** é a primeira linha de
|
||||
@@ -296,6 +296,7 @@ a doutrina de QA Visual do repo já diz que mock não estressa o egress real.
|
||||
| 2026-07-27 | **Task 4b** | UMA substituição em `app/api/v1/messages/_handler.ts`: `resolveWahaChatId({...})` → `adapter.resolveRecipient({...})`, com `const adapter = getAdapter("waha")` (literal fixo + comentário: `channel_sessions.provider` só existe a partir da Task 6, e o `select` do handler nem traz o campo). Import de `resolveWahaChatId` trocado por `getAdapter`. **+6/−2 linhas, 1 arquivo.** `getWahaClient`/`sendMedia`/`sendMessage`/`parseWahaMessageId` intocados | rede dos 8: `pnpm exec vitest run messages-handler-desfechos` → **8✓ exit 0**; suíte inteira **1060✓/0✗ · 140 arquivos · exit 0** (igual à Task 4a — nenhum teste novo, nenhuma regressão); typecheck **exit 0**; lint **exit 0** (156 warnings, 0 erros, nenhum no handler). Commit `650a795` | nada. A armadilha que a 4a anotou (o `chatId` é calculado ANTES do pre-check) não incomodou: `getAdapter` só consulta a tabela de providers, não precisa do canal configurado |
|
||||
| 2026-07-27 | **Task 4c** | Duas coisas, uma dependente da outra. (i) Contrato: `ChannelAdapter` ganhou `isConfigured(): boolean` e `readonly codes { notConfigured, sendFailed }`; `wahaAdapter` implementa com `getWahaClient() !== null` e os literais `waha_not_configured`/`waha_error`; 3 casos novos em `tests/unit/channel-adapter-waha.test.ts` (`vi.stubEnv` nos dois estados + os códigos). (ii) Handler: `if (!waha)` → `if (!adapter.isConfigured())` e `"waha_not_configured"` → `adapter.codes.notConfigured`. `getWahaClient()` mantido vivo (é a 4d que o remove) | **vermelho primeiro nos 3 casos novos** — o mais legível: `AssertionError: expected undefined to deeply equal { …(2) }` em `channel-adapter-waha.test.ts:75` (`codes carrega os literais que o handler grava`). Depois: 9✓ no arquivo do adapter; rede dos 8 → **8✓ exit 0**; suíte inteira **1063✓/0✗ · 140 arquivos · exit 0** (era 1060 — +3, os novos); typecheck **exit 0**; lint **exit 0** (156 warnings, 0 erros). Commit `f0acb82` | **o plano subestimou a armadilha:** "mantenha o `getWahaClient()` vivo" não faz o `tsc` passar — medi `TS18047: 'waha' is possibly 'null'` nas linhas 279 e 290, porque trocar o `if` tira o narrowing. Resolvido com `waha!` + comentário, apagado na 4d (ver seção "Duas armadilhas de compilação") |
|
||||
| 2026-07-27 | **Task 4d** | `waha!.sendMedia(...)`/`waha!.sendMessage(...)` + `parseWahaMessageId(...)` → **um** `adapter.send({ sessionRef, to, kind, media\|body })` por ramo; `"waha_error"` → `adapter.codes.sendFailed`; `getWahaClient`, `wahaSendPlanFor` e `parseWahaMessageId` saíram dos imports do handler (sobrou `isMediaPathOwnedBy`). `storage_sign_failed` **continua literal**, como o plano manda. Em `lib/channels/types.ts`, `OutboundKind` passou a derivar de `SendMessageInput["type"]` — sem isso não compila (ver armadilha 2). **+33/−28 linhas, 2 arquivos** | rede dos 8 → **8✓ exit 0**; suíte inteira **1063✓/0✗ · 140 arquivos · exit 0**; typecheck **exit 0**; lint **exit 0** (156 warnings, 0 erros). **Sabotagem pós-refactor:** renomeando a chave `media` do envelope, `× 4. com media_storage_path…` vermelheceu sozinho (1 falhou / 7 passaram) e o arquivo voltou com SHA-256 `9a8b73fc…` idêntico. O caso 6 (`error_message === 'waha_500'`, sem corpo) passou intacto → **a assimetria de erro atravessa o seam**, confirmando que o alerta da 4a não procedia. Commit `c5221cb` | (a) `OutboundKind` da Task 3 era mais estreito que o chamador real (5 valores à mão × 8 no schema) — corrigido derivando, sem mudança de comportamento; (b) sobrou `"waha_unknown"` na linha 306 (fallback de `error_message` quando o throw não é `Error`): fora do escopo da 4d, que só troca `error_code`; é dívida da Task 7, registrada na tabela acima; (c) **nada foi provado pela tela** — a jornada e o `gates.csv` são a Task 4e, conduzida separadamente |
|
||||
| 2026-07-27 | **Task 4e** | **Zero linha de produção.** Rebuild do HEAD `0074066` + restart do que servia código velho; jornada de 7 paradas re-vivida; `gates.csv` regravado com o filtro do turno da vez; prova extra do caminho manual (`evidence/canais/task4/08-envio-real.png`); `evidence/canais/README.md` ganhou a seção `task4/` | **O build era velho e isso foi medido antes de qualquer teste:** a 3007 servia `BUILD_ID K9dkpcdBMT642C2RDbP2_` de **13:16**, anterior aos commits `650a795`/`f0acb82`/`c5221cb` (15:04–15:12) — e o worker da 8797 tinha subido 13:17, também antes. Derrubei os dois (nossos), `pnpm build` **exit 0** → `BUILD_ID 4W3v83yHSysyCUIj3YQLD` às 15:20, e provei o conteúdo, não só o carimbo: **todo** chunk de `.next/server` que contém o handler (`media_storage_path fora da conversa`) contém também `resolveRecipient` (3/3). Jornada **3✓/0✗ em 37,5s**; turno REAL de IA provocado (trace `1a595cbe` às 18:24:05Z, 8 gates); `diff evidence/canais/baseline/gates.csv evidence/canais/task4/gates.csv` → **vazio, exit 0** (idem contra a `task1`). **A prova que o `gates.csv` não dá:** envio manual pelo inbox, pela tela, atravessando `adapter.send` inteiro → `messages.status='sent'`, `external_id='3EB0644366757BD8B9CA71'`, `error_code` nulo | (a) **o caminho manual nunca tinha chegado ao `adapter.send` neste banco** — na baseline, na task1 e na task4 as 3 mensagens do inbox morrem em `missing_phone_number` (contato do seed sem telefone) e a do turno de IA em `channel_session_not_working`: o `diff` vazio prova a cadeia de gates, não o envio. Para exercitar o ramo, apontei **temporariamente** a sessão do seed para a sessão WAHA `WORKING` e dei ao contato o número da própria conta do WAHA (envio para si mesmo), enviei pela tela, medi, e **reverti os dois campos** aos valores originais; (b) **o Postgres local segfaultou 2× no meio** (`signal 11` às 18:22:51 e 18:28:41 UTC, PostgREST `503 PGRST002`, GoTrue em `recovery mode`) e sujou as duas primeiras execuções da jornada — sintomas `No active organization` e `/app/radar` redirecionando para `/app`. Mesmo container já tinha segfaultado 2× às 15:28/15:29, **antes** da baseline: é defeito do stack local, e o diff de produção das 4b/4c/4d toca 3 arquivos, nenhum no caminho de auth/radar. Terceira execução, com o banco de pé, 3✓; (c) **defeito real que o crash expôs, e que NÃO é desta branch:** `loadAuthUser` (`lib/auth/server.ts:46-50`) descarta o erro do `select` em `user_organizations` — falha de query vira "usuário sem organização" e o app responde 403 `no_active_org`/redireciona, sem dizer que o banco caiu. Não consertei (fora do escopo); (d) `evidence/canais/task4/03-inbox.png` pegou a timeline ainda carregando: a jornada não espera a primeira bolha, só o campo "Mensagem" |
|
||||
|
||||
### Duas armadilhas de compilação das Tasks 4c/4d (medidas, não previstas pelo plano)
|
||||
|
||||
|
||||
@@ -124,3 +124,62 @@ psql "$DATABASE_URL" -c "\copy (select e->>'gate' as gate, e->>'verdict' as verd
|
||||
```
|
||||
|
||||
Um turno contra um turno — que é o escopo em que a baseline foi gravada.
|
||||
|
||||
---
|
||||
|
||||
## `task4/` — a jornada com o handler falando com o `ChannelAdapter` (Task 4e)
|
||||
|
||||
As Tasks 4b/4c/4d trocaram as chamadas diretas ao WAHA em `app/api/v1/messages/_handler.ts`
|
||||
por `adapter.*`. Até aqui nada disso tinha saído do `vitest`. Esta pasta é a prova pela
|
||||
tela, servida por um build **feito depois** dos três commits do refactor (`BUILD_ID`
|
||||
`4W3v83yHSysyCUIj3YQLD`, gerado 15:20; o último commit do refactor é de 15:12 — o
|
||||
processo que estava na 3007 servia um build de 13:16, anterior a tudo, e foi derrubado).
|
||||
|
||||
| Arquivo | O que prova, agora com o handler atrás do seam |
|
||||
|---|---|
|
||||
| `evidence/canais/task4/01-login.png` | login + MFA entram na conta real |
|
||||
| `evidence/canais/task4/02-qr.png` | Conexões segue mostrando o estado real da sessão WAHA |
|
||||
| `evidence/canais/task4/03-inbox.png` | inbox carrega as conversas do tenant |
|
||||
| `evidence/canais/task4/04-texto-enviado.png` | texto enviado pelo inbox — mesmo desfecho da baseline (`Falhou`, contato do seed sem telefone) |
|
||||
| `evidence/canais/task4/05-audio-enviado.png` | áudio enviado pelo inbox — o ramo de mídia do `adapter.send` |
|
||||
| `evidence/canais/task4/06-followup.png` | follow-up agendado pela tela |
|
||||
| `evidence/canais/task4/07-radar.png` | Radar de Risco carregado |
|
||||
| `evidence/canais/task4/08-envio-real.png` | **o que faltava:** envio manual pelo inbox que atravessa `adapter.send` inteiro e chega ao WAHA — bolha com ✓, não `Falhou` |
|
||||
| `evidence/canais/task4/gates.csv` | a cadeia `before_send` de um turno REAL de IA, **idêntica** à da baseline (`diff` vazio, exit 0) |
|
||||
|
||||
### Por que o `08` existe — o `gates.csv` não cobre o que a 4d refatorou
|
||||
|
||||
O `gates.csv` prova a cadeia do turno de IA. O caminho **manual** do inbox (o que a Task 4d
|
||||
mexeu) nunca chegou a `adapter.send` neste banco: o contato do seed do radar não tem
|
||||
telefone, então `resolveRecipient` devolve `null` e a mensagem morre em
|
||||
`missing_phone_number` — foi assim na baseline, na task1 e na task4. Nenhuma mensagem
|
||||
jamais saiu com `status='sent'` aqui.
|
||||
|
||||
Para exercitar o ramo de verdade, a Task 4e apontou **temporariamente** a sessão do seed
|
||||
para a sessão WAHA que está `WORKING` e deu ao contato o número da própria conta do WAHA
|
||||
(envio para si mesmo — real, sem terceiro no meio), enviou **pela tela**, mediu, e
|
||||
**reverteu os dois campos** aos valores originais. Medido em `messages`:
|
||||
|
||||
```
|
||||
status = sent · external_id = 3EB0644366757BD8B9CA71 · error_code = null
|
||||
```
|
||||
|
||||
`external_id` não-nulo é o ID que o WhatsApp devolveu: a mensagem saiu de verdade, pelo
|
||||
`adapter.send`, com o `parseWahaMessageId` do outro lado do seam.
|
||||
|
||||
### O Postgres local segfaultou no meio (e isso não é regressão do refactor)
|
||||
|
||||
A primeira e a segunda execuções da jornada saíram sujas — toast `No active organization` e
|
||||
`/app/radar` redirecionando para `/app`. Ambos os sintomas são o mesmo defeito:
|
||||
`loadAuthUser` (`lib/auth/server.ts`) **descarta o erro** do `select` em
|
||||
`user_organizations`, então uma falha de query vira "usuário sem organização".
|
||||
|
||||
A falha de query foi medida na fonte: `docker logs supabase_db_deskcomm-crm` mostra
|
||||
`server process ... was terminated by signal 11: Segmentation fault` às 18:22:51 e
|
||||
18:28:41 UTC — exatamente as duas janelas —, o PostgREST respondendo `503 PGRST002` e o
|
||||
GoTrue `FATAL: the database system is in recovery mode`. O mesmo container já tinha
|
||||
segfaultado 2× às 15:28/15:29, **antes** da execução da baseline. É defeito do stack local
|
||||
(pgvector/pg17 + walsender do Realtime), pré-existente, e o diff de produção das Tasks
|
||||
4b/4c/4d toca 3 arquivos — `app/api/v1/messages/_handler.ts`, `lib/channels/types.ts` e
|
||||
`lib/channels/adapters/waha.ts` — nenhum deles no caminho de auth ou do radar. A terceira
|
||||
execução, com o banco de pé, passou 3/3 e é a que está gravada aqui.
|
||||
|
||||
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,9 @@
|
||||
gate,verdict,code
|
||||
case_promise,pass,""
|
||||
disclosure,pass,""
|
||||
lgpd,pass,""
|
||||
pacing,pass,""
|
||||
promise,pass,""
|
||||
semantic_promise,pass,""
|
||||
spinning,pass,""
|
||||
stop,pass,""
|
||||
|