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 '' in response.text assert '' 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 'Blockers abertos56' in open_page.text assert 'Produtos afetados55' in open_page.text assert 'Clientes afetados1' in open_page.text resolved_page = client.get("/blockers?status=RESOLVED") assert 'Blockers abertos0' in resolved_page.text assert 'Produtos afetados1' in resolved_page.text all_page = client.get("/blockers?status=ALL") assert 'Blockers abertos57' in all_page.text assert 'Produtos afetados57' 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 "DesconhecidaDesconhecida" 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 '