fix(admin): extensão removida deixa de aparecer como instalada

Remoção de extensão é soft delete: `extension_installations.removed_at` ganha
data e a linha fica. A tela de `/admin/extensoes` lia a tabela inteira, então
uma extensão já removida continuava listada como instalada, com a contagem de
empresas dela junto.

A irmã de produção (`lib/extensions/service.ts`) filtra no banco; esta passa a
filtrar igual. É a única leitora de `extension_installations` fora do service.

O filtro vira cerca: o caso novo em
`tests/unit/extensoes-da-instalacao-conta-sem-nomear.test.ts` recorta a consulta
das instaladas e exige o `removed_at`. Sabotado nesta branch: sem a linha,
`Tests 1 failed | 2 passed`; com ela, `Tests 3 passed`.

Resgatado de 0e2abb08e (wt-1380), que ficou fora do merge do #1380.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9fyW7pXJCsyDPtu5PoQpN
This commit is contained in:
melgarafael
2026-09-23 16:12:35 -03:00
co-authored by Claude Opus 5.5
parent 5b3186aa36
commit db5dfc57fd
3 changed files with 26 additions and 0 deletions
@@ -0,0 +1,7 @@
---
impacto: nada_mudou
secao: corrigido
titulo: Extensão removida deixa de aparecer como instalada no painel do dono do servidor
---
Remover uma extensão marca a instalação como removida, mas a linha continua no banco. A tela `/admin/extensoes` lia todas as linhas, então uma extensão já removida seguia listada como instalada, junto com a contagem de empresas dela. Agora a tela ignora as instalações removidas, do mesmo jeito que o restante do sistema de extensões já fazia.
+5
View File
@@ -52,9 +52,14 @@ export default async function Page() {
const db = createAdminClient();
const [catalogos, instalacoes, vinculos] = await Promise.all([
db.from("extension_catalogs").select("id,origin,revision,digest,admitted_at").order("admitted_at", { ascending: false }),
// Remoção aqui é soft delete: a linha fica, com `removed_at` preenchido. Sem
// este filtro a tela chamaria de "instalada" uma extensão que o operador já
// removeu — e contaria os vínculos dela junto. A irmã de produção
// (`lib/extensions/service.ts`) filtra no banco pelo mesmo motivo.
db
.from("extension_installations")
.select("id,publisher,name,version,installed_at")
.is("removed_at", null)
.order("installed_at", { ascending: false }),
db.from("organization_extensions").select("installation_id,enabled"),
]);
@@ -39,6 +39,20 @@ describe("a tela de extensões da instalação", () => {
).toEqual(["installation_id", "enabled"]);
});
it("não chama de instalada a extensão que o operador já removeu", () => {
// Remoção é soft delete (`extension_installations.removed_at`), então a linha
// continua lá. Uma tela que não filtra mostra a removida como instalada e conta
// os vínculos dela — e o erro é invisível justamente para quem removeu.
const inicio = semProsa.indexOf('from("extension_installations")');
expect(inicio, `nenhuma leitura de extension_installations em ${TELA}`).toBeGreaterThan(-1);
const proxima = semProsa.indexOf('.from("', inicio + 1);
const consulta = semProsa.slice(inicio, proxima === -1 ? undefined : proxima);
expect(
consulta,
"a consulta das instaladas não filtra `removed_at`. Ver o cabeçalho deste caso.",
).toContain('.is("removed_at", null)');
});
it("a varredura ENCONTRA o select — um regex quebrado passaria por vacuidade", () => {
// Sem este caso, renomear a tabela ou trocar as aspas deixaria o caso acima
// verde sobre um conjunto vazio, que é o modo silencioso de uma cerca morrer.