mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
Medido em `pnpm test:unit` sobre a árvore mesclada (905 arquivos, 7 vermelhos; dois
eram timeout sob carga e passam isolados; um já saiu no commit anterior). Os quatro
restantes:
- lib/random-id: ExtensionsManager.tsx ("use client") chamava crypto.randomUUID()
cru em dois lugares. Em http://IP (VPS sem TLS) o navegador não expõe randomUUID
e a tela morreria antes do fetch — é o achado nº 9 do deploy VPS, e a casa usa
randomId() por isso.
- documentacao-aponta-para-o-que-existe: o gate comparava o alvo do link com o
disco SEM decodificar %20/%C3%A3, então o link válido do TRIAGEM.md para o
PROG-017 (nome com espaço e acento) reprovava como morto. Causa raiz no gate:
decodifica antes de existsSync; `%` solto continua valendo como está.
- navegacao-registry "some com a seção que ficou vazia pela permissão": Extensões
tinha entrado em Organização › "Dados e acesso" (LGPD e API tokens, admin-only)
visível ao viewer, e a seção deixou de sumir. Extensões passa para "Sua empresa",
onde o viewer já vê Time; o teste do hub, que a branch tinha ajustado para a
seção antiga, agora afirma a nova e que "Dados e acesso" segue sumindo. A spec
dizia "grupo CRM" na tabela do Sistema Vivo; corrigida para o que o código faz.
- numero-de-jornada-e-unico: a main usou J22 para o App da Meta; J23 está reservada
numa branch do lote 11; a jornada das extensões passa a J24.
Gates rodados: random-id, documentacao-aponta, navegacao-registry, nav-hub,
numero-de-jornada, navegacao-completude, i18n-espanhol, idioma-da-interface,
ExtensionsManager.test, e2e-cobertura-completa → 10 arquivos, 86 casos verdes;
typecheck 0 erros.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhoHBoTBHeSxHtuSsBZS3V
201 lines
8.4 KiB
TypeScript
201 lines
8.4 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
/**
|
|
* Gate contra a classe de defeito mais silenciosa da documentação: a afirmação
|
|
* que envelheceu sem ninguém voltar.
|
|
*
|
|
* Ela tem duas formas, e as duas foram medidas neste repo em 2026-08-14:
|
|
*
|
|
* 1. **Ponteiro para o vazio.** `CLAUDE.md` e `AGENTS.md` mandavam procurar o job
|
|
* `imagens-ok` em `.github/workflows/imagens.yml` — arquivo que nunca existiu;
|
|
* o job vive em `publish-image.yml`. Um path citado em documento de autoridade
|
|
* se lê como prova, e quem o segue conclui que a coisa não existe.
|
|
*
|
|
* 2. **Nota de pendência que sobreviveu à pendência.** O mesmo parágrafo da
|
|
* doutrina de packaging mentiu DUAS vezes sobre o mesmo fato, em sentidos
|
|
* opostos: primeiro afirmou no presente que `imagens-ok` era obrigatório
|
|
* quando não era; depois, corrigido para "ainda não está", continuou dizendo
|
|
* isso depois da ativação — que aconteceu no mesmo dia. Ninguém volta para
|
|
* trocar um "ainda não" por um "já": nota de pendência é dívida com data de
|
|
* vencimento e sem cobrador.
|
|
*
|
|
* Escopo deliberado: só os documentos de AUTORIDADE — aqueles que alguém lê para
|
|
* decidir. `docs/stories/`, `docs/handoffs/` e `docs/specs/` ficam de fora de
|
|
* propósito: são planejamento e histórico, cheios de paths que descrevem o que
|
|
* seria construído, e incluí-los faria o gate nascer com 384 violações e ser
|
|
* desligado na primeira semana. Dívida congelada, defeito novo reprovado.
|
|
*/
|
|
|
|
const RAIZ = path.resolve(__dirname, "../..");
|
|
|
|
/** Documentos que alguém lê para DECIDIR. Ampliar esta lista é sempre bem-vindo. */
|
|
const AUTORIDADE = [
|
|
"CLAUDE.md",
|
|
"AGENTS.md",
|
|
"CONTRIBUTING.md",
|
|
"ARCHITECTURE.md",
|
|
"README.md",
|
|
"SECURITY.md",
|
|
"docs/index.md",
|
|
"docs/current-state.md",
|
|
"docs/harness-audit.md",
|
|
"docs/threat-model.md",
|
|
"docs/DEPLOY-CHECKLIST.md",
|
|
"docs/SETUP.md",
|
|
"docs/doctrine",
|
|
"docs/runbooks",
|
|
"docs/adr",
|
|
"triagem",
|
|
// Skills embutidas: alguém as lê para AGIR (instalar, contribuir). A fonte é
|
|
// `.agents/skills`; o espelho `.claude/skills` é byte-idêntico (gate próprio).
|
|
".agents/skills",
|
|
];
|
|
|
|
/**
|
|
* Um único ponteiro morto conhecido, e ele é HONESTO: o runbook cita o script e,
|
|
* na mesma frase, declara que ele ainda não existe. Congelado com justificativa
|
|
* em vez de silenciado — se um dia o script nascer, esta entrada sai.
|
|
*/
|
|
const PONTEIROS_MORTOS_ACEITOS = new Set([
|
|
"docs/runbooks/ai-credentials-rotation.md::scripts/rotate-ai-cred-aes-key.ts",
|
|
]);
|
|
|
|
/**
|
|
* Nomes ilustrativos: `lib/foo/bar.ts` não é ponteiro, é exemplo.
|
|
*
|
|
* O segmento tem que terminar ali — `(?=\/|\.|$)`. Sem essa âncora, a alternativa
|
|
* `bar` casava o começo de `barra-do-navegador`, e `lib/branding/barra-do-navegador.ts`
|
|
* (arquivo REAL, existe hoje) ficava invisível ao gate: se alguém o renomeasse, o
|
|
* documento continuaria apontando para o vazio com o gate verde. Uma isenção
|
|
* larga demais é pior que isenção nenhuma, porque parece cobertura.
|
|
*
|
|
* `...`, `<` e `NNNN` seguem sem âncora de propósito: são marcas de reticência e de
|
|
* placeholder de template (`<timestamp>`, `NNNN_slug.sql`), não nomes de segmento.
|
|
*/
|
|
const EH_PLACEHOLDER =
|
|
/(^|\/)(?:(?:foo|bar|baz|exemplo|caminho|nome-da-nota)(?=\/|\.|$)|NNNN|\.\.\.|<)/;
|
|
|
|
const LINK_RELATIVO = /\[[^\]]*\]\((?!https?:|#|mailto:)([^)#\s]+)/g;
|
|
const PATH_EM_CRASE =
|
|
/`((?:app|lib|components|workers|scripts|tests|supabase|hooks|docs|hostgator-setup-kit|triagem|\.github|\.agents|\.claude|\.codex|\.cursor|\.opencode)\/[A-Za-z0-9_./[\]-]+\.(?:ts|tsx|sql|sh|yml|yaml|json|md|mdc|toml|mjs))`/g;
|
|
|
|
function decodificar(alvo: string): string {
|
|
try {
|
|
return decodeURIComponent(alvo);
|
|
} catch {
|
|
return alvo; // `%` solto não é codificação; o alvo vale como está
|
|
}
|
|
}
|
|
|
|
function markdownsRastreados(): string[] {
|
|
const saida = execFileSync("git", ["ls-files", "*.md"], { cwd: RAIZ, encoding: "utf8" });
|
|
return saida.split("\n").filter(Boolean);
|
|
}
|
|
|
|
function ehAutoridade(arquivo: string): boolean {
|
|
return AUTORIDADE.some((a) => arquivo === a || arquivo.startsWith(`${a}/`));
|
|
}
|
|
|
|
describe("documentação — o que ela aponta existe", () => {
|
|
const docs = markdownsRastreados().filter(ehAutoridade);
|
|
|
|
it("a lista de documentos de autoridade não está vazia", () => {
|
|
// Sem esta asserção o teste passaria com um glob quebrado, varrendo zero
|
|
// arquivos e devolvendo verde — o modo de falha mais barato de todos.
|
|
expect(docs.length, "nenhum documento de autoridade encontrado").toBeGreaterThan(20);
|
|
expect(docs, "CLAUDE.md saiu do escopo").toContain("CLAUDE.md");
|
|
expect(docs.some((d) => d.startsWith("docs/doctrine/"))).toBe(true);
|
|
});
|
|
|
|
it("nenhum link relativo aponta para arquivo que não existe", () => {
|
|
const mortos: string[] = [];
|
|
|
|
for (const doc of docs) {
|
|
const base = path.dirname(path.join(RAIZ, doc));
|
|
const texto = fs.readFileSync(path.join(RAIZ, doc), "utf8");
|
|
|
|
for (const [, alvo] of texto.matchAll(LINK_RELATIVO)) {
|
|
if (!alvo || EH_PLACEHOLDER.test(alvo)) continue;
|
|
// Markdown permite `%20`/`%C3%A3` no alvo (é assim que se linka um nome com
|
|
// espaço ou acento sem os `<>`); o disco não conhece a forma codificada.
|
|
// Sem decodificar, um link VÁLIDO para arquivo existente reprovava como morto.
|
|
if (fs.existsSync(path.resolve(base, decodificar(alvo)))) continue;
|
|
if (PONTEIROS_MORTOS_ACEITOS.has(`${doc}::${alvo}`)) continue;
|
|
mortos.push(`${doc} → ${alvo}`);
|
|
}
|
|
}
|
|
|
|
expect(mortos, `link(s) para arquivo inexistente:\n${mortos.join("\n")}`).toEqual([]);
|
|
});
|
|
|
|
it("nenhum path citado em crase aponta para arquivo que não existe", () => {
|
|
const mortos: string[] = [];
|
|
|
|
for (const doc of docs) {
|
|
const texto = fs.readFileSync(path.join(RAIZ, doc), "utf8");
|
|
|
|
for (const [, alvo] of texto.matchAll(PATH_EM_CRASE)) {
|
|
if (!alvo || EH_PLACEHOLDER.test(alvo)) continue;
|
|
if (fs.existsSync(path.join(RAIZ, alvo))) continue;
|
|
if (PONTEIROS_MORTOS_ACEITOS.has(`${doc}::${alvo}`)) continue;
|
|
mortos.push(`${doc} → ${alvo}`);
|
|
}
|
|
}
|
|
|
|
expect(mortos, `path(s) citados que não existem:\n${mortos.join("\n")}`).toEqual([]);
|
|
});
|
|
|
|
it("nenhuma nota de pendência sobre gates já ativados sobreviveu", () => {
|
|
// Frases LITERAIS que este repo já carregou depois de a pendência ser
|
|
// resolvida. Não é lista de palavras proibidas: é o registro de uma dívida
|
|
// que apodreceu — cada entrada aqui foi um texto real que enganou alguém.
|
|
const FRASES_MORTAS = [
|
|
"assim que o check entrar na branch protection",
|
|
"imagem quebrada ainda não bloqueia merge",
|
|
"`imagens-ok` ainda não está",
|
|
"imagens-ok` ainda não é obrigatório",
|
|
];
|
|
|
|
const violacoes: string[] = [];
|
|
|
|
/**
|
|
* A isenção olha o que vem ANTES da frase, não a linha inteira.
|
|
*
|
|
* A primeira versão filtrava a linha toda com `!l.includes("dizia")`, e isso
|
|
* abria um buraco medido: `Nota: o \`imagens-ok\` ainda não é obrigatório, ao
|
|
* contrário do que a doc dizia.` passava verde — afirmação FALSA e EM VIGOR,
|
|
* isenta porque a palavra aparecia depois dela. Em prosa pt-br, "dizia" é
|
|
* palavra comum; vetar a linha por contê-la é vetar quase qualquer parágrafo.
|
|
*
|
|
* A distinção que sobrevive é posicional: quem CITA o erro passado põe a marca
|
|
* antes ("o arquivo dizia \`…\`"); quem AFIRMA põe a frase primeiro. 60
|
|
* caracteres cobrem a introdução típica sem alcançar a oração seguinte.
|
|
*/
|
|
const CITA = /(já disse|dizia|dizendo|afirmava|carregou|carregava|["“«])[^"“«]{0,60}$/;
|
|
|
|
for (const doc of docs) {
|
|
const linhas = fs.readFileSync(path.join(RAIZ, doc), "utf8").split("\n");
|
|
|
|
for (const linha of linhas) {
|
|
if (linha.trimStart().startsWith(">")) continue; // bloco de nota
|
|
for (const frase of FRASES_MORTAS) {
|
|
const i = linha.indexOf(frase);
|
|
if (i === -1) continue;
|
|
if (CITA.test(linha.slice(0, i))) continue;
|
|
violacoes.push(`${doc}: "${frase}"`);
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(
|
|
violacoes,
|
|
`nota de pendência sobrevivente — o gate citado JÁ está ativo:\n${violacoes.join("\n")}\n` +
|
|
`confira a régua real: gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts'`,
|
|
).toEqual([]);
|
|
});
|
|
});
|