mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
fix(crm-vivo): o motivo e o ator param na borda da API, não na tela [wave 3, bloco 2.5]
A 0071 criou reason/actor_kind, o tipo TimelineItem passou a declará-los e as duas
superfícies passaram a lê-los — mas TIMELINE_COLS da rota ficou no vocabulário antigo
e nunca os pediu ao banco.
O defeito compilava verde porque os campos são OPCIONAIS no tipo: `it.reason` virava
undefined, o corpo da linha caía no resumo do payload (JSON de UUID na cara do usuário)
e todo ator virava "não registrado". A interrogação do opcional é que calava o compilador.
Provado na tela antes e depois (tests/sonda-timeline-contato.ts):
antes: stage_changed / {"pipeline_id":"35bf4a…","to_stage_id":"ab0e70…"}
depois: Mudou de estágio · Você/time / Movido de Proposta enviada para Avaliação
Isto fecha de uma vez os dois vermelhos do cenário 10 (motivo específico ausente e
ator ausente): eram uma causa raiz só, na borda do transporte.
Na sonda, duas frouxidões minhas corrigidas junto: espera por CONTEÚDO em vez de relógio
(BRIEFING §7.1, lei que eu mesmo escrevi e era o único a quebrar) e contagem de <li>
escopada ao painel — o menu lateral também é lista e inflava a medida.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gpdoAwEGCcM9DwzC6qtkL
This commit is contained in:
co-authored by
Claude Opus 5
parent
6de50ba268
commit
1d6a762008
@@ -20,8 +20,23 @@ import type { TimelineItem } from "@/lib/types/contacts";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* As colunas que a timeline entrega. **Tem que acompanhar `TimelineItem`**
|
||||
* (`lib/types/contacts.ts`) — e essa concordância não é vigiada por nada: isto
|
||||
* é uma string, o `.select()` aceita qualquer coisa, e o resultado é convertido
|
||||
* para `TimelineItem` sem o compilador conferir.
|
||||
*
|
||||
* Já custou uma vez: a `0071` criou `reason`/`actor_kind`, o tipo passou a
|
||||
* declará-los, a tela passou a lê-los — e esta lista ficou para trás. Como os
|
||||
* campos são **opcionais** no tipo, `it.reason` virava `undefined`, o corpo da
|
||||
* linha caía no resumo do payload (JSON de UUID na cara do usuário) e todo ator
|
||||
* virava "não registrado". Tudo isso compilando verde: a interrogação do
|
||||
* opcional é que cala o compilador.
|
||||
*
|
||||
* Campo novo em `TimelineItem` → campo novo AQUI, no mesmo commit.
|
||||
*/
|
||||
const TIMELINE_COLS =
|
||||
"id, organization_id, lead_id, contact_id, source_module, source_id, type, payload, metadata, performed_at, performed_by_user_id";
|
||||
"id, organization_id, lead_id, contact_id, source_module, source_id, type, payload, metadata, performed_at, performed_by_user_id, actor_kind, actor_agent_id, reason, evidence";
|
||||
|
||||
interface Cursor {
|
||||
performed_at: string;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Sonda do orquestrador — por que a aba Timeline do contato aparece VAZIA
|
||||
* enquanto o banco tem 5 atividades para o mesmo contato?
|
||||
*
|
||||
* A tela não mostrou nem o estado vazio ("Nenhuma atividade registrada ainda"),
|
||||
* nem esqueleto, nem erro. Nada. Isso é um quarto estado que o componente não
|
||||
* declara — então a resposta não está no pixel, está no que a rota devolveu.
|
||||
*
|
||||
* Esta sonda não julga a tela: ela grava o que a API respondeu e o que o console
|
||||
* gritou, para o veredito sair de dado e não de suposição.
|
||||
*
|
||||
* Run: E2E_PORT=3020 npx tsx tests/sonda-timeline-contato.ts
|
||||
*/
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
import { BASE, EVIDENCE, login, shotPage } from "./qa-helpers";
|
||||
|
||||
const CONTATO_ID = "5b37994e-4ba0-46ff-99ca-7b8364a67909"; // Ana Souza LGPD E2E
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const browser = await chromium.launch();
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
const errosConsole: string[] = [];
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") errosConsole.push(m.text().slice(0, 300));
|
||||
});
|
||||
page.on("pageerror", (e) => errosConsole.push(`pageerror: ${e.message.slice(0, 300)}`));
|
||||
|
||||
// Guardado em PROPRIEDADE, não em `let`: atribuição que só acontece dentro de
|
||||
// callback não é vista pela análise de fluxo do TS, que estreita a variável
|
||||
// para `null` e faz o ramo verdadeiro virar `never`. Acesso a propriedade
|
||||
// preserva o tipo declarado.
|
||||
const capturas: { timeline: { status: number; corpo: string } | null } = { timeline: null };
|
||||
// Um 429 sem endereço é ruído; com endereço é diagnóstico. Se quem apanhar for
|
||||
// o realtime-token, o canal volta a assinar como anônimo e o defeito que já
|
||||
// corrigimos ressuscita calado — exatamente a falha que não se vê.
|
||||
const estourados: string[] = [];
|
||||
page.on("response", async (r) => {
|
||||
if (r.status() === 429) estourados.push(r.url().replace(BASE, ""));
|
||||
if (!r.url().includes("/timeline")) return;
|
||||
capturas.timeline = {
|
||||
status: r.status(),
|
||||
corpo: (await r.text().catch(() => "")).slice(0, 600),
|
||||
};
|
||||
});
|
||||
|
||||
await login(page, "admin");
|
||||
await page.goto(`${BASE}/app/contacts/${CONTATO_ID}`, { waitUntil: "networkidle" });
|
||||
await page.getByRole("tab", { name: "Timeline" }).click();
|
||||
|
||||
// Espera por CONTEÚDO, nunca por relógio (BRIEFING §7.1) — a busca só dispara
|
||||
// depois do clique, e um `waitForTimeout` ganha nesta máquina e perde na do CI.
|
||||
// Aceita os DOIS desfechos legítimos: linha renderizada OU estado vazio. Se
|
||||
// esperasse só pela linha, uma timeline honestamente vazia travaria a sonda.
|
||||
const painel = page.locator("main");
|
||||
await painel
|
||||
.locator("li")
|
||||
.or(painel.getByText("Nenhuma atividade registrada"))
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 15_000 });
|
||||
|
||||
// O que o usuário realmente enxerga na área abaixo das abas.
|
||||
const textoVisivel = (await painel.innerText().catch(() => "")).slice(0, 400);
|
||||
// Escopado ao painel: o menu lateral também é lista, e contar `li` solto
|
||||
// inflaria o número com navegação — medida frouxa que eu mesmo reprovaria.
|
||||
const linhas = await painel.locator("li").count();
|
||||
|
||||
await shotPage(page, "sonda-timeline-contato.png");
|
||||
|
||||
console.info("=== RESPOSTA DA ROTA /timeline ===");
|
||||
console.info(capturas.timeline ? `HTTP ${capturas.timeline.status}\n${capturas.timeline.corpo}` : "NENHUMA CHAMADA À ROTA");
|
||||
console.info("\n=== QUEM LEVOU 429 ===");
|
||||
console.info(estourados.length ? estourados.join("\n") : "(nenhum)");
|
||||
console.info("\n=== ERROS DE CONSOLE ===");
|
||||
console.info(errosConsole.length ? errosConsole.join("\n") : "(nenhum)");
|
||||
console.info("\n=== ITENS <li> RENDERIZADOS ===");
|
||||
console.info(String(linhas));
|
||||
console.info("\n=== TEXTO VISÍVEL ===");
|
||||
console.info(textoVisivel);
|
||||
console.info(`\nprint: ${EVIDENCE}/sonda-timeline-contato.png`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
void main();
|
||||
Reference in New Issue
Block a user