mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
`lib/mcp/tools/catalogo/*.ts` declarava `description` em 51 capacidades; o tipo a documentava como "Texto tecnico entregue ao MODELO" e sete cabeçalhos repetiam "`description` fala com o modelo". Nenhum consumidor lia esse campo: a ponte do turno (`lib/ai/runtime/tools.ts`) monta `def.description`, do HANDLER, e a rota `/api/v1/mcp/tools` também. As duas fontes divergiam em 48 das 51. ## Por que remover, e não sincronizar Fonte única mudaria o que 48 tools dizem ao modelo — risco alto, ganho zero. Um gate de paridade obrigaria manter dois textos sincronizados para sempre, com a duplicata seguindo lá para ser editada por engano. Remover mata a armadilha na raiz: não dá para editar o lugar errado se o lugar não existe. E a remoção SE AUTO-VERIFICA. Tirei o campo do tipo e o `tsc` apontou cada leitura — prova mais forte que grep. ## O que o typecheck achou: a dívida não era teórica Um leitor, e o pior possível: `evidence/ia-360-w4/medicao-vazamento/remedir-com-operador.ts`, o script que mede vazamento de vocabulário do agente. A função se chama `descreverFerramentas` e o comentário diz "A ferramenta como o modelo a vê: nome + descrição, que é o que pode vazar" — e lia `TOOL_CATALOG.description`, exatamente o texto que o modelo NÃO vê. NÃO MEDIDO: se isso muda o resultado daquela medição. O `name` é idêntico nas duas fontes e é o vetor principal de vazamento, então o efeito pode ser nulo, mas não rodei. Corrigi a fonte para `allTools` e deixei a ressalva no script. Não reabri a medição arquivada de outra branch. ## O buraco que a remoção expôs `tests/unit/catalogo-servido.test.ts` testava a junção com FIXTURES (`description: "faz algo"`), nunca com o catálogo real — nenhum gate garantia que uma capacidade servida tem descrição. Esvaziar a de um handler passaria calado: a tela sem explicação e o modelo com uma ferramenta sem contrato. Caso novo: toda capacidade servida tem descrição não-vazia E ela é IDÊNTICA à do handler. A segunda metade é a que importa — se reaparecer uma cópia no catálogo e a junção preferi-la, reprova. Sabotagens: `description` de um handler vira "" → 1 → 1 (a primeira tentativa não sabotou nada: escrevi `description: "" ||`, e `"" || "texto"` devolve o texto — instrumento quebrado, não gate fraco); junção servindo outro texto → 1 → 2. ## Correção de um número que publiquei A mensagem do commitc56416aadiz "1819 unitários". O real naquele SHA é 1818, medido depois com `git stash` e a árvore limpa emc56416aa. Rodei a suíte com a árvore ligeiramente diferente da commitada (antes do `rm` do protótipo e do stage final) e publiquei como se fosse do commit. Régua daqui em diante: número que sai em artefato público é medido DEPOIS do stage. ## Saldo 130 linhas removidas, 63 acrescentadas, 9 arquivos. Os 7 cabeçalhos falsos foram reescritos com o que é verdade e POR QUE o campo não existe mais — para ninguém "completar" o catálogo de volta. O BRIEFING-ia-360.md acompanhou. Provado na tela: `/app/ai/agents/<mcp_agent>`, Supabase local (HTML de /login com 2× `127.0.0.1:54321`, 0× `*.supabase.co`), a capacidade segue com rótulo, explicação, categoria e área — 6/6, zero erro de console. typecheck 0 · lint 0 errors · 1819 unitários (1818 + 1 caso novo) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkS3mzwtXughmjVC5FCoNo
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
/**
|
|
* PROVA DE TELA — o rótulo da capacidade `crm_list_at_risk_leads` na tela em que
|
|
* o dono do negócio decide se liga a capacidade no agente.
|
|
*
|
|
* Por que isto é prova e não formalidade: o passo 4 fez a capacidade passar a
|
|
* listar TAMBÉM as demandas sem próximo passo. Se o rótulo continuasse dizendo
|
|
* só "quem esfriou e está sem resposta", quem configura ligaria uma coisa
|
|
* achando que ligou outra — invariante 5 (informação com propósito) na
|
|
* superfície de configuração, que é onde a decisão acontece.
|
|
*
|
|
* Uso: `npx tsx tests/sonda-capacidade-radar-tela.ts`
|
|
* Requer o app em produção na 3100 apontando para o Supabase LOCAL.
|
|
*/
|
|
import { chromium } from "@playwright/test";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const BASE = process.env.E2E_PORT ? `http://127.0.0.1:${process.env.E2E_PORT}` : "http://127.0.0.1:3100";
|
|
const AGENTE = "36ed74d7-3505-46ab-bc2b-9656626e118c"; // kind=mcp_agent — o rag_bot cai no editor legado, sem ToolPicker
|
|
const c = JSON.parse(readFileSync("/Users/rafaelmelgaco/DeskcommCRM/.e2e-creds.json", "utf8"));
|
|
|
|
async function main(): Promise<void> {
|
|
const b = await chromium.launch();
|
|
const p = await b.newPage({ viewport: { width: 1280, height: 900 } });
|
|
const erros: string[] = [];
|
|
p.on("console", (m) => {
|
|
if (m.type() === "error") erros.push(m.text());
|
|
});
|
|
|
|
await p.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
|
|
await p.click('input[type="email"]');
|
|
await p.locator('input[type="email"]').pressSequentially(c.users.manager.email, { delay: 8 });
|
|
await p.click('input[type="password"]');
|
|
await p.locator('input[type="password"]').pressSequentially(c.password, { delay: 8 });
|
|
await p.click('button[type="submit"]');
|
|
await p.waitForURL(/\/app/, { timeout: 30000 });
|
|
|
|
await p.goto(`${BASE}/app/ai/agents/${AGENTE}`, { waitUntil: "networkidle" });
|
|
await p.waitForTimeout(1500);
|
|
|
|
// A lista completa de capacidades vive atrás do modo avançado.
|
|
const toggle = p.locator('[data-testid="toggle-avancado"]');
|
|
if (await toggle.count()) {
|
|
await toggle.first().click();
|
|
await p.waitForTimeout(800);
|
|
}
|
|
|
|
const alvo = p.locator('[data-testid="capacidade-crm_list_at_risk_leads"]');
|
|
await alvo.first().scrollIntoViewIfNeeded().catch(() => {});
|
|
await p.waitForTimeout(300);
|
|
|
|
const m = (await p.evaluate(`(() => {
|
|
var el = document.querySelector('[data-testid="capacidade-crm_list_at_risk_leads"]');
|
|
if (!el) return { presente: false };
|
|
var r = el.getBoundingClientRect();
|
|
var texto = el.innerText.replace(/\\s+/g, " ").trim();
|
|
return {
|
|
presente: true,
|
|
texto: texto,
|
|
// Medidas por ferramenta, nunca a olho.
|
|
largura: Math.round(r.width),
|
|
altura: Math.round(r.height),
|
|
visivel: r.width > 0 && r.height > 0,
|
|
dentro_da_viewport: r.left >= 0 && r.right <= window.innerWidth,
|
|
scroll_horizontal: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
// O gate de linguagem proíbe jargão para quem configura.
|
|
jargao: ["mcp","api","payload","uuid","schema","query","lead","stage"].filter(function (j) {
|
|
return new RegExp("\\\\b" + j + "s?\\\\b", "i").test(texto);
|
|
}),
|
|
};
|
|
})()`)) as Record<string, unknown>;
|
|
|
|
console.log(JSON.stringify({ ...m, erros_de_console: erros }, null, 2));
|
|
|
|
await p.screenshot({ path: "evidence/passo4-capacidade-radar.png", fullPage: false });
|
|
await b.close();
|
|
|
|
const texto = String(m.texto ?? "");
|
|
const casos: Array<[string, boolean]> = [
|
|
["a capacidade está na tela", m.presente === true],
|
|
["o rótulo novo apareceu", texto.includes("ficou sem próximo passo")],
|
|
["a explicação cita quem está esperando", texto.includes("esperando sem que nada")],
|
|
["sem jargão para quem configura", (m.jargao as string[]).length === 0],
|
|
["sem scroll horizontal", m.scroll_horizontal === false],
|
|
["sem erro de console", erros.length === 0],
|
|
];
|
|
let falhas = 0;
|
|
for (const [nome, ok] of casos) {
|
|
console.log(`${ok ? " ok " : "FALHA "} ${nome}`);
|
|
if (!ok) falhas += 1;
|
|
}
|
|
console.log(falhas === 0 ? "\nTODOS OS CASOS PASSARAM" : `\n${falhas} CASO(S) FALHARAM`);
|
|
process.exit(falhas === 0 ? 0 : 1);
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|