mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
* docs(agente): a spec dos tres papeis, e a terceira porta que a doutrina nao previa A doutrina FALAR/OPERAR ja existia e o passo 1 dela ja estava fechado com numero (30% de vazamento com prompt de operador, 0% com prompt de atendimento, 18 turnos, gate desarmado). O que faltava era a spec que rege o resto. Fecha as sete decisoes de produto. As tres que mudam o desenho: uma unidade com tres papeis (nao tres agentes), o Operador roda DEPOIS do envio (o lead nunca espera o CRM), e desligar o Operador NAO desliga o registro — a declaracao do Conversador ja e linguagem de negocio e vira estado por codigo, sem LLM. Registra a TERCEIRA porta de vazamento, que a doutrina subestimava: o vazamento medido `unsafe_url:https_required` nao veio da description nem do name da tool, veio do DADO que ela devolveu. Esconder a ferramenta nao fecha essa porta — exige camada de projecao propria. Na mesma resposta chegaram UUIDs crus de usuario a tela do cliente, que o detector nao pega por desenho. Registra tambem que o botao Testar da tela nao exercita guardrail nenhum: /versions/[vid]/test chama runAgent do runtime deprecated, que nao importa runBeforeSend. O self-hoster testa, ve limpo, publica, e producao diverge. Nao muda comportamento — tres documentos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a declaracao do turno — a fronteira FALAR/OPERAR vira contrato Passo 2 da spec 16. O checkpoint ja era a metade de um contrato entre quem fala e quem opera; faltava o campo que diz o QUE ACONTECEU no turno, e faltava declara-lo como fronteira em vez de subproduto. A declaracao viaja na chamada de FECHAMENTO que ja existe, nao numa tool. O motivo e o que este arquivo ja escrevia sobre o checkpoint: uma tool dependeria de o modelo lembrar de chama-la, e o turno em que ele esquecesse seria um lead parado em silencio. Custo adicional zero — e a mesma chamada de modelo. AUSENTE NAO E VAZIA, e o desenho inteiro gira nisso. `declaracao: undefined` (coluna NULL) = o modelo nao declarou; `{nada_a_declarar: true}` = ele avaliou e nao havia nada. Um `.default({})` no Zod, ou um `not null default` na coluna, colapsaria os dois e faria "esqueceu" parecer "nao havia nada" — escondendo o esquecimento exatamente onde o invariante 4 do sistema vivo manda mostra-lo. LeadCheckpointRow faz Omit e redeclara o campo porque as duas pontas representam o "nao sei" diferente: o modelo omite, o Postgres guarda null. A declaracao ja NASCE com leitor, senao seria evento sem consumer: buildHandoffSummary passa a entregar ao humano o que a pessoa quer e o que foi prometido a ela, antes de compromissos e objecoes — e o prazo vai colado na promessa, porque quem assume precisa do "ate quando" na mesma linha. Um artefato, dois leitores (invariante 2). O texto que o Conversador le nao carrega vocabulario de sistema, e ha teste que reprova se carregar — e o defeito de 30% que a spec existe para matar. Schema em tripla: migration 0110 + apendice idempotente no baseline + MANIFEST. Evidencia observada: 16 testes novos; suite 2603 passed (270 arquivos); typecheck limpo; lint 0 erros e 189 warnings — o MESMO numero medido na main com as mudancas em stash, entao nenhum warning novo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a projecao — fecha a terceira porta, a que a doutrina subestimava Passo 3 da spec 16. As duas primeiras portas de vazamento (nome e descricao da ferramenta) se fecham nao mostrando a ferramenta. A terceira nao: e o DADO que ela devolve, e foi medida em turno real — "deram erro na acao de webhook: unsafe_url:https_required", copiado do resultado para a tela do cliente. ALLOWLIST, nunca denylist: campo novo no contexto nao chega ao cliente por omissao. A linha e identificador do SISTEMA sai (lead_id, conversation_id, media_storage_path), dado da PESSOA fica (nome, telefone, e-mail) — confundir as duas produz um agente que nao sabe com quem fala. O interruptor se justifica sozinho: a projecao arma quando NENHUMA ferramenta de catalogo entrou no turno, porque e exatamente ai que os ids nao tem uso (as MCP os recebem como argumento, as nativas resolvem pelo closure). Liga-la sempre quebraria o agente que opera; por env seria mais um botao para errar. No passo 6, quando o Conversador perder as ferramentas de escrita, ela passa a valer sozinha. ACHADO NAO PREVISTO, corrigido aqui: search_knowledge devolvia chunk_id e knowledge_source_id CRUS ao modelo, dois UUIDs por resultado, em toda busca com RAG — sem uso nenhum do lado dele, ja que as citacoes sao montadas pelo codigo. Era fonte silenciosa do UUID cru que a medicao viu chegar na tela do cliente. Fecha tambem a releitura: sem isso a projecao seria decorativa, bastava o modelo chamar get_lead_context para receber tudo cru de volta. E o cabecalho do bloco de estado deixa de imprimir o nome da tabela (lead_state). UM BUG QUE QUASE ENTROU, registrado porque o teste dele fica: eu aplicava a traducao de erro ao CORPO das mensagens. "Meu site tem um webhook quebrado" — frase legitima de quem vende integracao — viraria "nao consegui concluir essa verificacao", e o agente responderia a uma pergunta que ninguem fez. Os testes medem contra os turnos REAIS de gpt-5.6-terra versionados em evidence/, com controle positivo em cada caso e falha declarada se a evidencia sumir. Fixture inventada mede a imaginacao de quem a escreveu, que e justamente a que ja falhou — senao o defeito nao teria acontecido. `projeta` ficou OPCIONAL no tipo de buildOpening porque tests/invariants/** e congelado por hook: torna-lo obrigatorio forcaria editar um invariante existente, que a catraca proibe com razao. Custo registrado no handoff. Evidencia observada: 18 testes novos; suite 2621 passed (271 arquivos); test:db verde (68 arquivos, 462 passed, 1 skipped); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(projecao): a traducao de erro pisava no conteudo do RAG do tenant Bug MEU, introduzido no commit anterior e pego pela sabotagem — nao por leitura. `traduzirErroCru` era aplicada a TODA string do retorno de ferramenta. O detector de vazamento considera "webhook" vazamento (com razao, na SAIDA), e a base de conhecimento de qualquer empresa que venda integracao fala webhook. O `content` do search_knowledge viraria "nao consegui concluir essa verificacao" linha apos linha: a projecao destruiria o RAG do tenant para proteger contra um vazamento que o gate de saida ja cobre. E a MESMA falha que a de reescrever a fala do cliente, corrigida horas antes, numa irma que nao se parece por fora: as duas nascem de aplicar num texto de TERCEIROS um filtro desenhado para texto do SISTEMA. Procurei a classe depois de pegar a primeira instancia e nao achei esta — ela so apareceu quando a sabotagem mostrou que o teste passava pelo mecanismo errado. Agora a traducao so alcanca valor sob chave de erro (lista fechada e curta: erro/error/message/code/reason/detail e os equivalentes em pt). Dois testes novos: o conteudo do RAG sobrevive intacto falando "webhook", e o campo de erro continua traduzido — a restricao nao desarmou a defesa. COMO O TESTE ANTERIOR PASSAVA SEM MEDIR: o detector pega UUID (categoria erro_cru), entao os casos de evidencia passavam pela TRADUCAO mesmo com a remocao de chave desligada — dois mecanismos redundantes, e eu atribuia o resultado ao errado. A sabotagem S2 derrubava 1 caso de 3; era o sinal. Evidencia observada: 20 testes; typecheck limpo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): o bug do RAG e a sabotagem que o pegou Registra no handoff vivo o defeito que entrei e corrigi (traducao pisando no conteudo da base de conhecimento), COMO o teste passava sem medir (dois mecanismos redundantes, credito atribuido ao errado), e o sinal que denunciou — S2 derrubando 1 caso de 3, quando devia derrubar os 3. Depois da correcao: S2 derruba 3 de 3. Oito sabotagens, oito reprovacoes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): nasce o OPERADOR — disparo imposto pelo runtime, nunca pelo modelo Passo 4 da spec 16. O papel que mexe no sistema e nunca fala com o lead: ele nao tem send_message no toolset. A separacao e por AUSENCIA, a unica forma que nao depende de o modelo obedecer. O DISPARO E DO RUNTIME. O job `operator_turn` e enfileirado no fim do turno, logo apos o checkpoint existir, com sourceEventId = job do Conversador (retry da fila nao gera um segundo Operador). Se o Conversador o "chamasse", devolveria o problema inteiro: voltaria a depender de o modelo lembrar, e o turno em que ele nao achasse necessario seria um lead parado no funil, em silencio. Depois do checkpoint porque a declaracao E o insumo — antes, ele leria o turno anterior. O CURTO-CIRCUITO E ONDE O PASSO 2 SE PAGA. `nada_a_declarar: true` => nao chama modelo (quem avaliou estava la, com todo o contexto). Declaracao AUSENTE => roda, porque ausente significa que NINGUEM avaliou, e e ai que promessa fica orfa. Os dois estados que o passo 2 se recusou a colapsar decidem, aqui, se a chave do self-hoster e gasta. Ha teste afirmando que eles levam a decisoes OPOSTAS. `operator_enabled` nasce false: migration nao liga sozinha um papel que gasta chamada de modelo por turno. `operator_model` null = herda o do Conversador. O loader le `?? false` para o clone que ainda nao aplicou a 0111 — schema velho desliga o papel, nunca gasta modelo por engano. Promessa declarada e nao cumprida vira `promise_unfulfilled` na Central, com copy que diz o que o CLIENTE espera, nao o que o sistema deixou de gravar — do lado de la existe uma pessoa que ouviu um compromisso. DOIS ERROS MEUS QUE AS CATRACAS DO REPO PEGARAM, nao eu: - criei um SEGUNDO bloco de `job_queue_kind_check` no baseline quando ja existe um. Isso quebra o update.sh de todo clone com vocabulario posterior. Eu tinha aplicado essa licao corretamente ao agent_inbox_items minutos antes e a irma passou batido — o padrao pego numa ocorrencia da alibi as outras. Consolidado no bloco unico (tests/unit/baseline-constraint-reconstruida.test.ts); - o kind novo divergia entre banco e TypeScript (tests/invariants/vocabulario-banco-x-typescript.test.ts). Evidencia observada: 7 testes de decisao + 5 invariantes de schema; suite 2630 passed (272 arquivos); test:db verde (69 arquivos, 467 passed); typecheck limpo; lint 0 erros / 189 warnings (mesmo numero da main). Schema em tripla: migration 0111 + apendice no baseline + MANIFEST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 4 registrado, com as duas catracas que me pegaram Registra o que o passo 4 entregou e o que NAO entregou (o Operador tem canal, ainda nao tem mao), a sabotagem com contagem prevista, e os dois erros meus que foram pegos por catraca do repo e nao por leitura minha — porque a saida limpa sem essa nota creditaria ao rigor o que foi merito do mecanismo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): o Operador ganha MAO — ferramentas proprias, e nenhuma delas fala Fatia 1 do passo 5. O papel deixa de so registrar promessa e passa a operar: le `operator_tool_ids`, monta o toolset pela ponte MCP e roda um turno de modelo com briefing proprio e atribuicao de custo propria (purpose 'operator_turn' — sem isso o gasto dele entraria como se fosse conversa, e "quanto custa ligar o papel" nao teria resposta). COLUNA PROPRIA, nao reuso de tool_ids, por tres razoes que se somam: a tela nao pode mentir (lista compartilhada faria a secao Operador configurar o que o Conversador executa); o teto de 20 se resolve por DIVISAO em vez de aumento (hoje sao ate 32 tools num prompt so, e o e2e capacidades-do-agente esta fora do CI por causa disso); e o passo 6 vira migration de DADOS, mover ids entre colunas. SEM CANAL, e isso e estrutural: send_message e tool NATIVA do engine e nao existe no catalogo, entao nao ha id que a ligue; e crm_send_whatsapp_message esta em BLOCKED_TOOL_IDS, que agora e exportado para ser aussertivel — garantia que nenhum teste consegue ler e garantia que ninguem percebe quando some. O system do papel fala de CRM com todas as letras, e PODE: esse texto nao alcanca cliente. E a assimetria exata da spec — no Conversador o mesmo vocabulario e o defeito de 30% medido. Sem ferramenta configurada ele nao chama modelo: descobrir que nao tem mao nao vale a chave do self-hoster. Herdar as do Conversador daria 20 capacidades a quem nao escolheu nenhuma, entao o default e vazio. TERCEIRA CATRACA DO REPO A ME PEGAR: VERSION_COLUMNS esta copiado em 6 arquivos e eu atualizei 1. O proprio teste conta que `cases_enabled` fez isso (entrou em 2 de 7) e o sintoma nao e typecheck nem teste — e um campo que "se desmarca sozinho" na tela depois do refresh, e o save seguinte grava por cima. Repeti o padrao que o comentario descrevia. Evidencia observada: 8 testes novos; suite unit 1459 passed (154 arquivos); test:db verde (69 arquivos, 467 passed); typecheck limpo. Schema em tripla: migration 0112 + apendice no baseline + MANIFEST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(ui): a tela ganha os PAPEIS — e diz a consequencia, nao o nome da feature Fatia 2 do passo 5. A configuracao do agente deixa de ser uma lista unica e passa a ter navegacao por papel: "Conversa com o cliente" e "Organiza o sistema". Os rotulos dizem o que cada papel FAZ — Conversador/Operador e o nosso vocabulario interno; quem configura pensa em "quem fala com meu cliente" e "quem organiza minha casa". SECOES, nao abas com save proprio: o rascunho e um so. Duas telas com dois saves fariam o usuario publicar metade da configuracao e criariam dois caminhos para o mesmo ai_agent_versions. A REGUA DA DISCIPLINA DE INFORMACAO e dizer a CONSEQUENCIA. Com o papel desligado, a tela explica o que CONTINUA acontecendo (o basico e registrado sozinho — decisao da spec 16 §2.1) e o que PARA (decidir sobre a operacao). Sem a primeira frase o usuario conclui que desligar deixa o sistema cego e liga por medo, nao por escolha. Ha teste exigindo as duas. Papel desligado NAO mostra a configuracao dele: convidar alguem a configurar o que nao vai rodar produz a conclusao de que o produto quebrou. O modelo herdado tem NOME na tela ("A mesma que conversa"), em vez de aparecer como pendencia — e ganhou o caminho de VOLTA: um Select nao oferece "nenhum" como item, entao sem o botao a escolha seria de mao unica e o usuario nao teria como saber por que. QUARTA CATRACA a me pegar nesta sessao: VERSION_COLUMNS em 6 arquivos, eu atualizei 1 (corrigido na fatia anterior). Ha um teste novo que varre a tela inteira atras do nosso vocabulario (MCP, tool_ids, operator_, job, payload). Evidencia observada: 8 testes de componente; suite unit 1459 passed antes deste arquivo; typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 5 parcial, e o bloqueio que impediu a prova de tela Registra as duas fatias entregues (mao do Operador + tela dos papeis) e o motivo de a prova de jornada NAO ter sido feita: .env.local aponta para a nuvem, e o playwright sobe o app com next start, que carrega .env.local. Rodar e2e nesta maquina hoje cria agentes e conversas de teste no banco de PRODUCAO. Nao e especifico deste epico — vale para qualquer sessao que rode e2e aqui agora. Fica declarado que o que esta provado da tela e comportamento de componente, nao jornada de usuario. Sao coisas diferentes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): a suite escrevia no banco de PRODUCAO — tres camadas de conserto Medido em 2026-08-06. `.env.local` de um checkout de trabalho aponta para a nuvem, e o playwright sobe o app com `next start`, que o carrega. A suite criava organizacoes, usuarios e agentes de teste no banco real, passando verde. A org `e2e-test-org` esta na producao deste projeto desde 2026-04-29. Ela nao chegou la por acidente de uma sessao: chegou porque esse era o comportamento normal da suite. ISOLAR O webServer NAO BASTAVA, e essa foi a descoberta. Os scripts de seed leem `.env.local` DIRETO DO DISCO, ignorando process.env — entao o env injetado no servidor nunca os alcancava, e as specs chamam esses seeds sozinhas no meio do teste. O sintoma que denunciou: o factor TOTP em `.e2e-creds.json` nao existia no banco local, porque tinha sido criado na nuvem. 93 arquivos tem esse padrao. As tres camadas: 1. `.env.e2e` (gerado por `pnpm e2e:env`, nao versionado) + injecao explicita no webServer, com guard que RECUSA rodar se o arquivo faltar — cair no .env.local em silencio e o modo de falha caro; 2. `pnpm e2e:build`, porque as tres NEXT_PUBLIC_* sao embutidas no BUNDLE: buildar com o env errado e trocar so no start deixaria a URL de producao dentro do JavaScript do browser. `node --env-file` nao serve aqui — o next build cria Workers e o Node recusa propagar a flag (ERR_WORKER_INVALID_EXEC_ARGV). O script prova as DUAS direcoes: host de producao ausente do bundle E host local presente (sem o controle positivo, "nao achei producao" pode ser um grep que nao acha nada); 3. `scripts/lib/env-de-teste.ts` — process.env VENCE o arquivo, e todo seed ANUNCIA o destino ("escrevendo em LOCAL" / "⚠️ REMOTO"). Um seed que escreve em producao acha os mesmos dados de teste de sempre e termina com "✅ Seed completo": sem a linha impressa, nada distingue os dois casos. `seed-e2e-credentials.ts` migrado (e o que a suite chama sempre). Os outros ~14 seeds e as ~78 sondas ficam declarados como divida no handoff. GANHO ESTRUTURAL do worktree dedicado: aqui nao existe `.env.local`, entao um script que o leia do disco falha ALTO (ENOENT) em vez de escrever na nuvem. O isolamento deixa de depender de disciplina. Evidencia observada: spec agente-novo-e-uso 5/5 passed contra o banco LOCAL (era 0/5 antes, por MFA — o factor do banco nao batia com o do arquivo); suite unit 1467 passed (155 arquivos); typecheck limpo; guard do playwright recusa sem .env.e2e (verificado). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): o worktree dedicado, e as tres camadas do isolamento do e2e Registra o achado que destravou a prova de tela e que vale para o repo inteiro: a suite escrevia em producao, e isolar so o webServer nao bastava porque os seeds leem .env.local do disco. Registra tambem o ganho estrutural do worktree (sem .env.local, script que o leia falha alto em vez de escrever na nuvem) e as tres dividas que sobraram: os ~92 scripts nao migrados, o spec da aba nova, e a decisao sobre as fixtures que ja estao na producao. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): os 16 seeds param de ler .env.local do disco, e um gate congela isso Completa o conserto de783ee85b. Todos os `scripts/seed-e2e-*.ts` passam a usar `credenciaisSupabaseDeTeste()`: process.env VENCE o arquivo, e cada um ANUNCIA contra qual banco vai escrever. Dois ja estavam com a precedencia certa e so ganharam o anuncio — `capacidades` lia o arquivo como fallback e `invite` usava process.env puro. O `capacidades` tambem estourava (ENOENT) num worktree sem .env.local, que e justamente onde a ausencia do arquivo E a protecao; o helper trata os dois casos. O GATE que impede a volta do padrao: tests/unit/seed-nao-le-env-local-do-disco. Sem ele o 17o seed nasce copiando o mais proximo, e escrever em producao nao e reparavel por code review depois do fato. Cobre `scripts/seed-e2e-*` — o que a suite roda SOZINHA; as ~78 sondas que alguem dispara a mao ficam declaradas no handoff, nao escondidas. A PRIMEIRA VERSAO DA REGEX DO GATE NAO MEDIA NADA. Era `/readFileSync\s*\(\s*(?:path\.join\([^)]*)?...\.env\.local/` e o `[^)]*` parava no `)` de `process.cwd()`, no meio de `path.join(process.cwd(), ".env.local")` — a alternativa nunca casava. Descoberto por SABOTAGEM: reintroduzir a leitura num seed nao vermelhou nada. A versao atual nao modela a sintaxe da chamada, procura o literal — regex que entende estrutura erra calada. Evidencia observada: os 16 seeds EXECUTADOS de verdade, 16/16 anunciando "escrevendo em LOCAL" (typecheck sozinho nao pega `env is not defined`, ja aconteceu nesta serie); gate 33 passed; typecheck limpo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): os 16 seeds migrados, e a regex do gate que nao media nada Registra a migracao completa dos seeds e o gate que a congela — inclusive o fato de a primeira regex do gate nao pegar o padrao real, descoberto por sabotagem e nao por leitura. Atualiza a divida remanescente: sao ~78 sondas, disparadas a mao, nao mais 14 seeds automaticos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * test(e2e): a aba do Operador provada pela TELA, com o refresh que pega campo perdido O passo 5 tinha teste de componente, que mede o componente e nao o produto: entre a escolha do usuario e o banco existem o formulario, a server action, o Zod, o VERSION_COLUMNS de seis arquivos e o SELECT que rele. Cada um pode perder um campo sem quebrar teste de unidade nenhum. O CASO CENTRAL e salvar → RECARREGAR → conferir, e nao e zelo generico: e o sintoma exato que agent-version-columns-drift descreve — "um campo que se desmarca sozinho depois do refresh, e o save seguinte grava o valor errado por cima". Aconteceu com cases_enabled (2 de 7 arquivos) e eu repeti com operator_* (1 de 6) nesta serie. Este e o teste que teria pego. DOIS ACHADOS AO ESCREVER, os dois medidos e nao supostos: 1. `default_agent_id` do seed de credenciais e um **rag_bot**, e a tela de papeis e do mcp_agent — apontar para ele fazia o spec falhar como se a aba nao existisse. O spec agora semeia a PROPRIA precondicao no beforeAll em vez de depender de um mcp_agent que outra spec deixou (depender da ordem alfabetica dos arquivos ja mordeu este repo). 2. Um login por teste custava 7 logins para exercitar uma aba, e o produto tem teto de 60 por IP a cada 5 minutos — o MESMO que protege a conta de um cliente real. A bateria estourava no 3o caso e a falha aparecia como "campo de MFA nao apareceu", parecendo defeito de produto onde havia limite de ambiente. Agora e UM login numa pagina compartilhada, com mode serial porque os casos passam a compartilhar navegacao. Evidencia observada: 7/7 passed em 1.8min contra o Supabase LOCAL (era 6/7 em 7.5min); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): a prova pela tela, e a sabotagem que mostrou o culpado Registra o spec e2e da aba do Operador (7/7 em 1.8min) e a sabotagem que o valida: removi operator_enabled do SELECT de UM dos seis arquivos — o defeito exato que ja aconteceu com cases_enabled — e o spec reprovou apontando o culpado na propria mensagem. Registra os dois achados de escrita: o default_agent_id do seed e um rag_bot (a tela de papeis e do mcp_agent), e um login por teste estourava o teto do proprio produto, fazendo limite de ambiente parecer defeito. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(ui): o botao Testar deixa de mostrar resposta que nenhum gate examinou Fatia 3 do passo 5, e o ultimo item aberto dele. O DEFEITO, medido em 2026-08-05 com controle positivo: a rota /versions/[vid]/test chama runAgent de lib/ai/runtime/agent.ts, marcado @deprecated, que nao importa runBeforeSend. Nenhum literal da cadeia (internal_vocabulary, case_promise_without_case, messaging_window_closed) aparece no build do app Next — ela vive no processo do worker. O self-hoster testava, via limpo, publicava, e producao divergia. Falha-em-verde no caminho de primeira impressao, que num produto instalado pela propria pessoa ninguem descobre. O CONSERTO NAO FINGE COBERTURA. Seis dos dez gates dependem de estado que so existe no turno real — contadores de pacing, janela de copias, carimbo da ultima inbound, ledger de envios, base legal. Fabricar esse estado daria um veredito INVENTADO, que e pior que veredito nenhum porque tem aparencia de prova. Entao a avaliacao cobre o que e decidivel so com o texto (o gate de vocabulario interno — justamente o que a medicao elegeu como mais caro: 30% dos turnos com prompt de operador vazavam) e DECLARA os outros nove na tela, com o motivo de cada um. Mostrar "nao verificado" em voz alta e o conserto. Silencio que parece aprovacao era o defeito. Os motivos sao escritos para o DONO DO NEGOCIO, nao para nos: "depende de quando o contato falou com voce pela ultima vez", nao "depende do send_ledger". Ha teste varrendo jargao nessa lista — repetir na tela de configuracao o vocabulario que a spec 16 existe para matar seria ironico. A lista de nao-avaliados e AMARRADA a BEFORE_SEND_GATES: acrescentar um gate a cadeia vermelha o teste e forca decidir de que lado ele cai. O caminho do stub tambem avalia — um caminho sem avaliacao voltaria a ser o verde que nao olhou para nada, so que mais dificil de notar. Evidencia observada: 6 testes novos, dois deles com os textos REAIS medidos (o que vazou e o que o modelo reescreveu depois do veto); suite 2685 passed (276 arquivos); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 5 fechado — o Testar declara o que nao consegue verificar Registra o ultimo item do passo 5 e a decisao que o define: nao fingir cobertura. Seis dos dez gates dependem do turno real, e fabricar esse estado daria veredito inventado — entao a tela mostra o que foi verificado E o que nao foi, com motivo escrito para o dono do negocio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): a CURA — a ferramenta sai do Conversador quando o Operador a assume Passo 6 da spec 16, o ultimo. O gate de vazamento e REDE: barra na saida e ensina o modelo. A cura e o Conversador nunca ter visto o vocabulario — ele nao repete o nome de uma ferramenta que nao esta no contexto dele. Foi pelo NOME que o vazamento voltou depois de a descricao ser limpa (crm_list_webhook_sources, medido), e nome de tool e contrato de wire que nao se renomeia. A REGRA QUE IMPEDE O BURACO: a capacidade so muda de dono quando o novo dono EXISTE. A nativa sai do Conversador apenas se o Operador estiver ligado E tiver, marcado na tela, um equivalente que faca a mesma coisa. Ligar o papel e esquecer de marcar crm_move_lead_stage tiraria o avanco do funil de um lado sem dar a ninguem — o funil pararia de andar em silencio, que e o modo de falha que este epico existe para combater. Tirar de um lado sem garantir o outro nao separa papeis, perde capacidade. E A INSTRUCAO SAI DO PROMPT JUNTO. Remover a ferramenta e manter a linha "Marque-o com update_lead_state" seria o pior dos dois mundos: o modelo tentaria chamar o que nao existe E o nome continuaria no contexto — ou seja, a cura nao teria acontecido. Ha teste separado so para isso. O QUE NAO SAI, MEDIDO CONTRA O CATALOGO REAL: save_lead_note, open_human_case e provide_case_update nao tem equivalente MCP — entrega-las deixaria a capacidade orfa. Ficam, com a divida declarada. request_human_handoff fica por outra razao: ela EXISTE no catalogo mas esta em BLOCKED_TOOL_IDS (a variante do catalogo nao silencia o harness), e passar a conversa para uma pessoa e decisao sobre a CONVERSA, com efeito imediato — adia-la ao turno seguinte deixaria o assistente respondendo depois de o lead pedir um humano, num caminho que a Meta fiscaliza. A tabela de equivalencia e verificada nos DOIS sentidos: toda nativa listada existe em AGENT_TOOL_DEFS e todo equivalente existe em TOOL_CATALOG. Um typo em qualquer lado faria o corte nunca acontecer, silenciosamente. Zero mudanca para quem nao ligou o papel: operator_enabled nasce false. Evidencia observada: 16 testes novos; suite 2701 passed (277 arquivos); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): passo 6 parcial — o mecanismo esta provado, o efeito nao Registra o que a cura entregou e, com o mesmo destaque, o que ela ainda NAO provou: o sinal de sucesso definido pela spec e a taxa de 30% cair, e isso exige re-rodar a medicao de 18 turnos com o Operador ligado. Nao foi feito. Esta provado o MECANISMO (a ferramenta e o nome somem do contexto do Conversador), nao o EFEITO (a taxa cair). Sao coisas diferentes. Registra tambem as 3 nativas que ficaram por falta de equivalente no catalogo — divida declarada, nao um corte pela metade escondido. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * medicao(passo6): re-medido — o vazamento NAO foi a zero, e agora se sabe por que O sinal que a spec 16 define para o passo 6 e "o vazamento vai a zero por AUSENCIA, nao por filtro". Medido: nao vai. 10 cenarios x 3 configuracoes, mesmo modelo da linha de base (gpt-5.6-terra): A CONTROLE (base replicada) 10.0% · B passo 6 como entregue 10.0% · C cura completa (operacao sai do Conversador) 10.0% As tres iguais, e o MESMO cenario vazando. Em C — sem nenhuma ferramenta de webhook no contexto — o modelo ainda disse "webhook": parte do vocabulario vem do MODELO, nao do contexto. Ausencia de ferramenta reduz superficie; nao elimina vazamento. A frase "so se fecha nao mostrando" fecha as portas 1, 2 e 3 — nao fecha o modelo. O CONTROLE NAO REPRODUZ OS 30%, e a causa esta identificada: o instrumento nao executa as ferramentas, entao nao exercita a porta 3 (dado retornado), que era 2 dos 3 vazamentos originais. Ele mede a porta 1/2 e achou 1 de 10 — exatamente o unico vazamento por NOME da linha de base. Calibracao parcial e explicada. O que a medicao CONFIRMA: o prompt e a variavel dominante. O valor do epico nao e "zero por ausencia" — e o dono do negocio deixar de precisar escrever "atenda E organize" no prompt do Conversador, porque agora existe um papel para isso. Foi essa troca que a linha de base mediu levando 30% a 0%. O INSTRUMENTO REPORTOU 0,0% NAS TRES CONFIGURACOES NA PRIMEIRA EXECUCAO. Parecia o passo 6 funcionando perfeitamente. As 30 chamadas tinham voltado HTTP 400 (gpt-5.6-terra recusa function tools com reasoning em /chat/completions), os textos vieram vazios, e o detector nao acha nada em texto vazio. O erro estava capturado por linha; o resumo so imprimia a taxa. Corrigido: recusa calcular taxa quando ha turnos que nao rodaram, e sai com codigo 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * medicao(passo6): a taxa TOTAL, com ferramentas executadas — 30% cai para 10% A medicao que faltava, agora com o controle CALIBRADO: a configuracao A reproduziu 30,0% com os MESMOS tres cenarios da linha de base, com as ferramentas rodando de verdade contra o banco local. Zero respostas vazias. A CONTROLE (21 capacidades) ......... 3/10 = 30,0% C sem as de OPERACAO (14) ........... 1/10 = 10,0% Tirar as sete ferramentas de operacao eliminou DOIS dos tres vazamentos — exatamente os que vinham do DADO retornado (unsafe_url:https_required e admin/manager+UUIDs). Sobrou o cenario 3: sem nenhuma ferramenta de webhook no contexto, o modelo AINDA disse "webhook". O termo veio dele, nao do que lhe foi mostrado. "So se fecha nao mostrando" fecha as portas 1, 2 e 3 — nao fecha o modelo. CONSEQUENCIA PARA O PASSO 6 COMO ESTA: ele entrega as NATIVAS com equivalente, e nenhum dos tres vazamentos veio delas. Nao move a taxa. O que move e entregar ao Operador tambem as `crm_list_*` de OPERACAO — que nao servem para responder pergunta de paciente, servem para o dono cuidar da casa, que e a definicao do papel. Recomendacao medida, com numero: 30% -> 10%. DOIS INSTRUMENTOS QUEBRADOS NO CAMINHO. O primeiro imprimiu 0,0% nas tres configuracoes (30 chamadas com HTTP 400, textos vazios, detector nao acha nada em vazio). O segundo coletou 10 turnos vazios e o spec passou — tres causas em cadeia, cada uma escondida pela anterior: credencial cifrada com outra chave, AI_CRED_AES_KEY de 21 bytes no .env.e2e (placeholder do CI, que nao exercita cifra), e o spec nunca validando a credencial que criava (funcionava so reaproveitando uma validada a mao). Os tres consertados, o gerador do .env.e2e passa a emitir chaves de 32 bytes de verdade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * feat(agente): as capacidades de OPERACAO saem do Conversador — 30,0% cai para 10,0% Aplica a recomendacao que a medicao anterior produziu, e re-mede com o codigo no caminho. A CONTROLE (todas as capacidades) .... 3/10 = 30,0% C codigo aplicado .................... 1/10 = 10,0% Ferramentas EXECUTADAS contra dados reais, zero respostas vazias nas duas corridas, controle calibrado contra a linha de base (mesmos tres cenarios). O MECANISMO E DIFERENTE do resto do modulo: aqui nao ha mapa de equivalencia, e a MESMA ferramenta mudando de dono. A condicao continua sendo a mesma — so sai se o Operador estiver ligado e a tiver marcada, para a capacidade nunca ficar orfa. Os dois vazamentos que sumiram vinham do DADO que essas ferramentas devolvem (unsafe_url:https_required e admin/manager+UUIDs), nao do nome nem da descricao. E a porta 3, a que "nao mostrar a ferramenta" so fecha tirando a ferramenta. crm_list_pipelines/stages/tags FICAM: saber em que etapa o lead esta e contexto de CONVERSA. Tira-las trocaria um vazamento por um atendimento pior. O LACO ENTRE CODIGO E MEDICAO ESTA FECHADO: o spec de coleta pergunta ao codigo quais capacidades sobram, em vez de repetir a lista. Uma copia paralela mediria a copia, e poderia divergir do que o turno monta sem nada vermelhar. Ha teste travando o conjunto que sobra — mudar a lista sem re-medir reprova. Os 10% que restam sao o residuo do MODELO: sem nenhuma ferramenta de webhook no contexto, ele ainda diz "webhook". O gate segue cobrindo isso. Evidencia observada: 21 testes; suite 2706 passed (277 arquivos); typecheck limpo; lint 0 erros. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * docs(handoff): fixtures de e2e removidas da producao, e dois defeitos que a operacao expos Removidas em 2026-08-06, com backup previo: as orgs `e2e-test-org` e `e2e-tenant-b` e tudo que pendurava nelas — 5 usuarios @deskcomm.test, 57 contatos, 144 mensagens, 56 leads, 18 agentes, 5 canais. Restaram 3 orgs, todas legitimas. `onboarding-teste` NAO foi tocada: nao e fixture automatica, pode ser teste manual do dono. A operacao expos DOIS DEFEITOS REAIS do produto, os dois da mesma familia (entidade que nao se consegue apagar): 1. DELETAR UMA ORGANIZACAO FALHA. O cascade apaga os filhos, e o trigger de audit de cada um tenta inserir em api_audit_log referenciando a org que ja nao existe → violacao de FK. So funciona apagando os filhos ANTES, com o pai vivo, e a propria api_audit_log por ultimo. 2. DELETAR UM AGENTE DONO DE LEAD FALHA. O SET NULL em crm_leads.owner_agent_id viola crm_leads_owner_kind_coherence, que exige owner_agent_id quando owner_kind='ai'. Ou seja: um agente que ja atendeu alguem nao pode ser removido. Nenhum dos dois foi corrigido aqui — a tarefa era limpar, e consertar schema de producao no mesmo movimento seria escopo que ninguem pediu. Ficam registrados. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(e2e): as sondas param de ler .env.local do disco — 108 arquivos, zero restantes Fecha a divida que sobrou do conserto dos seeds. Todas as sondas, provas e capturas de scripts/ e tests/ passam a usar `carregarEnvLocal()`. POR QUE UMA FUNCAO NOVA E NAO `credenciaisSupabaseDeTeste`: essas ~96 sondas nao leem so as credenciais do Supabase — cada uma pega o que precisa (WAHA_API_KEY, OPENAI_API_KEY, SUPABASE_DB_URL) de um Record<string,string> que montavam a mao. `carregarEnvLocal()` devolve o MESMO shape, com duas diferencas que sao o conserto inteiro: process.env VENCE o arquivo, e a ausencia do arquivo nao e erro — no worktree dedicado de e2e nao existe .env.local, e e essa ausencia que protege. O loader antigo estourava com ENOENT ali. Oito formatos diferentes de loader inline, migrados em quatro passadas com verificacao a cada uma. Os quatro ultimos casos (funcoes que remontavam o texto do arquivo para re-parsear) foram substituidos por inteiro, nao remendados. O GATE FOI ESTENDIDO e ficou mais rigoroso que o meu grep: ele pegou 11 arquivos que eu tinha dado por migrados (formatos multi-linha que a minha busca nao casava). Agora sao 188 casos cobrindo scripts/ + tests/ + tests/e2e/. O anuncio de destino continua exigido so dos SEEDS: sonda e disparada a mao, com a pessoa lendo a saida. LINT: subiu para 235 warnings com a migracao — 44 arquivos ficaram com import de `fs`/`path` orfao depois que o loader saiu. Removidos: 185 agora, contra 183 na base. Os +2 sao de arquivo novo, nao regressao. Evidencia observada: gate 188 passed; suite 2861 passed (277 arquivos); typecheck limpo; 0 arquivos lendo .env.local do disco. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(db): duas entidades que nao se conseguia apagar — organizacao e agente Achados operando: ao remover as fixtures de E2E da producao, os dois erros apareceram um depois do outro, cada um abortando a transacao inteira. Sao da mesma familia — uma escrita AUTOMATICA (trigger de audit, SET NULL de FK) reagindo ao DELETE e violando regra que vale para o estado normal, nao para a remocao. DEFEITO 1 · apagar uma ORGANIZACAO falhava. O cascade apaga os filhos e o trigger de audit de cada um insere em api_audit_log com o organization_id de uma org que ja nao existe. Agora o audit e pulado no DELETE quando a org sumiu — nao se perde auditoria, porque essa linha seria apagada pelo cascade em seguida. A checagem fica SO no ramo DELETE: um `exists` no INSERT/UPDATE cobraria um SELECT em todo hot path de escrita para cobrir um caso que nao ocorre la. DEFEITO 2 · apagar um AGENTE que ja atendeu falhava. A FK e ON DELETE SET NULL e o CHECK exige owner_agent_id quando owner_kind='ai'; o SET NULL zerava um lado e deixava o outro. Agora um BEFORE DELETE desfaz a atribuicao INTEIRA antes de a FK agir, e o lead fica sem dono em vez de meio-atribuido. O CHECK NAO foi afrouxado: tolerar 'ai' sem agente trocaria erro barulhento por dado incoerente em silencio — ha teste cobrando isso. TRES CATRACAS DO REPO ME PEGARAM AQUI: - a varredura de hardening: minha funcao nova nasceu SECURITY DEFINER executavel por `authenticated` e podendo escrever; meu revoke cobria `public` e `anon` e faltou a terceira origem. Revogada — seguro porque o unico call site e o trigger, e o Postgres nao exige EXECUTE do usuario para invocar funcao de trigger; - o hook de sequencia de migration: 0113 ja existe na main (e em 6 branches). Renumerada para 0115, que e o proximo livre em TODAS as branches locais; - e um caso MEU passava pelo motivo errado: o teste do CHECK montava o insert por subquery e caia em `stage_id` nulo — teria seguido verde se alguem afrouxasse o CHECK. Agora usa ids diretos. Schema em tripla: migration 0115 + apendice no baseline + MANIFEST. Evidencia observada: test:db verde (70 arquivos, 470 passed); typecheck limpo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * test(db): o caso do defeito 1 usava tabela SEM audit e nao media nada DESKCOMM_GOV_INVARIANTS_EDIT=1 — e a razao, porque a valvula pede que se cite: este invariante foi criado por mim no commit ANTERIOR desta mesma branch (9f98784a), nao e invariante estabelecido do repo. Nao e "invariante incomodo sendo silenciado": e um teste recem-escrito que a sabotagem provou nao medir nada. Manda-lo para a inbox em vez de corrigi-lo deixaria um caso verde permanentemente cego no repo. DESCOBERTO PELA SABOTAGEM, nao por leitura: desfiz o conserto do audit no baseline e o test:db seguiu VERDE, 470 passed. O caso usava `contacts`, que NAO tem o trigger fn_audit_log_row — ele vive so nas seis tabelas de IA (ai_agents, ai_agent_versions, ai_agent_runs, ai_provider_credentials, ai_routers, ai_router_members). O caso passava por AUSENCIA de audit, nao por conserto. Agora usa `ai_agents`, que e de onde o erro veio na producao (a org de fixture tinha 18 agentes). E ha um controle positivo que reprova se a tabela escolhida deixar de disparar audit — exatamente o furo que deixou isto passar. Evidencia observada: com o conserto DESFEITO, 1 failed (era 470 passed antes da correcao); com o conserto no lugar, test:db verde 471 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WKf64hXUbFatzofJHZREr7 * fix(seeds): a travessia de credenciais perdia o dbUrl e derrubava o CI O refactor de `scripts/lib/env-de-teste.ts` está certo no mérito — o motivo escrito no cabeçalho dele é um defeito sério de verdade: cada seed lia `.env.local` do disco ignorando `process.env`, e a suíte E2E semeava em PRODUÇÃO mesmo com o ambiente de teste injetado. Mas a travessia saiu incompleta. `CredenciaisSupabase` não declarava `dbUrl` e nenhum dos dois ramos a devolvia, enquanto **13 seeds** já liam `credenciais.dbUrl` para abrir `pg.Pool`. POR QUE NENHUM GATE PEGOU: `tsconfig.json` tem `"exclude": ["scripts/**"]`, então os seeds não são typechecked — ler propriedade que o tipo não declara não custa nada ao compilador. (O `as Record<string, string>` no objeto `env` de cada seed não é a causa; eu cheguei a achar que fosse, e é o `exclude` que decide.) E O SINTOMA ESCOLHIA O AMBIENTE, que é o pior tipo de defeito para achar: local → cai no ramo do ARQUIVO, ou o shell já tem a variável → passa CI → o workflow EXPORTA as credenciais, o ramo "ambiente" vence → undefined Com `connectionString: undefined` o `pg` cai no default do libpq. No CI isso é `ECONNREFUSED 127.0.0.1:5432` — o Supabase local publica na 54322. Medido nesta máquina, mesmo comando, com as credenciais exportadas no ambiente (o que o CI faz): antes → [seed-e2e-escalacao] falhou: database "rafaelmelgaco" does not exist depois → [seed-e2e-escalacao] escrevendo em LOCAL: http://127.0.0.1:54321 (origem: ambiente) … travas ligadas O erro local difere do erro do CI porque esta máquina tem um Postgres local — mesma causa (connection string indefinida), superfície diferente. O ramo "ambiente" cai no `.env.local` antes de desistir: quem exporta URL + service role não necessariamente exporta a conexão direta. Isso não contradiz a regra da função — `process.env` continua vencendo quando existe, e há um caso de teste afirmando exatamente isso. O teste vive em `tests/` e não em `scripts/` de propósito: importar o helper de lá é o que o traz para dentro do `tsconfig`, que é a razão de fundo de o defeito ter passado. Sabotado nos dois ramos: zerar o do ambiente reprova 3 casos, o do arquivo reprova 1. typecheck limpo, lint sem erros, 2927 testes unitários verdes. * fix(e2e): o CI cria o .env.e2e que a suíte passou a exigir O PR protegeu a suíte contra escrever em produção — `playwright.config.ts` exige um `.env.e2e` e falha alto quando falta. A proteção está certa; o que saiu incompleto foi a outra ponta: `.github/workflows/e2e.yml` não foi tocado. O job seguiu rodando `pnpm build` e chamando a suíte sem gerar nada, e o e2e morreu em `Falta o .env.e2e` depois de 3m48s. Três consertos, todos medidos: 1. O workflow gera o `.env.e2e` (`pnpm e2e:env`) depois de o stack local subir, e builda com `pnpm e2e:build` — não `pnpm build`. As três `NEXT_PUBLIC_*` são embutidas no BUNDLE durante o build: buildar com um env e trocar só no `next start` deixaria a URL errada dentro do JavaScript do browser. Medido: `==> OK (controle): o host local (127.0.0.1:54321) ESTÁ no bundle`. 2. `gerar-env-e2e.sh` usa o `supabase` do PATH quando existe (é o que o CI instala via supabase/setup-cli) e cai no `npx` só na máquina do dev. A CLI não é dependência deste projeto — insistir no `npx` custaria um download do registry em cada uma das três chamadas. 3. O heredoc que escreve o `.env.e2e` não é quotado, e um comentário dentro dele tem crases. O shell tentou EXECUTAR o que estava entre elas: scripts/gerar-env-e2e.sh: linha 79: credential_decrypt_failed: comando não encontrado Saía em stderr e o arquivo gerado perdia exatamente o identificador que o comentário existe para nomear (`# agente morria em , o que aparecia`). Varri o heredoc inteiro: as crases eram a única expansão indesejada, as `$VAR` são todas de propósito. A guarda nova é sobre a ORDEM, não a presença: `tests/unit/ e2e-workflow-honra-o-env.test.ts` casa quem gera o arquivo contra quem o consome e exige que o índice do primeiro seja menor. Sabotei as quatro propriedades uma a uma — passo removido, ordem invertida, `pnpm build` de volta, e a exigência sumindo do playwright.config (guarda de vacuidade) — e cada uma vermelha o caso certo. gov:verify: 0 erros de lint, 286 arquivos / 2931 testes (2927 + 4). e2e:build: exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HstH7nNmrTvCtZsasppeHj * fix(e2e): uma fonte de ambiente, em vez de três redações do mesmo segredo O conserto anterior destravou a suíte (27 specs passaram, contra zero). As 8 que sobraram vermelhas — 6 de system-update no `heartbeat`, o drain do anti-SSRF e o invite-lifecycle — eram todas 401 em rota interna, e a causa era a mesma: `INTERNAL_SECRET` estava declarado em TRÊS lugares com valores diferentes. .env.e2e e2e-placeholder-nao-e-segredo → o servidor env: dos 2 passos ci-placeholder-nao-e-segredo → o processo de teste .env.local do seed ci-placeholder-nao-e-segredo → os seeds MEDIDO (Playwright 1.5x, webServer que imprime o que recebeu): var só no process.env → CHEGA (mescla, não substitui) var nos dois, valores dif. → vence a do `env:` do config Ou seja: o servidor pegava a do arquivo, o teste mandava a do passo, e igualar as strings à mão só adiaria a próxima divergência. O conserto é fonte única — o workflow publica o `.env.e2e` inteiro no `$GITHUB_ENV` e o `.env.local` do seed passa a ser `cp .env.e2e`. Zero segredos redigitados no workflow (era 24 linhas). De brinde, o processo de teste passa a ver as chaves de CIFRA de verdade (32 bytes, geradas pelo script) em vez do rótulo de 21 caracteres que o bloco `env:` fixava — o mesmo que o comentário do script já registrava como causa de `credential_decrypt_failed`. Dois comentários que a medição desmentiu, corrigidos junto: - `playwright.config.ts` avisava "NÃO declare `env:` aqui sem espalhar `process.env` junto ... declarar substitui o ambiente inteiro" — e a linha logo acima declarava `env: envDoE2E()`. O arquivo se contradizia, e a afirmação é falsa: o Playwright mescla. Trocado pela tabela medida. - `gerar-env-e2e.sh` prometia "valores iguais aos do CI" e eles não eram iguais. É a frase que fazia a divergência parecer impossível. A guarda ganhou as duas propriedades novas, sabotadas uma a uma: redigitar `INTERNAL_SECRET` num passo → vermelho; parar de publicar o `.env.e2e` (que faria o caso anterior passar por ausência) → vermelho. gov:verify: 0 erros de lint, 286 arquivos / 2933 testes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HstH7nNmrTvCtZsasppeHj --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
4.0 KiB
TypeScript
85 lines
4.0 KiB
TypeScript
/**
|
|
* A PROVA DE TELA dos cenários 25 e 26: o card ANDA no Kanban quando o agente
|
|
* avança o funil dele — sem reload e sem ninguém tocar.
|
|
*
|
|
* Sem realtime de propósito, pelo mesmo caminho do cenário 23: a rede de
|
|
* segurança traz a mudança, e o que estava bloqueado por uma investigação em
|
|
* disputa passa a depender de uma peça que não pode mentir.
|
|
*/
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
import { chromium } from "@playwright/test";
|
|
import { createClient } from "@supabase/supabase-js";
|
|
|
|
import { BASE, CARD_ATTR, login } from "./qa-helpers";
|
|
import { sincronizaEstagioDoAgente } from "@/lib/leads/agent-stage-sync";
|
|
import { carregarEnvLocal } from "../scripts/lib/env-de-teste";
|
|
|
|
const env = carregarEnvLocal();
|
|
const admin = createClient(env.NEXT_PUBLIC_SUPABASE_URL!, env.SUPABASE_SERVICE_ROLE_KEY!, {
|
|
auth: { persistSession: false },
|
|
});
|
|
const ORG = "6e567068-fd1c-4f94-ae1f-40e0334be190";
|
|
const PIPE = "35bf4ac9-c5e0-4f7d-846a-99b1bcc92d69";
|
|
|
|
/** Em que COLUNA do board o card está — a pergunta que a tela responde. */
|
|
async function colunaDoCard(page: import("@playwright/test").Page, leadId: string): Promise<string> {
|
|
return page.evaluate((id) => {
|
|
const card = document.querySelector(`[data-rfd-draggable-id="${id}"]`);
|
|
if (!card) return "(card não está na tela)";
|
|
const coluna = card.closest("[data-rfd-droppable-id]")?.parentElement;
|
|
const titulo = coluna?.querySelector("h3, h2, [class*='font-medium']");
|
|
return titulo?.textContent?.trim() ?? "(coluna sem título)";
|
|
}, leadId);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const { data: st } = await admin
|
|
.from("crm_stages").select("id, name, slug, agent_stage_hint").eq("pipeline_id", PIPE).order("position");
|
|
const estagios = (st ?? []) as Array<{ id: string; name: string; slug: string; agent_stage_hint: string | null }>;
|
|
const primeiro = estagios[0]!;
|
|
const destino = estagios.find((e) => e.agent_stage_hint === "negotiating")!;
|
|
|
|
const contactId = randomUUID();
|
|
await admin.from("contacts").insert({ id: contactId, organization_id: ORG, name: "sonda tela 25" });
|
|
const leadId = randomUUID();
|
|
await admin.from("crm_leads").insert({
|
|
id: leadId, organization_id: ORG, pipeline_id: PIPE, stage_id: primeiro.id,
|
|
contact_id: contactId, title: "O agente vai mover este", position_in_stage: 1,
|
|
});
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newContext({ viewport: { width: 1600, height: 950 } }).then((c) => c.newPage());
|
|
await login(page, "manager");
|
|
await page.goto(`${BASE}/app/pipelines/${PIPE}`, { waitUntil: "networkidle" });
|
|
await page.locator(`[${CARD_ATTR}="${leadId}"]`).waitFor({ timeout: 15000 });
|
|
await page.waitForTimeout(2500);
|
|
|
|
console.info(`ANTES · o card está em: ${await colunaDoCard(page, leadId)}`);
|
|
console.info(` (o tenant chama de "${primeiro.name}"; o agente vai pedir "negotiating", que aqui é "${destino.name}")`);
|
|
await page.screenshot({ path: "evidence/wave8-agente-move-antes.png" });
|
|
|
|
// ── O AGENTE AVANÇA O FUNIL DELE. Ninguém toca na tela. ──────────────────
|
|
const r = await sincronizaEstagioDoAgente(admin, {
|
|
organizationId: ORG, contactId, passo: "negotiating",
|
|
});
|
|
console.info(`\nAGENTE · moveu=${r.moveu} · motivo=${r.motivo} · destino=${r.stageName ?? "-"}`);
|
|
|
|
// A rede roda a cada 45s; o retorno à aba a exercita antes.
|
|
await page.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
|
|
let onde = "";
|
|
for (let i = 0; i < 8; i++) {
|
|
await page.waitForTimeout(8000);
|
|
onde = await colunaDoCard(page, leadId);
|
|
if (onde.includes(destino.name)) { console.info(` chegou à tela em ~${(i + 1) * 8}s`); break; }
|
|
}
|
|
console.info(`\nDEPOIS · o card está em: ${onde}`);
|
|
console.info(` o card ANDOU sem reload? ${onde.includes(destino.name) ? "SIM" : "NÃO ← ficou parado"}`);
|
|
await page.screenshot({ path: "evidence/wave8-agente-move-depois.png" });
|
|
|
|
await admin.from("crm_leads").delete().eq("id", leadId);
|
|
await admin.from("contacts").delete().eq("id", contactId);
|
|
await browser.close();
|
|
}
|
|
void main();
|