mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
O dano nao e hipotetico e esta escrito no commit ac9472c5, por quem o descobriu
por acaso: a 1.4.1 existia para consertar dois avisos errados da 1.4.0 — um deles
mandava o operador APAGAR A CONEXAO que estava funcionando —, e como a tela
mostrava so a secao da versao-alvo, quem pulasse da 1.4.0 para a 1.5.0 nunca os
leria. O contorno da epoca foi carregar os avisos orfaos para a versao seguinte,
a mao. Isto aposenta esse contorno.
Medido no CHANGELOG real, quem esta na 1.4.0 indo para a 1.6.0:
hoje -> secoes ["1.6.0"] avisos []
agora -> secoes ["1.6.0","1.5.0","1.4.1"] avisos ["1.5.0","1.4.1"]
Dois avisos de acao manual que hoje simplesmente somem.
**A selecao e POSICIONAL, nao semver.** O arquivo ja vem do mais novo para o mais
antigo, e comparar numero exigiria um comparador que nao existe no projeto — e que
tropecaria em `v1.1.1-jmpo.1`, tag de fork que este repo carrega.
**Os quatro casos sao escritos um a um de proposito.** Um `slice(iAlvo, iInst)`
ingenuo devolve lista VAZIA quando a instalada e mais nova que a alvo, e ainda
assim diria `completa: true`: a tela ficaria sem corpo nenhum afirmando estar
inteira. Ha caso de teste para cada ramo.
**O limite inferior e `running`, nunca `current_version`.** Depois de um rollback,
`current_version` nomeia a versao que QUEBROU (o `git checkout` deu certo; quem
nao subiu foi o container) — a faixa sairia vazia justamente para quem mais
precisa le-la. Coberto por caso proprio na rota.
**O sinal de honestidade e estrutural, e e um so:** achei o cabecalho da versao
instalada no texto recebido? O agente manda o CHANGELOG cortado em bytes, e um
corpo truncado no meio da frase e indistinguivel de um corpo inteiro para quem le.
Sem `complete`, a tela afirmaria completude que nao tem.
**O agente passa a mandar MENOS e melhor, em vez de mais.** Subir o `head -c`
mataria o heartbeat inteiro: o teto do Zod e 64000 sobre a string JA ESCAPADA, e o
422 morre calado, virando "agente offline" 24h depois. Entao o `awk` para de
imprimir AO IMPRIMIR o cabecalho da versao instalada — e o cabecalho entra de
proposito, e ele que prova a completude. Medido: quem esta na 1.4.1 indo para a
1.6.0 passa de 30.000 para 18.032 bytes, terminando exatamente em
`## [1.4.1] — 2026-08-25`.
E o app funciona com o agente VELHO, o que nao e detalhe: o `agent.sh` novo so
chega na VPS no update seguinte. Medido sobre o corte cego de 30.000 bytes:
instalada 1.4.1 -> 2 secoes completa=true; instalada 1.4.0 -> 3 secoes
completa=true.
**Nenhum aviso entra em `<details>`.** Esconder o que exige acao e o defeito que
esta tela existe para consertar; so o CORPO das versoes intermediarias e recolhido.
O e2e prova isso pela tela, com boundingBox (os dois avisos ANTES do botao) e
abrindo o `<details>` com clique — texto dentro de um fechado nao e visivel para o
Playwright, entao afirmar `toBeVisible()` sem abrir seria um teste que passa por
motivo errado.
Este commit traz o proprio fragmento em `.changes/`, e o gate pegou a primeira
versao dele: um `**negrito**` que atravessava a quebra de linha, que chegaria a
tela com os asteriscos a mostra. `pnpm release:conferir` -> 1.6.0 + minor = 1.7.0.
Evidencia: `pnpm typecheck` zerado; `pnpm lint` 0 errors; `pnpm vitest run
--exclude 'lib/ai/dispatcher/rate-limit.test.ts'` -> 498 arquivos, 5626 testes,
verde. A exclusao e defeito de AMBIENTE pre-existente (medido: na main pura da 5
failed COM `.env.local` no disco e 5 passed SEM ele), nao desta branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAtZo5GWZn8ckdmsoUVKts
278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "fs";
|
|
import { join } from "path";
|
|
|
|
import { extractChangelogRange, extractChangelogSection, markdownParaTextoSimples } from "./changelog";
|
|
|
|
const CHANGELOG = `# Changelog
|
|
|
|
Todas as mudanças relevantes deste projeto são documentadas aqui.
|
|
|
|
## [Não lançado]
|
|
|
|
Sem novidades pendentes de lançamento.
|
|
|
|
## [1.1.0] — 2026-08-02
|
|
|
|
**⚠️ Requer atenção**
|
|
|
|
Se você usa número próprio no WhatsApp, reconecte depois de atualizar.
|
|
|
|
### Adicionado
|
|
|
|
- Botão de atualizar pela própria tela.
|
|
|
|
## [1.0.0] — 2026-07-27
|
|
|
|
Primeira versão marcada do DeskcommCRM.
|
|
|
|
[Não lançado]: https://github.com/melgarafael/DeskcommCRM/compare/v1.0.0...HEAD
|
|
[1.0.0]: https://github.com/melgarafael/DeskcommCRM/releases/tag/v1.0.0
|
|
`;
|
|
|
|
describe("extractChangelogSection", () => {
|
|
it("acha a versão pedida e devolve só o corpo dela", () => {
|
|
const section = extractChangelogSection(CHANGELOG, "1.1.0");
|
|
expect(section?.version).toBe("1.1.0");
|
|
expect(section?.body).toContain("Botão de atualizar pela própria tela.");
|
|
expect(section?.body).not.toContain("Primeira versão marcada");
|
|
expect(section?.body).not.toContain("## [1.1.0]");
|
|
});
|
|
|
|
it("aceita a versão com o prefixo v da tag", () => {
|
|
expect(extractChangelogSection(CHANGELOG, "v1.1.0")?.version).toBe("1.1.0");
|
|
});
|
|
|
|
it("extrai o bloco de atenção separado do corpo (formato negrito)", () => {
|
|
const section = extractChangelogSection(CHANGELOG, "1.1.0");
|
|
expect(section?.requiresAttention).toContain("reconecte depois de atualizar");
|
|
});
|
|
|
|
it("extrai bloco de atenção em heading (### ⚠️ Requer atenção)", () => {
|
|
const raw = `## [1.0.0] — 2026-07-27
|
|
### ⚠️ Requer atenção
|
|
Seu sistema precisa de ação.
|
|
### Adicionado
|
|
- Algo novo`;
|
|
const section = extractChangelogSection(raw, "1.0.0");
|
|
expect(section?.requiresAttention).toContain("Seu sistema precisa de ação");
|
|
});
|
|
|
|
it("devolve null no bloco de atenção quando a versão não tem um", () => {
|
|
const raw = `## [2.0.0] — 2026-09-01
|
|
### Adicionado
|
|
- Coisa`;
|
|
expect(extractChangelogSection(raw, "2.0.0")?.requiresAttention).toBeNull();
|
|
});
|
|
|
|
it("para no próximo heading quando existe: testa com Não lançado que tem 1.1.0 seguinte", () => {
|
|
// O teste placebo original testava 1.0.0 (última seção, sem próxima).
|
|
// Agora testamos "Não lançado" que tem "1.1.0" como próxima seção.
|
|
// A sabotagem (end = lines.length) faria incluir "Botão de atualizar".
|
|
const section = extractChangelogSection(CHANGELOG, "Não lançado");
|
|
expect(section?.body).toContain("Sem novidades pendentes");
|
|
expect(section?.body).not.toContain("Botão de atualizar pela própria tela.");
|
|
});
|
|
|
|
it("remove referências de link do final (formato [algo]: http://...)", () => {
|
|
// 1.0.0 é a última versão, então body inclui [1.0.0]: ... antes da limpeza.
|
|
const section = extractChangelogSection(CHANGELOG, "1.0.0");
|
|
expect(section?.body).not.toContain("[Não lançado]:");
|
|
expect(section?.body).not.toContain("github.com");
|
|
expect(section?.body).toContain("Primeira versão marcada");
|
|
});
|
|
|
|
it("devolve null para versão ausente", () => {
|
|
expect(extractChangelogSection(CHANGELOG, "9.9.9")).toBeNull();
|
|
});
|
|
|
|
it("devolve null para entrada vazia ou lixo", () => {
|
|
expect(extractChangelogSection("", "1.0.0")).toBeNull();
|
|
expect(extractChangelogSection("qualquer coisa sem headings", "1.0.0")).toBeNull();
|
|
});
|
|
|
|
it("não confunde 1.1.0 com 1.1.0-beta", () => {
|
|
const raw = "## [1.1.0-beta] — 2026-08-01\n\nbeta\n\n## [1.1.0] — 2026-08-02\n\nfinal\n";
|
|
expect(extractChangelogSection(raw, "1.1.0")?.body.trim()).toBe("final");
|
|
});
|
|
|
|
it("não corta bloco de atenção em negrito no meio (ex: **Atenção especial**: ...)", () => {
|
|
// IMPORTANT 3: negrito no meio não termina o bloco, só heading de verdade.
|
|
const raw = `## [1.0.0] — 2026-07-27
|
|
**⚠️ Requer atenção**
|
|
Leia isto: **Atenção especial** antes de atualizar.
|
|
Mais informação aqui.
|
|
## [2.0.0]
|
|
Novo`;
|
|
const section = extractChangelogSection(raw, "1.0.0");
|
|
expect(section?.requiresAttention).toContain("Atenção especial");
|
|
expect(section?.requiresAttention).toContain("Mais informação");
|
|
});
|
|
|
|
it("lê CHANGELOG.md real e encontra 1.0.0 com requiresAttention", () => {
|
|
// Sem try/catch: se falhar, é informação crítica, não ruído.
|
|
// Usa __dirname do teste, não process.cwd() que é frágil.
|
|
const realChangelog = readFileSync(join(__dirname, "../../CHANGELOG.md"), "utf8");
|
|
const section = extractChangelogSection(realChangelog, "1.0.0");
|
|
expect(section).not.toBeNull();
|
|
expect(section?.version).toBe("1.0.0");
|
|
// No CHANGELOG real, linha 76 tem "### ⚠️ Requer atenção" com conteúdo
|
|
expect(section?.requiresAttention).not.toBeNull();
|
|
});
|
|
|
|
it("body NÃO contém o texto do bloco de atenção — não pode duplicar na tela", () => {
|
|
// 1.1.0 no fixture tem o bloco em negrito (**⚠️ Requer atenção**).
|
|
const section = extractChangelogSection(CHANGELOG, "1.1.0");
|
|
expect(section?.requiresAttention).toContain("reconecte depois de atualizar");
|
|
expect(section?.body).not.toContain("reconecte depois de atualizar");
|
|
expect(section?.body).not.toContain("Requer atenção");
|
|
});
|
|
|
|
it("body NÃO contém o bloco de atenção do CHANGELOG.md real (heading)", () => {
|
|
const realChangelog = readFileSync(join(__dirname, "../../CHANGELOG.md"), "utf8");
|
|
const section = extractChangelogSection(realChangelog, "1.0.0");
|
|
expect(section?.requiresAttention).toContain("Node 22");
|
|
expect(section?.body).not.toContain("Node 22 é obrigatório");
|
|
expect(section?.body).not.toContain("Requer atenção");
|
|
});
|
|
|
|
it("remove referencias de link do bloco de atencao (versao ultima com bloco)", () => {
|
|
// Versão que é a última do arquivo E tem bloco de atenção:
|
|
// o defeito vaza referências de link em requiresAttention.
|
|
const raw = `## [1.0.0] — 2026-07-27
|
|
### ⚠️ Requer atenção
|
|
Node 22 é obrigatório.
|
|
[1.0.0]: https://github.com/example/releases/tag/v1.0.0`;
|
|
const section = extractChangelogSection(raw, "1.0.0");
|
|
expect(section?.requiresAttention).not.toContain("github.com");
|
|
expect(section?.requiresAttention).toContain("Node 22");
|
|
});
|
|
});
|
|
|
|
describe("markdownParaTextoSimples", () => {
|
|
// Contra o CHANGELOG.md REAL, não um fixture inventado — o revisor apontou
|
|
// com razão que um fixture escrito por quem implementa evita justo os
|
|
// casos reais (negrito, crase, lista) que o changelog de verdade usa.
|
|
const realChangelog = readFileSync(join(__dirname, "../../CHANGELOG.md"), "utf8");
|
|
|
|
it("limpa negrito e crase do bloco de atenção real, sem sumir com o conteúdo", () => {
|
|
const section = extractChangelogSection(realChangelog, "1.0.0");
|
|
const limpo = markdownParaTextoSimples(section!.requiresAttention!);
|
|
expect(limpo).not.toMatch(/[*`#]/);
|
|
expect(limpo).toContain("Node 22 é obrigatório para desenvolvimento.");
|
|
expect(limpo).toContain("WebSocket global");
|
|
expect(limpo).toMatch(/^•\s/);
|
|
});
|
|
|
|
it("limpa heading e crase do corpo real (várias seções, várias listas)", () => {
|
|
const section = extractChangelogSection(realChangelog, "1.0.0");
|
|
const limpo = markdownParaTextoSimples(section!.body);
|
|
expect(limpo).not.toMatch(/[*`#]/);
|
|
expect(limpo).toContain("Plataforma");
|
|
expect(limpo).toContain("fn_user_org_ids()");
|
|
expect(limpo).toMatch(/•\s+Multi-tenancy/);
|
|
});
|
|
|
|
it("não mexe em texto sem marcação nenhuma", () => {
|
|
expect(markdownParaTextoSimples("texto normal, sem markdown.")).toBe(
|
|
"texto normal, sem markdown.",
|
|
);
|
|
});
|
|
|
|
it("não confunde underscore de identificador com itálico (fn_user_org_ids)", () => {
|
|
// Achado rodando contra o CHANGELOG real: a primeira versão comia os
|
|
// "_" internos de nomes de função, virando "fnuserorg_ids()".
|
|
expect(markdownParaTextoSimples("resolvida por `fn_user_org_ids()`.")).toBe(
|
|
"resolvida por fn_user_org_ids().",
|
|
);
|
|
});
|
|
|
|
it("ainda converte itálico de underscore de verdade (delimitado por espaço)", () => {
|
|
expect(markdownParaTextoSimples("isso é _importante_ para todos")).toBe(
|
|
"isso é importante para todos",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("extractChangelogRange — todas as seções entre a instalada e a alvo", () => {
|
|
const TRES = [
|
|
"# Changelog",
|
|
"",
|
|
"## [Não lançado]",
|
|
"",
|
|
"## [1.2.0] — 2026-08-03",
|
|
"",
|
|
"### Adicionado",
|
|
"",
|
|
"- coisa nova",
|
|
"",
|
|
"## [1.1.0] — 2026-08-02",
|
|
"",
|
|
"### ⚠️ Requer atenção",
|
|
"",
|
|
"- reconecte o número depois de atualizar",
|
|
"",
|
|
"### Corrigido",
|
|
"",
|
|
"- conserto do meio",
|
|
"",
|
|
"## [1.0.0] — 2026-08-01",
|
|
"",
|
|
"- primeira",
|
|
"",
|
|
].join("\n");
|
|
|
|
it("devolve da mais nova para a mais antiga, sem incluir a instalada", () => {
|
|
const f = extractChangelogRange(TRES, "1.2.0", "1.0.0");
|
|
expect(f.secoes.map((s) => s.version)).toEqual(["1.2.0", "1.1.0"]);
|
|
expect(f.completa).toBe(true);
|
|
});
|
|
|
|
it("o aviso da versão do MEIO sobrevive — é o defeito que esta função existe para corrigir", () => {
|
|
// Quem pulava versões via só a seção-alvo, e o aviso de ação manual da
|
|
// versão intermediária desaparecia (commit ac9472c5).
|
|
const f = extractChangelogRange(TRES, "1.2.0", "1.0.0");
|
|
const comAviso = f.secoes.filter((s) => s.requiresAttention);
|
|
expect(comAviso.map((s) => s.version)).toEqual(["1.1.0"]);
|
|
expect(comAviso[0]?.requiresAttention).toContain("reconecte o número");
|
|
});
|
|
|
|
it("não afirma completude quando a instalada é um SHA (instalação fora de release)", () => {
|
|
const f = extractChangelogRange(TRES, "1.2.0", "abc1234");
|
|
expect(f.completa).toBe(false);
|
|
expect(f.secoes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("não afirma completude quando o texto chegou cortado antes da instalada", () => {
|
|
const cortado = TRES.slice(0, TRES.indexOf("## [1.0.0]"));
|
|
expect(extractChangelogRange(cortado, "1.2.0", "1.0.0").completa).toBe(false);
|
|
});
|
|
|
|
it("em dia: nada entre as duas, e isso NÃO é incompletude", () => {
|
|
expect(extractChangelogRange(TRES, "1.2.0", "1.2.0")).toEqual({ secoes: [], completa: true });
|
|
});
|
|
|
|
it("instalada mais nova que a alvo devolve a seção-alvo, nunca uma lista vazia dizendo-se completa", () => {
|
|
// Um `slice(iAlvo, iInst)` ingênuo devolveria [] com completa=true aqui: a
|
|
// tela ficaria sem corpo nenhum afirmando estar inteira.
|
|
const f = extractChangelogRange(TRES, "1.0.0", "1.2.0");
|
|
expect(f.secoes.map((s) => s.version)).toEqual(["1.0.0"]);
|
|
expect(f.secoes[0]?.body).toContain("primeira");
|
|
});
|
|
|
|
it("alvo que não existe no texto devolve vazio e incompleto", () => {
|
|
expect(extractChangelogRange(TRES, "9.9.9", "1.0.0")).toEqual({ secoes: [], completa: false });
|
|
});
|
|
|
|
it("faixa de uma versão só é igual à seção única — as duas leituras não podem divergir", () => {
|
|
const pelaFaixa = extractChangelogRange(TRES, "1.2.0", "1.1.0").secoes[0];
|
|
const pelaSecao = extractChangelogSection(TRES, "1.2.0");
|
|
expect(pelaFaixa).toEqual(pelaSecao);
|
|
});
|
|
|
|
it("`[Não lançado]` nunca entra na faixa", () => {
|
|
const f = extractChangelogRange(TRES, "1.2.0", "1.0.0");
|
|
expect(f.secoes.map((s) => s.version)).not.toContain("Não lançado");
|
|
});
|
|
});
|