Num clone limpo desta main, com o padrão do instalador git pra Windows,
`pnpm test:unit` dá 9 arquivos / 20 casos vermelhos e `pnpm lint:channels`
acusa 155 arquivos. Nenhum é defeito de produto: são os próprios gates lendo
o disco de um jeito que só funciona em Linux. Quem contribui do Windows abre
o repo já vermelho e não tem como saber o que é seu.
Três causas, medidas separadamente.
1. CRLF no checkout (5 arquivos, 12 casos)
Os blobs estão em LF, mas `core.autocrlf=true` — padrão do instalador git
pra Windows — entrega CRLF no disco, e os gates leem texto o tempo todo:
workflow YAML, mapa de jornadas, selo de tradução, lista de specs do e2e.
Um `\r` no fim da linha derruba regex ancorada.
Medido, mesma main, mesma máquina:
core.autocrlf=true -> 9 arquivos / 20 casos
core.autocrlf=false -> 5 arquivos / 8 casos
O `.gitattributes` já resolvia isto para `*.sql`, e o comentário de lá já
escreve o raciocínio inteiro — inclusive que "CI passa porque
ubuntu-latest não converte". Este commit estende a mesma regra ao resto.
`git add --renormalize .` não altera NENHUM arquivo: os blobs já são LF,
então muda só o que o checkout entrega.
2. Separador de caminho (3 arquivos)
`path.relative` devolve barra invertida no Windows, e as listas declaradas
contra as quais o resultado é comparado usam barra normal. Falha nas duas
pontas ao mesmo tempo: o arquivo justificado vira infrator, e a
justificativa vira órfã. Novo helper `tests/unit/helpers/caminho.ts`, num
lugar só — quatro cópias da mesma linha é a garantia de que uma diverge.
Mesma causa em `scripts/lint-channels.ts`, onde `ALLOWED` (regex ancorada
em `^lib/channels/`) e `DEBT` (Set de strings) nunca casavam: 155 acusados
numa árvore limpa, agora "ok (60 de dívida conhecida, nenhum novo)".
3. `npx` não é executável no Windows (1 arquivo)
`import-puro-sem-env` chamava `execFileSync("npx", ...)`, e no Windows o
`npx` é `npx.cmd`: `spawnSync npx ENOENT`. Agora chama `process.execPath`
com `node_modules/tsx/dist/cli.mjs`.
E o defeito grave que isso escondia: o CONTROLE POSITIVO passava com o
aparato quebrado. Ele espera `ok:false` para `@/lib/env`, e um filho que
nunca nasceu também dá `ok:false`. O controle ficava verde ao lado do caso
real vermelho, afirmando que o instrumento estava bom. `respondeu` separa
os dois: só conta como resposta uma saída com "OK" ou "ERRO:".
Sem fragmento em `.changes/`: nada aqui muda comportamento visível a quem
opera uma VPS.
`zernioBaseUrl()` resolvia com `??`, que só cai no padrão em null/undefined —
string vazia é valor e passa. O `.env.example` entrega ZERNIO_API_BASE_URL
VAZIA e promete, no comentário ao lado dela: "Vazio usa a produção do
provedor". O código não cumpria a própria promessa.
Consequência, medida executando:
baseUrl resolvida: ""
URL montada : "/v1/inbox/conversations?account_id=x"
fetch LANÇOU : TypeError: Failed to parse URL
Atinge quem faz `cp .env.example .env`, preenche as duas credenciais para
conectar o canal e deixa este override como veio — o caminho normal, já que o
override existe só para apontar homologação. Todo envio pelo canal quebra, nos
DOIS caminhos de credencial (env e sessão cifrada), que chamam esta mesma
função.
Por que nenhum gate pegou: a doutrina de QA manda testar com os envs opcionais
AUSENTES, e ausente sempre funcionou — o `??` pega `undefined`. O estado que
quebra é presente-e-vazio, que é exatamente o que copiar o `.env.example`
produz. E a chave não aparece em `lib/env.ts`, então nada a valida no boot.
tests/unit/canal-zernio-base-url-vazia.test.ts cobre vazia, ausente,
só-espaço, valor real e valor com espaço em volta, mais uma asserção de que a
URL montada é absoluta. Sabotado de volta para `??`, 4 dos 6 casos reprovam;
os 2 que sobrevivem são ausente e valor real, que já funcionavam.