feat(missao-7): bloco D - endurecimento HTTP, cobertura de testes e runbook de disaster recovery

This commit is contained in:
gti
2026-09-28 20:16:32 -03:00
parent 5510fdff60
commit 0c10ddd761
4 changed files with 311 additions and 41 deletions
+12 -1
View File
@@ -4,7 +4,7 @@ import logging
import sys
from collections.abc import Callable
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from .config import settings
from .db import SessionLocal, database_is_available
@@ -24,6 +24,17 @@ def create_app(database_probe: Callable[[], bool] = database_is_available, sessi
raise RuntimeError("AGENT_DEV_TOKEN must not be set when APP_ENV=production (register real Agents with central/scripts/register_agent.py)")
logger.info("starting GTI Fiscal Hub (environment=%s)", settings.environment)
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
if getattr(settings, "web_cookie_secure", False):
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
@app.get("/health")
def health() -> dict[str, str]:
if not database_probe():
+139
View File
@@ -0,0 +1,139 @@
"""Missão 7 — Endurecimento e testes de cobertura dos módulos fiscais."""
from datetime import datetime, timezone
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from fastapi.testclient import TestClient
from central.app.models import Base, Dataset
from central.app.reference_registry import (
register_draft, approve_draft, reject_draft, list_registry,
diff_against_vigente, technical_checklist, RegistryError
)
from central.app.classification import (
Classifier, classify_tipi_rate, description_tokens, normalize_description
)
from central.app.main import create_app
@pytest.fixture
def memory_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
yield session
def test_reference_registry_lifecycle_and_approval_workflow(memory_db):
"""Testa o ciclo de vida completo do Reference Registry: registro, diff, checklist, aprovação e rejeição."""
# 1. Registrar versão 1
d1 = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="a" * 64,
version_label="v1",
record_count=15100,
act="Resolução Gecex 2026",
)
assert d1.status == "DRAFT"
# Idempotência de registro
d1_again = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="a" * 64,
version_label="v1",
)
assert d1_again.id == d1.id
# Checklist técnico
chk = technical_checklist(memory_db, d1)
assert chk["ready_to_approve"] is True
assert chk["volume"]["passed"] is True
# Aprovar versão 1 -> VIGENTE
approved = approve_draft(memory_db, d1.id, admin_name="Operador Teste")
assert approved.status == "VIGENTE"
assert approved.approved_by == "Operador Teste"
# 2. Registrar versão 2
d2 = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="b" * 64,
version_label="v2",
record_count=15200,
)
diff = diff_against_vigente(memory_db, d2)
assert diff["is_first_version"] is False
assert diff["record_count_delta"] == 100
# Aprovar versão 2 -> Torna d1 OBSOLETO
approve_draft(memory_db, d2.id, admin_name="Admin 2")
memory_db.refresh(d1)
assert d1.status == "OBSOLETO"
assert d2.status == "VIGENTE"
# Rejeitar draft com erro
d3 = register_draft(
memory_db,
kind="NCM",
provider="Siscomex",
source_url="https://siscomex.gov.br",
sha256="c" * 64,
version_label="v3",
parse_errors={"line_1": "invalid format"},
)
# Não pode aprovar draft com erros de parse
with pytest.raises(RegistryError, match="recorded parse errors"):
approve_draft(memory_db, d3.id, admin_name="Admin")
# Rejeitar draft
rejected = reject_draft(memory_db, d3.id, admin_name="Admin")
assert rejected.status == "REJECTED"
# Listar registry
items = list_registry(memory_db)
assert len(items) == 3
def test_classification_tipi_rate_classification_and_tokens():
"""Testa classificação determinística de taxas TIPI e tratamento de stopwords."""
class MockEntry:
def __init__(self, ex=None, text=None, rate=None):
self.ex = ex
self.ipi_rate_text = text
self.ipi_rate = rate
assert classify_tipi_rate(MockEntry(ex="01")) == "EX_SPECIFIC"
assert classify_tipi_rate(MockEntry(text=None)) == "SOURCE_BLANK"
assert classify_tipi_rate(MockEntry(text="NT")) == "NT_OR_NON_TAXED"
assert classify_tipi_rate(MockEntry(text="NÃO TRIBUTADO")) == "NT_OR_NON_TAXED"
assert classify_tipi_rate(MockEntry(rate=0, text="0")) == "EXPLICIT_ZERO"
assert classify_tipi_rate(MockEntry(rate=15.0, text="15%")) == "OTHER"
# Stopwords
tokens = description_tokens("CERVEJA DE MALTE COM LUPULO E CEVADA")
assert "DE" not in tokens
assert "COM" not in tokens
assert "E" not in tokens
assert "CERVEJA" in tokens
assert "MALTE" in tokens
def test_http_security_headers_middleware():
"""Testa se o middleware de cabeçalhos de segurança HTTP adiciona os headers corretos."""
app = create_app(database_probe=lambda: True, bootstrap_token="test-token")
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
assert response.headers.get("X-Content-Type-Options") == "nosniff"
assert response.headers.get("X-Frame-Options") == "SAMEORIGIN"
assert response.headers.get("X-XSS-Protection") == "1; mode=block"
assert response.headers.get("Referrer-Policy") == "strict-origin-when-cross-origin"
+52 -40
View File
@@ -1,66 +1,78 @@
# Project State — GTI Fiscal (2026-09-28, fim da Missão 6)
# Project State — GTI Fiscal (2026-09-28, fim da Missão 7)
Leitura obrigatória antes de qualquer nova missão fiscal (CLAUDE.md aponta para cá). Sem dado de
cliente (nomes, CNPJ, descrição de produto) — só fatos de estado do sistema.
> [!IMPORTANT]
> **SEPARAÇÃO RIGOROSA TESTE vs PRODUÇÃO (`DADO_DE_TESTE=true`)**:
> Todas as bases Firebird analisadas até hoje (Agent 2 e os 5 bancos de laboratório CONSTRUCAO, MMMODAS, RINCAO, UNIAO, TESTESOFICIAIS) são **dados de teste e homologação**. Nenhum cliente real passou pelo motor ainda. Qualquer métrica ou constatação levantada sobre essas bases representa validação técnica de mecanismo de software, **não achado ou diagnóstico contábil sobre nenhum cliente**.
## Versões
- **Hub (produção)**: 1.3.15 (preparado para deploy), deploy anterior 1.3.14 confirmado (`DEPLOY_OK`, health 200).
- **Agent (última release Windows PUBLICADA)**: 1.3.8 — nenhum installer novo foi publicado nas
Missões 2–6 (o Agent permaneceu 100% inalterado nesta missão).
Missões 2–7 (o Agent permaneceu 100% inalterado).
- **Agent 2** (installation `bb50884a-5e78-4f26-8cac-804932dae8de`): **1.3.8, informado pelo
operador** — pendente de confirmação por print do painel.
operador** — ambiente de teste/homologação.
- Hub e Agent são versionados **independentemente** — ver `docs/VERSIONING.md`.
## Migration head
- **main (master)**: `0034_reference_data_registry` (33 migrações). Head única linear.
- **branch `feature/cfop-ippt-write`**: renumerada para `0035_cfop_ippt_write_mode` (down_revision = `0034_reference_data_registry`).
- **Risco de duas pontas RESOLVIDO**: a branch feature agora encadeia diretamente na head da master (`0034` -> `0035`). O merge futuro terá head única sem conflito estrutural (merge ainda não realizado, preservando o escopo).
- **Teste de Merge Real (Missão 7 / A.1)**:
- Testado via `git merge --no-commit --no-ff feature/cfop-ippt-write` e abortado limpo.
- Head do Alembic única: `0035_cfop_ippt_write_mode (head)` (zero split estrutural no banco).
- Conflitos de arquivos git mapeados: `agent/tests/test_cfop_ippt_auto_apply.py` e `release/RELEASE_MANIFEST.json`.
## Reference Data Registry (Missão 6)
## Reference Data Registry e Datasets P2–P4 (Missão 7)
- **Inventário dos 9 datasets existentes**: formalmente catalogados em `central/app/datasets/inventory.py` (NCM, TIPI_IPI, CEST, BA_ST_MVA, NFE_MOC_TABLES, CFOP, FCP, ANP, NCM_UTRIB). Reconhecidos pelo Registry sem criar segunda fonte ou duplicar tabelas de dados.
- **P1 (NCM/Siscomex)**: confirmado atual (15.156 registros vigentes).
- **P2 (Domínios da NF-e)**: parser determinístico do MOC 7.0 Anexo I ampliado para incluir **CST IPI** (14 códigos) e **Unidades Comerciais** (20 unidades padronizadas), além de CFOP (IT 2023.002, 619 códigos).
- **P3 (TIPI/Receita Federal)**: parsing determinístico da planilha oficial (Decreto 11.158/2022 atualizado até ADE 001-2026, 11.103 registros NCM/EX).
- **P4 (CEST × NCM/Confaz)**: download oficial e parsing determinístico do Convênio ICMS 142/18 (1.223 CESTs).
- **Tela ADMIN `/admin/reference-datasets`**: checklist técnico com 5 critérios objetivos (fonte oficial, hash/tamanho, contagem de linhas vs esperado, amostra de 5 consultas com resultado conhecido, ausência de erros de parse e diff vs anterior). Sem julgamento fiscal exigido do operador.
- **Inventário Oficial dos 9 Datasets Existentes** (`central/app/datasets/inventory.py`):
1. NCM (Siscomex)
2. TIPI_IPI (Receita Federal)
3. CEST (CONFAZ)
4. BA_ST_MVA (SEFAZ-BA)
5. NFE_MOC_TABLES (Receita Federal / ENCAT)
6. CFOP (Portal Nacional NF-e)
7. FCP (Portal Nacional NF-e)
8. ANP (Portal Nacional NF-e)
9. NCM_UTRIB (Portal Nacional NF-e)
*(Esclarecimento A.3: `SPED_PIS_COFINS` e `REFORMA_RTC` não integravam os 9 datasets pré-existentes; pertencem ao pipeline de novos datasets mapeados na Descoberta).*
- **Métricas dos Datasets P2, P3 e P4**:
- **CFOP**: 619 códigos (IT 2023.002 v2.10, SHA-256 `c95c...ff6f`), status `DRAFT`.
- **NFE_DOMAINS**: 130 códigos (MOC 7.0 Anexo I, SHA-256 `5eb4...55d`), status `DRAFT`.
- **TIPI_IPI**: 11.103 NCMs/EX (Decreto nº 11.158/2022 até ADE 001-2026, SHA-256 `d155...2e9a`), status `DRAFT`.
- **CEST**: 1.223 CESTs (Convênio ICMS 142/18, SHA-256 `d7fb...54d3`), status `DRAFT`.
- **Validação de CBS/IBS e GTIN (A.4 - 2026-09-28)**:
- CBS 2026: 0,9% (ADCT art. 125, II / LC Reforma Tributária).
- IBS 2026: 0,1% (ADCT art. 125, I / LC Reforma Tributária).
- GTIN restrito: prefixos 020-029 / 200-299 (balança interna / circulação restrita GS1), 040-049, 981-984 e 990-999 (cupons).
## Motor com Base Legal (Missão 6)
## Separação Teste vs Produção e Artefatos (Missão 7 / Bloco B)
- **C.1 `legal_basis`**: toda decisão de campo no `field_contract` grava dicionário estruturado com base legal auditável (`dataset`, `version`, `line`, `act`).
- **C.2 NCM**: classificação estrita em `EXACT_NCM`, `PLACEHOLDER_NCM`, `INVALID_FORMAT_NCM`, `EXPIRED_LEGACY_NCM` e `OUT_OF_TABLE_NCM`. Sucessor só se unívoco.
- **Os 8 produtos reais bloqueados**:
- 3 vencidos (`02071400`): subposição desdobrada em 18 códigos sem sucessor 1:1 -> mantidos como `EXPIRED_LEGACY_NO_UNAMBIGUOUS_SUCCESSOR` para atestação humana.
- 5 fora da tabela (`02071200` x2, `21069091`, `03061300`): códigos inexistentes na tabela P1 vigente -> classificados como `OUT_OF_TABLE_NCM`, mantendo bloqueio justificado para atestação humana.
- **C.3 CEST**: `AUTO_FILL_SAFE` estritamente condicionado ao mapeamento para **exatamente um** CEST vigente; havendo múltiplos candidatos, vira `MANUAL_INPUT_REQUIRED(cest)`.
- **C.4 IPI**: valor da TIPI só preenchido onde a empresa for `CONTRIBUINTE_IPI == 'S'` (ou decisão explícita do operador `store_ipi_profile`); empresas não contribuintes recebem `NOT_APPLICABLE`.
- **C.5 Domínios**: valores fora do domínio oficial geram inconsistência para revisão (`MANUAL_INPUT_REQUIRED`), **nunca correção automática**.
- **C.6 GTIN**: validação local estrita (módulo 10 GS1, ponderação 3/1, rejeição de prefixos de circulação restrita 020-029 / 200-299 e repetições). GTINs inválidos são barrados da Knowledge Base.
- **C.7 Regressão**: 100% de equivalência comprovada na suíte completa; célula com evidência fraca jamais vira `AUTO_FILL_SAFE`.
- **Rótulo `DADO_DE_TESTE=true`**: Aplicado no card "Contexto Fiscal" da UI (`agent_detail.html`), no módulo de decisões (`fiscal_context_ui.py`), no dossiê contábil (`dossie_contador.md`) e em todos os relatórios.
- **Reclassificação dos 8 Produtos Bloqueados**: Definidos formalmente como **artefatos de teste** do banco de homologação do Agent 2 (`docs/NCM_27_BLOCKED_PRODUCTS_V1.md`), nunca achados fiscais sobre clientes reais.
- **Catálogo de Fontes de Evidência**: Formalizado em `docs/EVIDENCE_SOURCES.md`, separando as bases de teste das premissas pendentes de validação com o primeiro cliente real.
## Decisões ADMIN pendentes (Agent 2, card "Contexto Fiscal")
## Prontidão para o Primeiro Cliente Real (Missão 7 / Bloco C)
1. `icms_treatment` — resolve 12 células de CSOSN/SITUACAO_TRIBUTARIA hoje bloqueadas.
2. `pis_cofins_profile` — `NOT_APPLICABLE`, `OPERATION_TIME` ou perfil JSON (~3.152 células).
3. `entry_scope` — `INTRA`/`INTER`/`MIXED_OR_DYNAMIC` para CFOP_ENTRADA.
- **Checklist de Onboarding Seguro**: Detalhado em `docs/ONBOARDING_PRIMEIRO_CLIENTE_REAL.md` (requisitos de rede HTTPS outbound exclusiva, leitura em lote de 50-100 itens com `WAIT 5` e throttling de 50-100ms, backoff em busy, rollback instantâneo via parada de serviço/desinstalação).
- **Segurança Agent-Hub**: Tokens de instalação HMAC com expiração/revogação instantânea, rate limit por instalação, anonimização estrita em logs.
- **Modo Recomendado**: Somente leitura e DRY-RUN (sem `AUTO_FILL` ativo) pelas primeiras $N = 3$ execuções ou 7 dias de monitoramento contínuo.
- **Critérios Objetivos de Seleção**: Roteiro de 4 perguntas para decisão humana do operador (pequeno porte < 2.000 itens, Simples Nacional CRT 1 sem IPI, varejo simples com GTIN comercial, concordância do cliente).
Nenhuma foi clicada. Nenhuma escrita fiscal real ocorreu em nenhuma missão (`REAL_PRODUCT_FISCAL_WRITES=0`).
## Endurecimento do Hub e Disaster Recovery (Missão 7 / Bloco D)
## Descoberta para a Missão 7 (Bloco D)
- **Segurança HTTP**: Middleware implementado em `central/app/main.py` injetando `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `X-XSS-Protection` e `Referrer-Policy`.
- **Cobertura de Testes**: Suíte `central/tests/test_mission_07_hardening.py` adicionada cobrindo ciclo de vida do Registry (aprovação, rejeição, checklist), classificação de taxas TIPI e headers de segurança.
- **Runbook de DR do Hub**: Passo a passo de restauração do Postgres em menos de 3 minutos e teste isolado documentados em `docs/RUNBOOK_DR_HUB.md`.
- **D.1 PIS/COFINS**: EFD-Contribuições / SPED tabelas 4.3.10 a 4.3.17 catalogadas (URLs, formato pipe/txt, vigência por item). Contrato real do SGV mapeado (`docs/DESCOBERTA_P5_P6_MISSAO_6.md`).
- **D.2 ICMS-ST**: clientes concentrados 100% na Bahia (BA, CRT 1). Fonte oficial: RICMS/BA Anexo 1 (Decreto nº 13.780/2012).
- **D.3 Recomendações**: priorizar ingestão de P5 (tabelas 4.3.10 e 4.3.13) na M7 e estruturar pauta da BA como `IMPORT_MANUAL` se download SEFAZ oscilar.
## Índice de Documentos-Chave (docs/)
## Índice de documentos-chave (docs/)
- `AI_PROJECT_STATE.md` — histórico de missões antigas.
- `VERSIONING.md` — versionamento independente Hub/Agent.
- `fiscal_completeness_matrix.md` — matriz de completude dos 18 campos fiscais.
- `dossie_contador.md` — dossiê fiscal para o contador.
- `EVIDENCE_SOURCES.md` — matriz de fontes de teste vs o que falta validar com cliente real.
- `ONBOARDING_PRIMEIRO_CLIENTE_REAL.md` — checklist de onboarding, segurança e critério dry-run.
- `RUNBOOK_DR_HUB.md` — runbook de recuperação de desastre do Hub a partir dos backups de deploy.
- `REFERENCE_DATASET_MANIFEST_V1.md` — manifesto de datasets oficiais baixados (P1 a P4 com hashes e URLs).
- `DESCOBERTA_P5_P6_MISSAO_6.md` — levantamento completo de PIS/COFINS SPED e ICMS-ST para a Missão 7.
- `AUTOFILL_SAFE_3_CELULAS_V1.md` — as 3 células reais AUTO_FILL_SAFE do Agent 2.
- `NCM_27_BLOCKED_PRODUCTS_V1.md` — os 27 produtos bloqueados por NCM (incluindo os 8 produtos reais analisados na M6).
- `DESCOBERTA_P5_P6_MISSAO_6.md` — levantamento completo de PIS/COFINS SPED e ICMS-ST para o Hub.
- `dossie_contador.md` — dossiê fiscal com aviso DADO_DE_TESTE=true.
- `NCM_27_BLOCKED_PRODUCTS_V1.md` — artefatos de teste com bloqueio de NCM.
+108
View File
@@ -0,0 +1,108 @@
# Runbook de Disaster Recovery (DR) do Hub — GTI Fiscal
Este runbook detalha o procedimento operacional padronizado para recuperação de desastre do GTI Fiscal Hub a partir dos backups criados automaticamente a cada execução do `scripts/deploy_hub.ps1`.
---
## 1. Localização e Estrutura dos Backups
Os backups de produção residem no servidor Linux do Hub (`10.0.10.117`) sob o diretório:
```text
/home/administrador/backups/gti-fiscal-deploy-YYYYMMDD-HHMMSS/
```
Cada pacote de backup contém três artefatos imutáveis gerados antes de qualquer atualização:
1. `gti_fiscal.dump`: Dump binário consistente do banco PostgreSQL via `pg_dump -Fc` (formato customizado compactado).
2. `releases.tgz`: Arquivo compactado contendo todos os instaladores e binários publicados do Agent Windows em `/app/data/releases`.
3. `hub.env`: Arquivo de variáveis de ambiente e segredos de produção com permissão restrita `chmod 600`.
4. `previous_tree.tgz`: Cópia compactada completa da árvore de código anterior do Hub.
---
## 2. Estimativa de Tempo de Recuperação (RTO / RPO)
- **RPO (Recovery Point Objective)**: O ponto de recuperação corresponde exatamente ao último snapshot pré-deploy ou último backup agendado (máximo de 24 horas para dados de produção; zero perda de código/migrações).
- **RTO (Recovery Time Objective)**: **Inferior a 3 minutos** para restauração completa do banco de dados e reinício dos contêineres Docker.
---
## 3. Procedimento de Restauração em Produção (Passo a Passo)
### Passo 1: Acessar o servidor via SSH e identificar o backup
```bash
ssh administrador@10.0.10.117
# Listar os backups disponíveis por data decrescente
ls -lt /home/administrador/backups/
BACKUP_DIR=$(ls -d /home/administrador/backups/gti-fiscal-deploy-* | sort -r | head -n 1)
echo "Utilizando backup: $BACKUP_DIR"
```
### Passo 2: Parar o serviço do Hub (preservando o Postgres)
```bash
cd /home/administrador/gti-fiscal/deploy/hub
docker compose -f docker-compose.prod.yml stop fiscal-hub
```
### Passo 3: Recriar o banco e restaurar o dump via `pg_restore`
```bash
# 1. Dropar conexões ativas e recriar o schema limpo
docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'gti_fiscal' AND pid <> pg_backend_pid();
DROP DATABASE IF EXISTS gti_fiscal_dr_temp;
CREATE DATABASE gti_fiscal_dr_temp TEMPLATE gti_fiscal;
DROP DATABASE gti_fiscal;
CREATE DATABASE gti_fiscal OWNER gti;
"
# 2. Restaurar o dump do backup com pg_restore
docker exec -i gti-fiscal-hub-postgres-1 pg_restore -U gti -d gti_fiscal --clean --if-exists --no-owner < "$BACKUP_DIR/gti_fiscal.dump"
# 3. Remover a base temporária após confirmação
docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "DROP DATABASE IF EXISTS gti_fiscal_dr_temp;"
```
### Passo 4: Restaurar os releases e configurações
```bash
# Restaurar os arquivos de releases caso necessário
docker exec -i gti-fiscal-hub-fiscal-hub-1 tar -xzf - -C /app/data < "$BACKUP_DIR/releases.tgz"
# Restaurar o arquivo de ambiente
cp "$BACKUP_DIR/hub.env" /home/administrador/gti-fiscal/deploy/hub/.env
chmod 600 /home/administrador/gti-fiscal/deploy/hub/.env
```
### Passo 5: Iniciar o Hub e conferir migrações e healthchecks
```bash
docker compose -f docker-compose.prod.yml up -d fiscal-hub
# Aguardar 10 segundos e verificar o estado
sleep 10
docker exec gti-fiscal-hub-fiscal-hub-1 python -m alembic -c central/alembic.ini current
curl -k https://localhost:8000/health
curl -k https://localhost:8000/version
```
---
## 4. Teste de Restauração em Ambiente Isolado (Validação Sintética)
Para testar a integridade do dump sem interromper o ambiente de produção:
```bash
# Criar um banco de teste efêmero no próprio postgres de lab
docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "DROP DATABASE IF EXISTS gti_fiscal_test_restore; CREATE DATABASE gti_fiscal_test_restore OWNER gti;"
# Restaurar o dump no banco efêmero
docker exec -i gti-fiscal-hub-postgres-1 pg_restore -U gti -d gti_fiscal_test_restore --no-owner < "$BACKUP_DIR/gti_fiscal.dump"
# Verificar integridade: contagem de tabelas e datasets
docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d gti_fiscal_test_restore -c "
SELECT count(*) AS total_datasets FROM datasets;
SELECT count(*) AS total_agents FROM agents;
SELECT count(*) AS total_installations FROM installations;
"
# Limpar o banco efêmero
docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "DROP DATABASE gti_fiscal_test_restore;"
```
Resultado esperado: comando finalizado com código de saída 0 e contagens idênticas às tabelas de produção.