- RTC: Agent reads ALL PARAMETROS rows every sync (conflict => off), reports rtc_source/rtc_observed_at/rtc_param_rows; legal_effective_context is informational only - PIS/COFINS: applicability from CRT + PISCOFINS flag + incidence only (never from empty fields); Simples w/o special treatment NOT_APPLICABLE, taxed regime with explicit N OPTIONAL, unreadable declaration NEEDS_REVIEW - CST: one digit with an official zero-padded form is VALID_AFTER_NORMALIZATION (non-blocking warning), nothing is written to the ERP - tests: RTC toggle 0->1->0 across syncs (Agent + Hub stale closing), non-circularity, normalization, panel provenance Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
590 lines
31 KiB
Python
590 lines
31 KiB
Python
from pathlib import Path
|
|
import re
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import select
|
|
|
|
from central.app.blocked_products import upsert_blocker
|
|
from central.app.events import record_event
|
|
from central.app.main import create_app
|
|
from central.app.models import Agent, AgentEvent, AgentSnapshot, Installation, Job, Tenant
|
|
from central.tests.test_control_plane import seed, setup_db
|
|
|
|
|
|
def _panel_client():
|
|
session_factory = setup_db()
|
|
agent_id, _ = seed(session_factory)
|
|
now = datetime(2026, 9, 22, 19, 11, tzinfo=timezone.utc)
|
|
with session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
agent = session.get(Agent, agent_id)
|
|
agent.agent_version = "0.9.0"
|
|
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
|
|
snapshot.total_products = 50
|
|
snapshot.fiscally_complete = 49
|
|
snapshot.safe_blocked = 1
|
|
snapshot.failed_products = 0
|
|
snapshot.coverage_percent = 98
|
|
job = Job(
|
|
agent_id=agent_id,
|
|
kind="REPROCESS_PRODUCTS",
|
|
status="RUNNING",
|
|
created_at=now - timedelta(minutes=3),
|
|
started_at=now - timedelta(minutes=2),
|
|
payload={"codigos": ["P-001"]},
|
|
)
|
|
session.add(job)
|
|
session.flush()
|
|
blocker = upsert_blocker(
|
|
session,
|
|
installation.tenant_id,
|
|
{
|
|
"company": "Acme",
|
|
"product_code": "P-001",
|
|
"description": "Produto de teste",
|
|
"blocker_type": "BLOCKED_MISSING_EVIDENCE",
|
|
"field": "CFOP",
|
|
"reason_code": "MISSING_CONTEXT",
|
|
"input_fingerprint": "page-test",
|
|
"evidence_summary": {"source": "snapshot"},
|
|
},
|
|
installation_id=installation.id,
|
|
)
|
|
blocker.first_seen_at = now - timedelta(days=1, seconds=11)
|
|
blocker.last_seen_at = now.replace(second=37)
|
|
record_event(
|
|
session,
|
|
tenant_id=installation.tenant_id,
|
|
installation_id=installation.id,
|
|
agent_id=agent_id,
|
|
severity="WARNING",
|
|
event_type="PRODUCT_BLOCKED",
|
|
message="Contexto fiscal ausente",
|
|
job_id=job.id,
|
|
product_code="P-001",
|
|
blocker_id=blocker.id,
|
|
occurred_at=now,
|
|
)
|
|
session.commit()
|
|
|
|
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
|
|
client._session_factory = session_factory
|
|
login = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
|
|
assert login.status_code == 303
|
|
return client, agent_id, blocker.id
|
|
|
|
|
|
def test_dashboard_renders_operational_queue_labels_and_human_dates():
|
|
client, _, _ = _panel_client()
|
|
|
|
response = client.get("/")
|
|
|
|
assert response.status_code == 200
|
|
for label in ("Servidores", "Online", "Atenção", "Offline", "Falhou", "Não informado"):
|
|
assert label in response.text
|
|
assert "Produtos com blockers abertos" in response.text
|
|
assert "Blockers abertos" in response.text
|
|
assert "Atenção necessária" in response.text
|
|
assert "Ver blockers" in response.text
|
|
assert "Jobs com falha" in response.text
|
|
assert "Agents desatualizados" in response.text
|
|
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", response.text)
|
|
|
|
|
|
def test_dashboard_coverage_displays_reconciled_product_buckets():
|
|
client, _, _ = _panel_client()
|
|
|
|
response = client.get("/")
|
|
|
|
assert response.status_code == 200
|
|
for label in ("Produtos avaliados", "Completos", "Bloqueados com segurança", "Falhas técnicas", "Não classificados"):
|
|
assert label in response.text
|
|
assert "Produtos com blockers abertos" in response.text
|
|
assert "Cobertura reconciliada" in response.text
|
|
|
|
|
|
def test_agents_scope_querystring_distinguishes_active_inventory_from_history():
|
|
client, _, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
tenant = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
|
|
tenant.active = False
|
|
session.commit()
|
|
|
|
current = client.get("/agents?scope=active")
|
|
history = client.get("/agents?scope=all")
|
|
|
|
assert current.status_code == history.status_code == 200
|
|
assert "Acme ERP" not in current.text
|
|
assert "Acme ERP" in history.text
|
|
|
|
|
|
def test_blockers_and_jobs_scope_routes_keep_history_and_server_filters():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
agent = session.get(Agent, agent_id)
|
|
agent.enabled = False
|
|
session.commit()
|
|
|
|
assert "P-001" not in client.get("/blockers?scope=active&field=CFOP&reason_code=MISSING_CONTEXT").text
|
|
assert "P-001" in client.get("/blockers?scope=all&field=CFOP&reason_code=MISSING_CONTEXT").text
|
|
assert "Acme ERP" not in client.get("/jobs?scope=active&status=RUNNING").text
|
|
assert "Acme ERP" in client.get("/jobs?scope=all&status=RUNNING").text
|
|
|
|
|
|
def test_dashboard_links_reach_scoped_health_blocker_failed_job_and_outdated_filters():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
upsert_blocker(session, installation.tenant_id, {"company": "Acme", "product_code": "P-NCM", "description": "Mouse GTI", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "NCM", "reason_code": "INVALID_NCM", "input_fingerprint": "ncm-link"}, installation_id=installation.id)
|
|
session.add(Job(agent_id=agent_id, kind="SYNC_INCREMENTAL", status="FAILED", created_at=datetime.now(timezone.utc), error="falha longa"))
|
|
session.commit()
|
|
|
|
home = client.get("/").text
|
|
|
|
assert 'href="/agents?scope=active"' in home
|
|
assert 'href="/agents?scope=active&health=ONLINE"' in home
|
|
assert 'href="/blockers?scope=active&status=OPEN&field=NCM"' in home
|
|
assert 'href="/jobs?scope=active&status=FAILED"' in home
|
|
assert 'href="/agents?scope=active&version=outdated"' in home
|
|
|
|
|
|
def test_list_filters_are_visible_and_clearable_with_ptbr_statuses():
|
|
client, _, _ = _panel_client()
|
|
|
|
agents = client.get("/agents?scope=active&health=ONLINE").text
|
|
blockers = client.get("/blockers?scope=active&field=CFOP").text
|
|
jobs = client.get("/jobs?scope=active&status=RUNNING").text
|
|
|
|
assert 'name="health"' in agents and 'value="ONLINE" selected' in agents
|
|
assert 'href="/agents?scope=active"' in agents and "Limpar filtros" in agents
|
|
assert "Saúde técnica: Online" not in agents
|
|
assert 'name="field"' in blockers and 'value="CFOP" selected' in blockers
|
|
assert "Evidência fiscal ausente" in blockers and "Prioridade: HIGH" not in blockers
|
|
assert 'href="/blockers?scope=active"' in blockers and "Limpar filtros" in blockers
|
|
assert 'value="RUNNING" selected' in jobs and "Executando" in jobs
|
|
assert 'href="/jobs?scope=active"' in jobs and "Limpar filtros" in jobs
|
|
|
|
|
|
def test_login_has_centered_card_and_explicit_field_labels():
|
|
client, _, _ = _panel_client()
|
|
|
|
response = client.get("/login")
|
|
|
|
assert response.status_code == 200
|
|
assert 'class="login-page"' in response.text
|
|
# Same card component as the GTI Gerenciador login (gti-theme.css .cartao).
|
|
assert 'class="cartao login-card"' in response.text
|
|
assert '<label for="login-username">Usuário</label>' in response.text
|
|
assert '<label for="login-password">Senha</label>' in response.text
|
|
|
|
|
|
def test_dashboard_lists_bounded_failed_job_rows_with_identity_error_age_and_empty_state():
|
|
client, agent_id, _ = _panel_client()
|
|
factory = client._session_factory
|
|
with factory() as session:
|
|
for index in range(12):
|
|
session.add(Job(agent_id=agent_id, kind="REPROCESS_PRODUCTS", status="FAILED", error=f"erro curto {index}", created_at=datetime(2026, 9, 22, 18, 0, index, tzinfo=timezone.utc), finished_at=datetime(2026, 9, 22, 18, 5, index, tzinfo=timezone.utc)))
|
|
session.commit()
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
assert "Acme" in response.text and "Acme ERP" in response.text and "srv-01" in response.text
|
|
assert "erro curto" in response.text and "Falhas recentes" in response.text and "Falhou" in response.text
|
|
assert 'href="/jobs?scope=active&status=FAILED&job_id=' in response.text
|
|
assert response.text.count('class="error-text job-error-summary"') == 10 # bounded, one clamped summary per row
|
|
assert 'title="2026-09-22T18:05:11+00:00"' in response.text
|
|
assert ">22/09/2026 15:05<" in response.text
|
|
with factory() as session:
|
|
session.query(Job).filter(Job.status == "FAILED").delete()
|
|
session.commit()
|
|
assert "Nenhum job com falha." in client.get("/").text
|
|
|
|
|
|
def test_blocker_summary_uses_complete_filtered_relation_and_tenant_product_identity():
|
|
client, agent_id, _ = _panel_client()
|
|
factory = client._session_factory
|
|
with factory() as session:
|
|
original = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
other_tenant = Tenant(name="Outra empresa")
|
|
session.add(other_tenant); session.flush()
|
|
other_agent = Agent(tenant_id=other_tenant.id, name="agent-2", token_hash="y" * 64)
|
|
session.add(other_agent); session.flush()
|
|
other_installation = Installation(agent_id=other_agent.id, tenant_id=other_tenant.id, installation_id="22222222-2222-4222-8222-222222222222", display_name="Outra ERP", hostname="srv-02")
|
|
session.add(other_installation); session.flush()
|
|
for index in range(55):
|
|
upsert_blocker(session, original.tenant_id, {"company": "Acme", "product_code": f"P-{index:03}", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": f"many-{index}"}, installation_id=original.id)
|
|
upsert_blocker(session, other_tenant.id, {"company": "Outra", "product_code": "P-000", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "NCM", "reason_code": "MISSING", "input_fingerprint": "other-tenant"}, installation_id=other_installation.id)
|
|
resolved = upsert_blocker(session, original.tenant_id, {"company": "Acme", "product_code": "P-RES", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "ST", "reason_code": "MISSING", "input_fingerprint": "resolved"}, installation_id=original.id)
|
|
resolved.resolved_at = datetime.now(timezone.utc)
|
|
session.commit()
|
|
open_page = client.get("/blockers?status=OPEN&field=CFOP")
|
|
assert open_page.status_code == 200
|
|
assert '<span>Blockers abertos</span><strong>56</strong>' in open_page.text
|
|
assert '<span>Produtos afetados</span><strong>55</strong>' in open_page.text
|
|
assert '<span>Clientes afetados</span><strong>1</strong>' in open_page.text
|
|
resolved_page = client.get("/blockers?status=RESOLVED")
|
|
assert '<span>Blockers abertos</span><strong>0</strong>' in resolved_page.text
|
|
assert '<span>Produtos afetados</span><strong>1</strong>' in resolved_page.text
|
|
all_page = client.get("/blockers?status=ALL")
|
|
assert '<span>Blockers abertos</span><strong>57</strong>' in all_page.text
|
|
assert '<span>Produtos afetados</span><strong>57</strong>' in all_page.text
|
|
assert "Produtos afetados" in open_page.text and "Clientes afetados" in open_page.text
|
|
|
|
|
|
def test_agents_render_separate_health_and_fiscal_status_with_server_sort():
|
|
client, _, _ = _panel_client()
|
|
|
|
response = client.get("/agents?sort=worst_coverage&health=ONLINE&search=Acme")
|
|
|
|
assert response.status_code == 200
|
|
for label in ("Cliente / servidor", "Saúde", "Status fiscal", "Cobertura", "Produtos", "Versão", "Último contato"):
|
|
assert label in response.text
|
|
assert "Acme ERP" in response.text
|
|
assert "srv-01" in response.text
|
|
assert "98,0%" in response.text
|
|
assert 'name="health"' in response.text
|
|
assert 'value="Acme"' in response.text
|
|
assert 'value="worst_coverage"' in response.text
|
|
|
|
|
|
def test_agent_list_and_detail_show_nao_informado_when_snapshot_missing():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
session.query(AgentSnapshot).filter(AgentSnapshot.installation_id == installation.id).delete()
|
|
session.commit()
|
|
|
|
listing = client.get("/agents")
|
|
assert listing.status_code == 200
|
|
assert "None completos" not in listing.text
|
|
assert "None bloqueados" not in listing.text
|
|
assert ">None<" not in listing.text
|
|
assert "Não informado" in listing.text
|
|
|
|
detail = client.get(f"/agents/{agent_id}")
|
|
assert detail.status_code == 200
|
|
assert ">None<" not in detail.text
|
|
assert "None completos" not in detail.text
|
|
assert "Não informado" in detail.text
|
|
|
|
|
|
def test_agent_list_hides_technical_uuid_and_explains_remainder():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
technical_uuid = installation.installation_id
|
|
agent = session.get(Agent, agent_id)
|
|
agent.agent_version = None
|
|
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
|
|
snapshot.version_drift = "UNKNOWN"
|
|
session.commit()
|
|
|
|
response = client.get("/agents")
|
|
|
|
assert response.status_code == 200
|
|
assert technical_uuid not in response.text
|
|
assert "bloqueados" in response.text
|
|
assert "Não informada" in response.text
|
|
assert "<td>Desconhecida<small>Desconhecida</small></td>" not in response.text
|
|
|
|
|
|
def test_agent_list_and_detail_show_localized_last_seen_with_iso_tooltip():
|
|
client, agent_id, _ = _panel_client()
|
|
expected_iso = '2026-09-22T19:11:37+00:00'
|
|
expected_display = '22/09/2026 16:11'
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
installation.last_seen_at = datetime(2026, 9, 22, 19, 11, 37, tzinfo=timezone.utc)
|
|
session.commit()
|
|
|
|
agents = client.get("/agents")
|
|
detail = client.get(f"/agents/{agent_id}")
|
|
|
|
assert agents.status_code == 200
|
|
assert f'title="{expected_iso}"' in agents.text
|
|
assert f'>{expected_display}<' in agents.text
|
|
assert detail.status_code == 200
|
|
assert f'title="{expected_iso}"' in detail.text
|
|
assert f'>{expected_display} ·' in detail.text
|
|
|
|
|
|
def test_jobs_render_client_identity_duration_and_preserve_filters():
|
|
client, _, _ = _panel_client()
|
|
|
|
response = client.get("/jobs?status=RUNNING&search=Acme")
|
|
|
|
assert response.status_code == 200
|
|
for label in ("Cliente / servidor", "Tipo / job", "Status", "Criado", "Finalizado / duração", "Erro / ação"):
|
|
assert label in response.text
|
|
assert "Acme" in response.text
|
|
assert "srv-01" in response.text
|
|
assert "em andamento" in response.text
|
|
assert 'value="Acme"' in response.text
|
|
assert '<option value="RUNNING" selected>' in response.text
|
|
assert "Agent 1" not in response.text
|
|
|
|
|
|
def test_jobs_show_localized_created_and_finished_with_iso_tooltips():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
session.add(Job(agent_id=agent_id, kind="REPROCESS_PRODUCTS", status="COMPLETED", created_at=datetime(2026, 9, 22, 19, 8, tzinfo=timezone.utc), started_at=datetime(2026, 9, 22, 19, 9, tzinfo=timezone.utc), finished_at=datetime(2026, 9, 22, 19, 10, tzinfo=timezone.utc)))
|
|
session.commit()
|
|
|
|
response = client.get("/jobs")
|
|
|
|
assert response.status_code == 200
|
|
assert 'title="2026-09-22T19:08:00+00:00"' in response.text
|
|
assert 'title="2026-09-22T19:10:00+00:00"' in response.text
|
|
assert ">22/09/2026 16:08<" in response.text
|
|
assert ">22/09/2026 16:10<" in response.text
|
|
|
|
|
|
def test_blocker_list_and_detail_show_localized_seen_times_with_iso_tooltips():
|
|
client, _, blocker_id = _panel_client()
|
|
|
|
listing = client.get("/blockers")
|
|
detail = client.get(f"/blockers/{blocker_id}")
|
|
|
|
assert listing.status_code == 200
|
|
assert 'title="2026-09-22T19:11:37+00:00"' in listing.text
|
|
assert ">22/09/2026 16:11<" in listing.text
|
|
assert detail.status_code == 200
|
|
assert 'title="2026-09-21T19:10:49+00:00"' in detail.text
|
|
assert 'title="2026-09-22T19:11:37+00:00"' in detail.text
|
|
assert ">22/09/2026 16:11<" in detail.text
|
|
|
|
|
|
def test_blockers_render_visible_filters_summary_priority_and_detail_link():
|
|
client, _, blocker_id = _panel_client()
|
|
|
|
response = client.get("/blockers?client=Acme&installation=srv-01&field=CFOP&blocker_type=BLOCKED_MISSING_EVIDENCE&status=OPEN&age=0h&search=P-001")
|
|
|
|
assert response.status_code == 200
|
|
for label in ("Cliente", "Instalação / servidor", "Campo", "Tipo de blocker", "Status", "Idade", "Produto ou busca", "Blockers abertos", "Produtos afetados", "Clientes afetados", "CFOP", "ST", "ICMS", "CSOSN", "NCM", "Outros", "Prioridade"):
|
|
assert label in response.text
|
|
assert f"/blockers/{blocker_id}" in response.text
|
|
assert "Alta" in response.text and "Evidência fiscal ausente" in response.text
|
|
assert 'value="Acme"' in response.text
|
|
assert 'value="BLOCKED_MISSING_EVIDENCE"' in response.text
|
|
assert "window.confirm" not in response.text
|
|
|
|
|
|
def test_agent_and_blocker_details_keep_safe_forms_and_human_event_rows():
|
|
client, agent_id, blocker_id = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
upsert_blocker(session, installation.tenant_id, {"company": "Acme", "product_code": "LEGACY", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "ST", "reason_code": "LEGACY", "input_fingerprint": "tenant-only"})
|
|
session.commit()
|
|
|
|
agent_response = client.get(f"/agents/{agent_id}?tab=events&severity=WARNING&event_type=PRODUCT_BLOCKED&page=2")
|
|
blocker_response = client.get(f"/blockers/{blocker_id}")
|
|
|
|
assert agent_response.status_code == 200
|
|
for label in ("Cliente", "Nome de exibição", "Hostname", "Saúde técnica", "Status fiscal", "Último heartbeat", "Versão", "Empresa", "UF", "CRT", "Eventos recentes", "Severidade", "Tipo de evento"):
|
|
assert label in agent_response.text
|
|
assert "Contexto fiscal ausente" in agent_response.text
|
|
assert f"/blockers?agent_id={agent_id}" in agent_response.text
|
|
assert ">CFOP</span><b>1</b>" in agent_response.text
|
|
assert ">ST</span><b>0</b>" in agent_response.text
|
|
agent_blockers = client.get(f"/blockers?agent_id={agent_id}")
|
|
assert "Produto de teste" in agent_blockers.text and "LEGACY" not in agent_blockers.text
|
|
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", re.sub(r' title="[^"]*"', "", agent_response.text))
|
|
assert 'value="WARNING" selected' in agent_response.text
|
|
assert 'value="PRODUCT_BLOCKED"' in agent_response.text
|
|
|
|
assert blocker_response.status_code == 200
|
|
for label in ("Detalhes do blocker", "Evidência", "Contexto atual", "Salvar contexto e reprocessar", "Chave de contexto", "Valor", "Escopo", "Nota"):
|
|
assert label in blocker_response.text
|
|
assert 'name="context"' not in blocker_response.text
|
|
assert "data-confirm" not in blocker_response.text
|
|
assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", re.sub(r' title="[^"]*"', "", blocker_response.text))
|
|
|
|
|
|
def test_legacy_blocker_detail_explains_unavailable_resolution_without_form():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
blocker = upsert_blocker(
|
|
session,
|
|
installation.tenant_id,
|
|
{"company": "Acme", "product_code": "LEGACY", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "ST", "reason_code": "LEGACY", "input_fingerprint": "unbound-detail"},
|
|
)
|
|
session.commit()
|
|
blocker_id = blocker.id
|
|
|
|
response = client.get(f"/blockers/{blocker_id}")
|
|
|
|
assert response.status_code == 200
|
|
assert "blocker legado de empresa" in response.text.lower()
|
|
assert "sem vínculo comprovado com um servidor" in response.text.lower()
|
|
assert "não é possível oferecer o reprocessamento de contexto com segurança" in response.text.lower()
|
|
assert "Salvar contexto e reprocessar" not in response.text
|
|
assert f'action="/blockers/{blocker_id}/resolve-context"' not in response.text
|
|
|
|
|
|
def test_agent_detail_resolves_agent_after_first_hundred_installations():
|
|
client, _, _ = _panel_client()
|
|
factory = client._session_factory
|
|
with factory() as session:
|
|
tenant = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
|
|
target_id = 0
|
|
for index in range(105):
|
|
agent = Agent(tenant_id=tenant.id, name=f"late-{index}", token_hash=f"{index:064d}")
|
|
session.add(agent); session.flush()
|
|
session.add(Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id=f"{index + 10:08d}-1111-4111-8111-111111111111", display_name=f"Late ERP {index}", hostname=f"late-{index}"))
|
|
target_id = agent.id
|
|
session.commit()
|
|
response = client.get(f"/agents/{target_id}")
|
|
assert response.status_code == 200
|
|
assert "Late ERP 104" in response.text
|
|
|
|
|
|
def test_agent_detail_reconciles_coverage_and_formats_missing_technical_fields():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
agent = session.get(Agent, agent_id)
|
|
agent.agent_version = None
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
|
|
snapshot.version_drift = "UNKNOWN"
|
|
session.commit()
|
|
|
|
response = client.get(f"/agents/{agent_id}")
|
|
|
|
assert response.status_code == 200
|
|
for label in ("Produtos", "Completos", "Bloqueados com segurança", "Falhas técnicas", "Não classificados", "Cobertura fiscal"):
|
|
assert label in response.text
|
|
assert "Não informado" in response.text
|
|
assert "Desconhecida" in response.text
|
|
assert "Desconhecida · Desconhecida" not in response.text
|
|
assert "ODS -" not in response.text
|
|
context = response.text.split("<h2>Contexto da empresa</h2>", 1)[1].split("</article>", 1)[0]
|
|
assert "Reprocessar tudo" not in context # the only reprocess action lives in the Processamento panel
|
|
assert response.text.count("Reprocessar tudo") == 1
|
|
assert not re.search(r"<(?:a|button)\b[^>]*>\s*</(?:a|button)>", context)
|
|
|
|
|
|
def test_agent_detail_reports_negative_coverage_counts_as_data_inconsistency():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
snapshot = session.scalar(select(AgentSnapshot).where(AgentSnapshot.installation_id == installation.id))
|
|
snapshot.failed_products = -1
|
|
session.commit()
|
|
|
|
response = client.get(f"/agents/{agent_id}")
|
|
|
|
assert response.status_code == 200
|
|
assert "contagens negativas" in response.text
|
|
|
|
|
|
def test_agent_event_filters_lookahead_and_safe_event_reference_links_and_confirmation():
|
|
client, agent_id, _ = _panel_client()
|
|
factory = client._session_factory
|
|
with factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
job = session.scalar(select(Job).where(Job.agent_id == agent_id))
|
|
for index in range(27):
|
|
session.add(AgentEvent(tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="INFO", event_type="HEARTBEAT", message=f"evento {index}", structured_context={}, job_id=job.id, product_code="P-001", occurred_at=datetime(2026, 9, 21 + index % 2, tzinfo=timezone.utc)))
|
|
session.add(AgentEvent(tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="INFO", event_type="HEARTBEAT", message="evento anterior ao filtro", structured_context={}, occurred_at=datetime(2026, 9, 20, tzinfo=timezone.utc)))
|
|
session.commit()
|
|
first = client.get(f"/agents/{agent_id}?tab=events&since=2026-09-21&until=2026-09-22")
|
|
assert first.status_code == 200
|
|
assert 'name="since"' in first.text and 'name="until"' in first.text
|
|
assert "evento anterior ao filtro" not in first.text
|
|
assert "Próxima" in first.text and "since=2026-09-21" in first.text and "until=2026-09-22" in first.text
|
|
last = client.get(f"/agents/{agent_id}?tab=events&event_page=2&since=2026-09-21&until=2026-09-22")
|
|
assert "Anterior" in last.text and "Próxima" not in last.text
|
|
assert "/jobs?job_id=" in first.text and "/blockers?agent_id=" in first.text
|
|
csrf = client.cookies.get("gti_csrf")
|
|
blocked = client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": csrf})
|
|
assert blocked.status_code == 400 and "confirmar" in blocked.text.lower() and 'name="confirm"' in blocked.text
|
|
confirmation = client.get(f"/agents/{agent_id}/reprocess-confirmation")
|
|
assert confirmation.status_code == 200 and 'name="confirm"' in confirmation.text and 'required' in confirmation.text
|
|
assert f'href="/agents/{agent_id}"' in confirmation.text
|
|
assert client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": "wrong", "confirm": "yes"}).status_code == 403
|
|
assert client.post(f"/agents/{agent_id}/reprocess", data={"csrf_token": csrf, "confirm": "yes"}, follow_redirects=False).status_code == 303
|
|
|
|
|
|
def test_theme_defaults_to_dark_and_never_overrides_a_saved_choice():
|
|
client, _, _ = _panel_client()
|
|
base = client.get("/agents").text
|
|
# no saved preference -> dark; an explicit "claro" is the only thing that yields light
|
|
assert 'tema === "claro" ? "claro" : "escuro"' in base
|
|
assert "prefers-color-scheme" not in (Path(__file__).parents[1] / "static" / "tema.js").read_text(encoding="utf-8")
|
|
script = (Path(__file__).parents[1] / "static" / "tema.js").read_text(encoding="utf-8")
|
|
assert 'return "escuro"' in script and 'localStorage.setItem("tema", novo)' in script
|
|
for page in ("login.html", "logged_out.html", "sso_error.html"):
|
|
assert 'data-tema="escuro"' in (Path(__file__).parents[1] / "templates" / page).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_operator_pages_use_portuguese_titles_and_consistent_ctas():
|
|
client, agent_id, _ = _panel_client()
|
|
assert "<title>Agents / Servidores | GTI Fiscal</title>" in client.get("/agents").text
|
|
assert "<title>Blockers | GTI Fiscal</title>" in client.get("/blockers").text
|
|
assert "<title>Jobs | GTI Fiscal</title>" in client.get("/jobs").text
|
|
dashboard = client.get("/").text
|
|
for old in ("Abrir fila", "Ver instalações", "Ver todos", "Ver job #"):
|
|
assert old not in dashboard
|
|
|
|
|
|
def test_agent_detail_processing_hierarchy_and_status_badges():
|
|
client, agent_id, _ = _panel_client()
|
|
page = client.get(f"/agents/{agent_id}").text
|
|
assert page.count("Reprocessar tudo") == 1
|
|
assert 'class="primary-action"' in page and 'class="botao secondary"' in page and 'class="caution"' in page
|
|
assert "Mantém o heartbeat, mas não executa jobs" in page and "Recalcula todos os produtos" in page
|
|
assert "Saúde técnica: " not in page and "Status fiscal: " not in page # context lives in the small label, not in the badge
|
|
assert '<small class="status-label">Saúde técnica</small>' in page
|
|
|
|
|
|
def _complete_sync(client, agent_id, fiscal_context):
|
|
with client._session_factory() as session:
|
|
session.add(Job(agent_id=agent_id, kind="SYNC_INCREMENTAL", status="COMPLETED", payload={}, result_summary={"fiscal_context": fiscal_context}, created_at=datetime(2026, 9, 22, 19, 0, tzinfo=timezone.utc),
|
|
finished_at=datetime(2026, 9, 22, 19, 5, tzinfo=timezone.utc)))
|
|
session.commit()
|
|
|
|
|
|
def test_agent_detail_has_one_coverage_and_shows_rtc_status_only_when_reported():
|
|
client, agent_id, _ = _panel_client()
|
|
plain = client.get(f"/agents/{agent_id}").text
|
|
assert "Reforma Tributária" not in plain # older Agents do not report it
|
|
for text in (plain,):
|
|
assert "Escopo anterior" not in text and "Escopo ampliado" not in text and "aplicabilidade desconhecida" not in text
|
|
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_supported": True, "rtc_catalog_valid": True})
|
|
off = client.get(f"/agents/{agent_id}").text
|
|
assert re.search(r"Reforma Tributária</dt><dd><span[^>]*>Desativada</span>", off) and "Escopo anterior" not in off and "Escopo ampliado" not in off
|
|
assert "cClassTrib" not in off # RTC off never surfaces RTC as a problem
|
|
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True})
|
|
on = client.get(f"/agents/{agent_id}").text
|
|
assert re.search(r"Reforma Tributária</dt><dd><span[^>]*>Ativa</span>", on)
|
|
|
|
|
|
def test_pis_cofins_and_rtc_blockers_appear_in_the_same_queue_and_dashboard_breakdown():
|
|
client, agent_id, _ = _panel_client()
|
|
with client._session_factory() as session:
|
|
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
|
|
for code, field, reason in (("P-010", "PIS", "PIS_CST_INVALID"), ("P-011", "COFINS", "COFINS_CST_MISSING"), ("P-012", "RTC", "CCLASSTRIB_MISSING")):
|
|
upsert_blocker(session, installation.tenant_id, {"company": "Acme", "product_code": code, "description": "x", "blocker_type": "BLOCKED_FISCAL_DATA", "field": field, "reason_code": reason, "input_fingerprint": code},
|
|
installation_id=installation.id)
|
|
dash = client.get("/").text
|
|
for key in ("PIS", "COFINS", "RTC", "CEST"):
|
|
assert f"<span>{key}</span>" in dash
|
|
queue = client.get("/blockers?scope=active&status=OPEN").text
|
|
assert "CST PIS inválido" in queue and "CST COFINS ausente" in queue and "cClassTrib ausente" in queue
|
|
assert "fila RTC" not in queue.lower()
|
|
filtered = client.get("/blockers?scope=active&status=OPEN&field=RTC").text
|
|
assert "cClassTrib ausente" in filtered and "CST PIS inválido" not in filtered
|
|
|
|
|
|
def test_rtc_status_shows_where_and_when_the_agent_observed_it():
|
|
client, agent_id, _ = _panel_client()
|
|
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA", "rtc_observed_at": "2026-09-24T20:00:00+00:00"})
|
|
text = client.get(f"/agents/{agent_id}").text
|
|
assert 'title="Lido no banco do cliente (PARAMETROS.USAR_REFORMA_TRIBUTARIA) em ' in text and ">Ativa<" in text
|
|
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_param_conflict": True, "rtc_source": "PARAMETROS.USAR_REFORMA_TRIBUTARIA"})
|
|
conflict = client.get(f"/agents/{agent_id}").text
|
|
assert "linhas de PARAMETROS divergentes" in conflict and ">Desativada<" in conflict
|
|
for legacy in ("Escopo anterior", "Escopo ampliado", "Legacy coverage", "Expanded coverage"):
|
|
assert legacy not in conflict
|