Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'

git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
This commit is contained in:
gti
2026-09-29 14:43:23 -03:00
868 changed files with 566394 additions and 0 deletions
@@ -0,0 +1,418 @@
"""Agent distribution: releases + secure enrollment.
Covers central/app/agent_distribution.py, enrollment.py, release_storage.py,
rate_limit.py, scripts/publish_agent_release.py and the new admin pages in
web.py. Security tests mirror the mission's checklist: expired/revoked/
second-use/tampered download links, expired/replayed/wrong-installation/
already-enrolled enrollment attempts, path traversal, rate limiting, and
that no secret ever reaches the audit log.
"""
import dataclasses
import io
import json
import sys
import tempfile
import zipfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from central.app import config, enrollment, rate_limit, release_storage
from central.app.main import create_app
from central.app.models import Agent, AgentEnrollment, AgentRelease, AuditEvent, Installation, Tenant
from central.scripts.publish_agent_release import publish
from central.tests.test_control_plane import seed, setup_db
VERSION = "1.2.3"
def make_release_zip(version=VERSION) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr(f"GTIFiscalAgent-{version}/gti-agent.ps1", "# fake install script\n")
z.writestr(f"GTIFiscalAgent-{version}/VERSION", version)
return buf.getvalue()
def write_temp_zip(data: bytes, version=VERSION) -> Path:
path = Path(tempfile.gettempdir()) / f"GTIFiscalAgent-{version}-{id(data)}.zip"
path.write_bytes(data)
return path
@pytest.fixture(autouse=True)
def clear_rate_limit():
rate_limit._attempts.clear()
yield
rate_limit._attempts.clear()
@pytest.fixture
def hub(tmp_path, monkeypatch):
replaced = dataclasses.replace(
config.settings,
agent_release_storage_path=str(tmp_path / "releases"),
agent_download_link_ttl_seconds=1800,
agent_download_max_uses_default=1,
agent_enrollment_ttl_seconds=86400,
base_url="https://gtifiscal.example.gti",
web_cookie_secure=False,
)
monkeypatch.setattr(config, "settings", replaced)
# publish_agent_release.py is written to also run standalone (`python
# central/scripts/publish_agent_release.py`), so it imports via a
# sys.path-inserted "app" package (same convention as register_agent.py)
# rather than "central.app" — a second, independent copy of every module
# it touches, including config. Patch that copy's settings too, or the
# release it publishes lands outside this test's tmp_path.
if "app.config" in sys.modules:
monkeypatch.setattr(sys.modules["app.config"], "settings", replaced)
factory = setup_db()
seed(factory) # tenant "Acme" + Installation "srv-01" + admin "operator"/"a-secure-password"
client = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None))
return client, factory
def login(client):
response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
assert response.status_code == 303
def csrf(client):
return client.cookies.get("gti_csrf", "")
def publish_release(factory, version=VERSION):
with factory() as session:
release, _created = publish(session, write_temp_zip(make_release_zip(version), version), version=version)
return release.id
def tenant_id(factory):
with factory() as session:
tenant = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
# /agents/new only offers active GTI_GERENCIADOR-sourced tenants
# (central/app/client_sync.py) — flip the shared fixture tenant so
# existing form-based tests keep working under that rule.
if tenant.source != "GTI_GERENCIADOR" or not tenant.active:
tenant.source, tenant.external_client_id, tenant.active = "GTI_GERENCIADOR", tenant.id, True
session.commit()
return tenant.id
def audits(factory):
with factory() as session:
return session.scalars(select(AuditEvent)).all()
def bundle_enrollment_fields(content: bytes) -> dict:
with zipfile.ZipFile(io.BytesIO(content)) as z:
[entry] = [n for n in z.namelist() if n.endswith("enrollment.json")]
return json.loads(z.read(entry))
# ---------- publish_agent_release.py ----------
def test_publish_is_idempotent_and_rejects_hash_conflict(hub, tmp_path):
_client, factory = hub
with factory() as session:
zip_bytes = make_release_zip("9.9.9")
release, created = publish(session, write_temp_zip(zip_bytes, "9.9.9"), version="9.9.9")
assert created and release.status == "PUBLISHED" and release.version == "9.9.9"
release2, created2 = publish(session, write_temp_zip(zip_bytes, "9.9.9"), version="9.9.9")
assert not created2 and release2.id == release.id # identical bytes: no-op
with factory() as session:
with pytest.raises(SystemExit):
publish(session, write_temp_zip(make_release_zip("9.9.9") + b"\x00", "9.9.9"), version="9.9.9")
def test_publish_records_published_at(hub, tmp_path):
_client, factory = hub
with factory() as session:
release, created = publish(session, write_temp_zip(make_release_zip("9.9.9"), "9.9.9"), version="9.9.9")
assert created and release.status == "PUBLISHED"
assert release.published_at is not None # publishing stamps the moment, not left NULL
def test_published_release_binary_never_stored_in_postgres(hub):
_client, factory = hub
release_id = publish_release(factory)
with factory() as session:
release = session.get(AgentRelease, release_id)
assert release.storage_path == Path(release.storage_path).name # a bare filename, never a path with directories
path = release_storage.safe_path_for(release.storage_path)
assert path.exists() and path.read_bytes() # binary lives on disk, outside the DB
# ---------- path traversal ----------
def test_release_storage_path_traversal_impossible(hub):
for hostile in ("../../etc/passwd", "sub/dir.zip", "/etc/passwd", "..\\..\\windows\\system32", ""):
with pytest.raises(ValueError):
release_storage.safe_path_for(hostile)
# ---------- full happy path: create -> link -> download -> enroll ----------
def test_full_installation_flow_creates_a_working_agent(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Servidor Principal", "expected_hostname": "SRV-UNIAO",
"release_id": release_id}, follow_redirects=False)
assert created.status_code == 303
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
linked = client.post(f"/agents/pending/{installation_uuid}/link", data={"csrf_token": csrf(client)}, follow_redirects=False)
assert linked.status_code == 303
download_url = linked.headers["location"].split("link=", 1)[1]
download_token = download_url.rsplit("/", 1)[-1]
# A technician on the client machine never has an admin session.
anon = TestClient(client.app)
downloaded = anon.get(f"/dl/agent/{download_token}")
assert downloaded.status_code == 200
assert downloaded.headers["content-type"] == "application/octet-stream"
assert "attachment" in downloaded.headers["content-disposition"]
payload = bundle_enrollment_fields(downloaded.content)
assert payload["installation_id"] == installation_uuid
enrolled = anon.post("/v1/agents/enroll", json={
"installation_id": payload["installation_id"], "enrollment_token": payload["enrollment_token"],
"agent_version": "1.2.3", "hostname": "SRV-UNIAO", "platform": "windows", "machine_fingerprint": "abc123",
})
assert enrolled.status_code == 200
body = enrolled.json()
assert body["agent_id"] and body["agent_token"] and body["hostname_mismatch"] is False
with factory() as session:
agent = session.get(Agent, body["agent_id"])
assert agent.enabled and agent.agent_version == "1.2.3"
installation = session.scalar(select(Installation).where(Installation.agent_id == agent.id))
assert installation.installation_id == installation_uuid and installation.hostname == "SRV-UNIAO"
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid))
assert enr.status == "ENROLLED" and enr.enrollment_token_hash is None and enr.agent_id == agent.id
# Replay: the very same enrollment token can never enroll a second Agent.
replay = anon.post("/v1/agents/enroll", json={"installation_id": payload["installation_id"], "enrollment_token": payload["enrollment_token"]})
assert replay.status_code == 401 and replay.json()["detail"] == "ALREADY_ENROLLED"
# Second use of the (single-use) download link is also rejected.
redownload = anon.get(f"/dl/agent/{download_token}")
assert redownload.status_code == 404
events = {event.action for event in audits(factory)}
assert {"INSTALLATION_CREATED", "DOWNLOAD_LINK_CREATED", "PACKAGE_DOWNLOADED", "ENROLLMENT_SUCCEEDED", "AGENT_CREDENTIAL_ISSUED", "ENROLLMENT_REJECTED"} <= events
secrets_leaked = [download_token, payload["enrollment_token"], body["agent_token"]]
for event in audits(factory):
haystack = json.dumps({"action": event.action, "target": event.target_id, "context": event.context})
for secret in secrets_leaked:
assert secret not in haystack
def test_direct_authenticated_download_does_not_need_a_link(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Loja 2", "release_id": release_id}, follow_redirects=False)
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
downloaded = client.get(f"/agents/pending/{installation_uuid}/download")
assert downloaded.status_code == 200
payload = bundle_enrollment_fields(downloaded.content)
assert payload["installation_id"] == installation_uuid
# ---------- route precedence: /agents/new vs /agents/{agent_id} ----------
def test_get_agents_new_renders_form_not_captured_by_dynamic_route(hub):
client, factory = hub
publish_release(factory)
tenant_id(factory) # at least one active GTI_GERENCIADOR tenant, or the form shows the empty state instead
login(client)
response = client.get("/agents/new")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "Nova instalação" in response.text
assert "Criar instalação" in response.text
def test_get_agents_releases_page_not_captured_by_dynamic_route(hub):
client, _factory = hub
login(client)
response = client.get("/agents/releases")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "Versões do Agent" in response.text
def test_get_agent_detail_still_renders_for_valid_numeric_id(hub):
client, factory = hub
login(client)
with factory() as session:
agent_id = session.scalar(select(Agent.id))
response = client.get(f"/agents/{agent_id}")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
def test_get_agents_non_numeric_slug_is_controlled_404_and_never_captures_statics(hub):
client, _factory = hub
login(client)
response = client.get("/agents/abc")
assert response.status_code == 404
assert "text/html" in response.headers["content-type"]
# the legitimate static routes keep working regardless of the slug above
assert client.get("/agents/new").status_code == 200
assert client.get("/agents/releases").status_code == 200
# ---------- download-link security ----------
def _pending(factory, release_id, **overrides):
import uuid as uuid_mod
with factory() as session:
enr = AgentEnrollment(enrollment_uuid=str(uuid_mod.uuid4()), tenant_id=tenant_id(factory),
display_name="Teste", release_id=release_id, status="PENDING", **overrides)
session.add(enr); session.commit()
return enr.enrollment_uuid
def test_expired_download_link_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
uid = _pending(factory, release_id)
token = enrollment.new_token()
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uid))
enr.download_token_hash = enrollment.token_digest(token)
enr.download_token_expires_at = datetime.now(timezone.utc) - timedelta(seconds=1)
enr.download_max_uses = 1
session.commit()
assert client.get(f"/dl/agent/{token}").status_code == 404
def test_revoked_installation_link_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Teste", "release_id": release_id}, follow_redirects=False)
uid = created.headers["location"].rsplit("/", 1)[-1]
linked = client.post(f"/agents/pending/{uid}/link", data={"csrf_token": csrf(client)}, follow_redirects=False)
token = linked.headers["location"].split("link=", 1)[1].rsplit("/", 1)[-1]
client.post(f"/agents/pending/{uid}/revoke", data={"csrf_token": csrf(client)}, follow_redirects=False)
assert client.get(f"/dl/agent/{token}").status_code == 404
def test_wrong_or_random_download_token_rejected(hub):
client, factory = hub
publish_release(factory)
assert client.get("/dl/agent/definitely-not-a-real-token").status_code == 404
def test_release_revoked_blocks_download(hub):
client, factory = hub
release_id = publish_release(factory)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Teste", "release_id": release_id}, follow_redirects=False)
uid = created.headers["location"].rsplit("/", 1)[-1]
linked = client.post(f"/agents/pending/{uid}/link", data={"csrf_token": csrf(client)}, follow_redirects=False)
token = linked.headers["location"].split("link=", 1)[1].rsplit("/", 1)[-1]
client.post(f"/agents/releases/{release_id}/revoke", data={"csrf_token": csrf(client)}, follow_redirects=False)
assert client.get(f"/dl/agent/{token}").status_code == 404
# ---------- enrollment security ----------
def _enrollable(factory, release_id, **overrides):
token = enrollment.new_token()
uid = _pending(factory, release_id)
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uid))
enr.enrollment_token_hash = enrollment.token_digest(token)
enr.enrollment_token_expires_at = overrides.pop("expires_at", datetime.now(timezone.utc) + timedelta(hours=1))
for key, value in overrides.items():
setattr(enr, key, value)
session.commit()
return uid, token
def test_expired_enrollment_token_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
uid, token = _enrollable(factory, release_id, expires_at=datetime.now(timezone.utc) - timedelta(seconds=1))
response = client.post("/v1/agents/enroll", json={"installation_id": uid, "enrollment_token": token})
assert response.status_code == 401 and response.json()["detail"] == "EXPIRED"
def test_wrong_installation_uuid_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
_uid, token = _enrollable(factory, release_id)
import uuid as uuid_mod
response = client.post("/v1/agents/enroll", json={"installation_id": str(uuid_mod.uuid4()), "enrollment_token": token})
assert response.status_code == 401 and response.json()["detail"] == "INVALID_INSTALLATION"
def test_malformed_installation_id_rejected(hub):
client, _factory = hub
response = client.post("/v1/agents/enroll", json={"installation_id": "not-a-uuid", "enrollment_token": "x"})
assert response.status_code == 401 and response.json()["detail"] == "INVALID_INSTALLATION"
def test_random_enrollment_token_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
uid, _token = _enrollable(factory, release_id)
response = client.post("/v1/agents/enroll", json={"installation_id": uid, "enrollment_token": enrollment.new_token()})
assert response.status_code == 401 and response.json()["detail"] == "INVALID_TOKEN"
def test_revoked_installation_enroll_rejected(hub):
client, factory = hub
release_id = publish_release(factory)
uid, token = _enrollable(factory, release_id, status="REVOKED")
response = client.post("/v1/agents/enroll", json={"installation_id": uid, "enrollment_token": token})
assert response.status_code == 401 and response.json()["detail"] == "REVOKED"
def test_hostname_mismatch_is_flagged_but_not_blocked(hub):
client, factory = hub
release_id = publish_release(factory)
uid, token = _enrollable(factory, release_id, expected_hostname="SRV-EXPECTED")
response = client.post("/v1/agents/enroll", json={"installation_id": uid, "enrollment_token": token, "hostname": "SRV-OUTRO"})
assert response.status_code == 200 and response.json()["hostname_mismatch"] is True
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uid))
assert enr.hostname_mismatch is True and enr.status == "ENROLLED"
# ---------- rate limiting ----------
def test_enroll_rate_limited_after_repeated_invalid_attempts(hub):
client, _factory = hub
for _ in range(rate_limit.MAX_ATTEMPTS):
response = client.post("/v1/agents/enroll", json={"installation_id": "not-a-uuid", "enrollment_token": "x"})
assert response.status_code == 401
blocked_response = client.post("/v1/agents/enroll", json={"installation_id": "not-a-uuid", "enrollment_token": "x"})
assert blocked_response.status_code == 429
def test_rate_limit_buckets_are_independent_per_ip(hub):
# Unit-level proof that the limiter itself keys on (bucket, ip) rather than a
# shared global counter — the end-to-end TestClient can't vary the caller's
# apparent IP (that requires the real ProxyHeadersMiddleware, wired up only
# under uvicorn --proxy-headers in production, per FORWARDED_ALLOW_IPS).
for _ in range(rate_limit.MAX_ATTEMPTS):
rate_limit.record_attempt("enroll", "198.51.100.1")
assert rate_limit.blocked("enroll", "198.51.100.1")
assert not rate_limit.blocked("enroll", "198.51.100.2")
@@ -0,0 +1,63 @@
"""Static checks on deploy/agent-windows/gti-agent.ps1's `install-auto` command
(the zero-touch path driven by the enrollment bundle from the panel). No
Windows host is available in CI, so these are text-level guarantees — the
same properties are exercised for real in the manual Windows smoke test
described in docs/PILOT_DEPLOYMENT.md.
"""
import re
from pathlib import Path
SCRIPT = (Path(__file__).parents[2] / "deploy" / "agent-windows" / "gti-agent.ps1").read_text(encoding="utf-8")
def install_auto_block() -> str:
start = SCRIPT.index("'install-auto' {")
end = SCRIPT.index("'configure' {")
return SCRIPT[start:end]
def test_install_auto_is_a_registered_command():
validate_set = re.search(r"ValidateSet\(([^)]*)\)", SCRIPT).group(1)
assert "'install-auto'" in validate_set
def test_install_auto_never_enables_firebird_writes():
block = install_auto_block()
assert "FIREBIRD_WRITES=0" in block
assert "FIREBIRD_WRITES=1" not in SCRIPT # nowhere in the whole script, not just this block
def test_install_auto_refuses_a_non_https_hub():
assert "hub_url -notmatch '^https://'" in install_auto_block()
def test_install_auto_verifies_package_sha256_before_installing():
block = install_auto_block()
assert "SHA256_VALIDATION" in block and "Get-FileHash" in block
assert block.index("SHA256_VALIDATION") < block.index("Invoke-RestMethod") # integrity checked before any network call
def test_install_auto_deletes_the_enrollment_bundle_after_use():
block = install_auto_block()
assert "Remove-Item $EnrollmentFile -Force" in block
assert block.index("Remove-Item $EnrollmentFile") > block.index("Invoke-RestMethod") # only after a successful enroll
def test_install_auto_registers_but_never_starts_the_service():
block = install_auto_block()
assert "install" in block and "Start-Service" not in block # Firebird config is still missing; starting it is a separate, explicit step
def test_install_auto_never_hardcodes_a_client_firebird_path():
block = install_auto_block()
assert "'FIREBIRD_DATABASE_PATH='" in block # written, but as an empty placeholder (no real path baked in)
def test_install_auto_restricts_the_env_file_acl():
block = install_auto_block()
assert "icacls $EnvFile /inheritance:r /grant:r 'Administrators:(F)' 'SYSTEM:(F)'" in block
def test_install_auto_is_idempotent_when_already_enrolled():
block = install_auto_block()
assert "Already enrolled and installed" in block and "exit 0" in block
@@ -0,0 +1,16 @@
from central.app.ai.budget_ledger import BudgetLedger, BudgetLimits
def test_budget_rejects_before_provider_when_daily_limit_is_exceeded():
ledger = BudgetLedger(BudgetLimits(max_run_cost_usd=2.0, max_daily_cost_usd=1.0))
assert ledger.check_and_reserve("r1", "t1", 0.75, 1, 0).allowed is True
decision = ledger.check_and_reserve("r2", "t1", 0.30, 1, 0)
assert decision.allowed is False
assert decision.reason == "AI_BUDGET_EXHAUSTED"
def test_budget_allows_exact_limit_and_counts_new_products():
ledger = BudgetLedger(BudgetLimits(max_run_cost_usd=1.0, max_new_products_per_run=2))
assert ledger.check_and_reserve("r1", "t1", 1.0, 1, 2).allowed is True
assert ledger.check_and_reserve("r1", "t1", 0.0, 0, 1).allowed is False
@@ -0,0 +1,33 @@
from central.app.ai.cache_policy import CONFLICT, NEEDS_REVALIDATION, VALID, cache_key, classify_knowledge_freshness
from central.app.ai.provider import BudgetGuard
def test_cache_key_changes_for_each_input_dimension():
base = ("input", "candidates", "model", "prompt", {"ncm": "n1", "tipi": "t1"})
key = cache_key(*base)
assert key != cache_key("changed", *base[1:])
assert key != cache_key(base[0], "changed", *base[2:])
assert key != cache_key(base[0], base[1], "changed", *base[3:])
assert key != cache_key(base[0], base[1], base[2], "changed", base[4])
assert key != cache_key(*base[:4], {"ncm": "n2", "tipi": "t1"})
assert key == cache_key(*base)
def test_dataset_freshness_is_fail_closed():
assert classify_knowledge_freshness({"ncm": "v1"}, {"ncm": "v1"}) == VALID
assert classify_knowledge_freshness({"ncm": "v1"}, {"ncm": "v2"}) == CONFLICT
assert classify_knowledge_freshness({"ncm": "v1"}, {"ncm": "v1", "tipi": "v2"}) == NEEDS_REVALIDATION
assert classify_knowledge_freshness({}, {"ncm": "v1"}) == NEEDS_REVALIDATION
def test_budget_blocks_before_call_and_actual_usage_cannot_exceed_limit():
guard = BudgetGuard(1.0, 1.0, 2.0)
assert guard.authorize(guard.estimate(600_000, 300_000)) is False
assert guard.authorize(guard.estimate(100_000, 50_000)) is True
assert guard.authorize_usage(100_000, 50_000, prior_cost_usd=0.9) is False
def test_unknown_cost_requires_explicit_limited_run():
guard = BudgetGuard(5.0, None, None)
assert guard.authorize_usage(1000, 100) is False
assert guard.authorize_usage(1000, 100, allow_unknown_cost=True) is True
@@ -0,0 +1,15 @@
from central.app.product_data_provider import result_cache_key
def test_same_classification_inputs_have_same_cache_key():
first = result_cache_key({"gtin": "789", "description": "X"}, ["12345678"], {"ncm": "v1"}, "prompt-v1", "model-v1")
second = result_cache_key({"description": "X", "gtin": "789"}, ["12345678"], {"ncm": "v1"}, "prompt-v1", "model-v1")
assert first == second
def test_changed_dataset_changes_cache_key():
first = result_cache_key({"gtin": "789"}, ["12345678"], {"ncm": "v1"}, "prompt-v1", "model-v1")
second = result_cache_key({"gtin": "789"}, ["12345678"], {"ncm": "v2"}, "prompt-v1", "model-v1")
assert first != second
+181
View File
@@ -0,0 +1,181 @@
"""AI semantic layer V1: fingerprint, KB, dedup, provider contract, budget guard, candidates."""
import json
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from central.app.models import Base, Dataset, NcmEntry, CestEntry, CestNcmEntry, TipiEntry
def _engine():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return engine
def _ncm_dataset(session, entries):
dataset = Dataset(kind="NCM", provider="Siscomex", sha256="a" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=len(entries))
session.add(dataset); session.flush()
for code, description in entries:
session.add(NcmEntry(dataset_id=dataset.id, code=code, description=description, valid_from="", valid_to=""))
session.flush()
return dataset
# ---------------- fingerprint / identity ----------------
def test_fingerprint_is_stable_and_ignores_unstable_context():
from central.app.ai.fingerprint import normalize_description, product_fingerprint, validate_gtin
a = product_fingerprint("Coca-Cola 2LT", "COCA-COLA", "UN", "2")
b = product_fingerprint("coca cola 2lt ", "coca-cola", "un", "2.0")
assert a == b
# fiscal/company context never participates in global identity
c = product_fingerprint("Coca-Cola 2LT", "COCA-COLA", "UN", "2")
assert a == c
assert normalize_description("Água Mineral 500ml") == "AGUA MINERAL 500ML"
assert validate_gtin("7891234567895") == "7891234567895" # valid GTIN-13
assert validate_gtin("7891234567890") is None # bad checksum
assert validate_gtin("123") is None
# ---------------- knowledge base ----------------
def test_kb_exact_fingerprint_hit_and_conflict_without_majority():
from central.app.ai.kb import KnowledgeBase, EXACT, FINGERPRINT
engine = _engine()
with Session(engine) as session:
kb = KnowledgeBase(session)
p1 = kb.upsert_product(fingerprint="f1", gtin="7891234567895", normalized_description="AGUA MINERAL 500ML")
# same GTIN arrives with a different fingerprint -> same product, exact hit
p2, hit = kb.resolve(gtin="7891234567895", fingerprint="other")
assert p2.id == p1.id and hit == EXACT
# same fingerprint, no GTIN -> fingerprint hit
p3, hit = kb.resolve(gtin=None, fingerprint="f2")
assert p3 is None and hit is None
p4 = kb.upsert_product(fingerprint="f2", normalized_description="OUTRO")
_, hit = kb.resolve(gtin=None, fingerprint="f2")
assert hit == FINGERPRINT
kb.add_knowledge(p1.id, "candidate_ncm", "22011000", source="OFFICIAL_DATASET", confidence=1.0)
kb.add_knowledge(p1.id, "candidate_ncm", "22019000", source="OFFICIAL_DATASET", confidence=1.0)
current = kb.current_knowledge(p1.id)
assert current["candidate_ncm"]["conflict"] is True # no majority auto-pick
assert current["candidate_ncm"]["values"] == {"22011000", "22019000"}
stats = kb.stats()
assert stats["products"] == 2 and stats["gtin_products"] == 1 and stats["conflicts"] == 1
def test_kb_aggregates_observations_across_tenants():
from central.app.ai.kb import KnowledgeBase
engine = _engine()
with Session(engine) as session:
kb = KnowledgeBase(session)
p = kb.upsert_product(fingerprint="f1", normalized_description="REFRIGERANTE 2L")
kb.record_observation(p.id, tenant_id=1, company_key="MMMODAS", erp_code="000001", database_label="MMMODAS", observed_fields={"ncm": "22021000"})
kb.record_observation(p.id, tenant_id=2, company_key="UNIAO", erp_code="000009", database_label="UNIAO", observed_fields={"ncm": "22021000"})
agg = kb.observation_summary(p.id)
assert agg["observation_count"] == 2 and agg["tenant_count"] == 2
assert agg["agreement"]["ncm"] == {"22021000": 2} and agg["conflicts"] == {}
# ---------------- work item dedup ----------------
def test_work_items_dedup_by_knowledge_needed():
from central.app.ai.workitems import build_work_items
questions = [
{"QUESTION_ID": "Q1", "DATABASE": "MMMODAS", "COMPANY": "MMMODAS", "SCOPE": "NCM", "GROUP": "", "NCM": "33059000", "KEY": "ex_tipi", "CELLS": 10},
{"QUESTION_ID": "Q2", "DATABASE": "UNIAO", "COMPANY": "UNIAO", "SCOPE": "NCM", "GROUP": "", "NCM": "33059000", "KEY": "ex_tipi", "CELLS": 5},
{"QUESTION_ID": "Q3", "DATABASE": "RINCAO", "COMPANY": "RINCAO", "SCOPE": "GROUP", "GROUP": "000015", "KEY": "product_role", "CELLS": 40},
{"QUESTION_ID": "Q4", "DATABASE": "CONSTRUCAO", "COMPANY": "CONSTRUCAO", "SCOPE": "PRODUCT", "GROUP": "", "NCM": "", "KEY": "ncm", "CELLS": 8, "QUESTION": "produto PRODUCT 000054", "DATABASE_CODE": "000054"},
]
items = build_work_items(questions, product_fingerprints={"CONSTRUCAO:000054": "fp-a"})
# 4 raw questions -> 3 deduped work items (two ex_tipi questions for NCM 33059000 collapse)
by_key = {i.dedup_key: i for i in items}
assert len(items) == 3
assert by_key["ex_tipi:33059000"].cells_covered == 15
assert by_key["ex_tipi:33059000"].question_refs == ["Q1", "Q2"]
assert by_key["product_role:RINCAO:000015"].cells_covered == 40
assert by_key["invalid_ncm:fp-a"].cells_covered == 8
# AI-ineligible keys never become work items
assert all(i.task_type in ("INVALID_NCM", "CEST", "EX_TIPI", "PRODUCT_ROLE") for i in items)
def test_work_items_accept_canonical_identity_map():
from central.app.ai.workitems import build_work_items
questions = [
{"QUESTION_ID": "Q1", "DATABASE": "A", "SCOPE": "PRODUCT", "KEY": "ncm", "CELLS": 1, "DATABASE_CODE": "1"},
{"QUESTION_ID": "Q2", "DATABASE": "B", "SCOPE": "PRODUCT", "KEY": "ncm", "CELLS": 1, "DATABASE_CODE": "2"},
]
items = build_work_items(questions, canonical_products={"A:1": "GTIN:7891234567895", "B:2": "GTIN:7891234567895"})
assert len(items) == 1 and items[0].cells_covered == 2
# ---------------- provider contract / structured output / budget ----------------
def test_structured_output_schema_and_baseline_provider():
from central.app.ai.provider import BaselineProvider, result_from_dict, SemanticOutputError
provider = BaselineProvider()
item = type("W", (), {"task_type": "INVALID_NCM", "dedup_key": "invalid_ncm:fp-a", "payload": {"description": "AGUA MINERAL NATURAL 500ML"}})()
candidates = [("22011000", "AGUAS MINERAIS NATURAIS", 0.75), ("22019000", "AGUAS ... OUTRAS", 0.25)]
result = provider.select(item, candidates, dataset_versions={"ncm": "sha-ncm"})
assert result.task_type == "INVALID_NCM" and result.selected_candidate == "22011000"
assert result.candidate_ids_seen == ["22011000", "22019000"]
assert result.model == "baseline-deterministic-v1" and result.prompt_version == "ai-semantic-v1"
assert result.dataset_versions and result.engine_version
as_dict = result.to_dict()
assert result_from_dict(as_dict).selected_candidate == "22011000"
# schema fail-closed: a provider answer outside the candidate list is invalid
try:
result_from_dict({**as_dict, "selected_candidate": "99999999"})
raise AssertionError("must reject")
except SemanticOutputError:
pass
# no candidates at all -> NONE, never invented
none_result = provider.select(item, [])
assert none_result.selected_candidate == "NONE"
def test_budget_guard_blocks_run_over_limit():
from central.app.ai.provider import BudgetGuard
guard = BudgetGuard(max_run_cost_usd=1.0, price_in_per_m=1.0, price_out_per_m=2.0)
plan = guard.estimate(items_in_tokens=600_000, items_out_tokens=300_000)
assert plan["estimated_cost"] > 0
assert guard.authorize(plan) is False # (600k*1 + 300k*2)/1M = 1.2 > 1.0
assert guard.authorize(guard.estimate(100_000, 50_000)) is True
# ---------------- deterministic candidates ----------------
def test_ncm_top_k_and_ex_tipi_candidates():
from central.app.ai.candidates import ncm_top_k, ex_tipi_candidates_for_ncm, cest_candidates_for_ncm
engine = _engine()
with Session(engine) as session:
_ncm_dataset(session, [("22011000", "AGUAS MINERAIS NATURAIS"), ("22019000", "AGUAS MINERAIS GASEIFICADAS OUTRAS"), ("08043000", "ABACAXIS FRESCOS")])
top = ncm_top_k(session, "AGUA MINERAL NATURAL", k=2)
assert [c[0] for c in top] == ["22011000", "22019000"]
dataset = Dataset(kind="TIPI_IPI", provider="RFB", sha256="b" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=2)
session.add(dataset); session.flush()
session.add(TipiEntry(dataset_id=dataset.id, ncm="33059000", ex=None, description="outros", ipi_rate=None, ipi_rate_text="NT", source_row=1))
session.add(TipiEntry(dataset_id=dataset.id, ncm="33059000", ex="01", description="preparacoes para alisar", ipi_rate=None, ipi_rate_text="NT", source_row=2))
cest_dataset = Dataset(kind="CEST", provider="CONFAZ", sha256="c" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add(cest_dataset); session.flush()
cest = CestEntry(dataset_id=cest_dataset.id, code="03.999.99", description="x", annex="II")
session.add(cest); session.flush()
session.add(CestNcmEntry(cest_entry_id=cest.id, ncm_code="22011000"))
session.commit()
assert [e.ex for e in ex_tipi_candidates_for_ncm(session, "33059000")] == ["01"]
assert [c.code for c in cest_candidates_for_ncm(session, "22011000")] == ["03.999.99"]
def test_kb_persist_roundtrip_models():
from central.app.models import KbProduct, KbKnowledge, KbObservation, TenantProductContext, AiWorkItem, AiRun
engine = _engine()
with Session(engine) as session:
p = KbProduct(fingerprint="f", normalized_description="D")
session.add(p); session.flush()
session.add(KbKnowledge(kb_product_id=p.id, knowledge_key="candidate_ncm", value="22011000", source="OFFICIAL_DATASET", confidence=1.0))
session.add(KbObservation(kb_product_id=p.id, tenant_id=1, company_key="C", erp_code="1", database_label="C", observed_fields={}))
session.add(TenantProductContext(tenant_id=1, company_key="C", erp_code="1", kb_product_id=p.id))
session.add(AiWorkItem(task_type="CEST", dedup_key="cest:1", payload={}, question_refs=[], cells_covered=1))
session.add(AiRun(provider="baseline", model="m", prompt_version="v1", input_tokens=1, output_tokens=1, cost_usd=0.0, status="DONE", results=[]))
session.commit()
assert session.query(KbProduct).count() == 1 and session.query(AiWorkItem).count() == 1
@@ -0,0 +1,18 @@
from central.app.operational_state import classify_product_state, summarize_operational_states
def test_twenty_thousand_products_keep_agent_healthy_with_safe_blocks():
states = [{"state": "COMPLETE"}] * 19900 + [{"state": "BLOCKED_MISSING_EVIDENCE"}] * 100
summary = summarize_operational_states(states)
assert summary["AUTONOMOUSLY_PROCESSABLE_PRODUCTS"] == 20000
assert summary["SAFE_BLOCKED"] == 100
assert summary["AGENT_HEALTH"] == "HEALTHY"
def test_new_evidence_changes_product_state_without_global_failure():
blocked = classify_product_state({"CODIGO": "P1"}, {"status": "INVALID_NCM"})
resolved = classify_product_state({"CODIGO": "P1"}, {"status": "EXACT_NCM"})
assert blocked["state"] == "BLOCKED_MISSING_EVIDENCE"
assert resolved["state"] == "COMPLETE"
@@ -0,0 +1,9 @@
def test_parser_extracts_item_cest_and_ncm_from_official_table_text():
from central.app.datasets.ba_st_mva import parse_page
records, rejected = parse_page("ITEM CEST NCM/SH DESCRIÇÃO\n3.3 03.003.00 2201.1 Água mineral 114%", 1)
assert records[0]["item"] == "3.3"
assert records[0]["cest"] == "03.003.00"
assert records[0]["ncm_sh"] == ["2201.1"]
assert not rejected
+68
View File
@@ -0,0 +1,68 @@
import pytest
from central.app.ai.batch_runner import run_batches
from central.app.ai.fake_provider import DeterministicFakeSemanticProvider
from central.app.ai.llm_provider import LlmEvalItem
def _item(work_item_id):
return LlmEvalItem(
work_item_id=work_item_id,
description=work_item_id,
normalized_description=work_item_id,
brand=None,
unit=None,
segment="test",
candidates=[("A", "candidate A", 1.0), ("B", "candidate B", 0.5)],
)
@pytest.mark.parametrize("batch_size", [5, 10, 20])
def test_run_batches_correlates_shuffled_answers_by_id(batch_size):
items = [_item(f"w{i}") for i in range(batch_size + 1)]
provider = DeterministicFakeSemanticProvider(shuffle=True)
result = run_batches(items, provider, batch_size=batch_size)
assert set(result.results_by_work_item_id) == {item.work_item_id for item in items}
assert all(result.results_by_work_item_id[item.work_item_id]["work_item_id"] == item.work_item_id for item in items)
assert result.errors == {}
assert result.provider_calls == 2
@pytest.mark.parametrize("scenario", ["MISSING_ITEM", "DUPLICATE_ITEM", "INVALID_JSON", "OUT_OF_CANDIDATE"])
def test_run_batches_preserves_failed_ids_for_contract_errors(scenario):
items = [_item("w1"), _item("w2")]
provider = DeterministicFakeSemanticProvider()
result = run_batches(items, provider, batch_size=2, scenario=scenario)
assert result.results_by_work_item_id == {}
assert set(result.errors) == {"w1", "w2"}
assert result.retries == 0
assert result.provider_calls == 1
@pytest.mark.parametrize("scenario", ["TIMEOUT", "429", "500"])
def test_run_batches_retries_transport_errors_and_preserves_failed_ids(scenario):
items = [_item("w1"), _item("w2")]
provider = DeterministicFakeSemanticProvider()
result = run_batches(items, provider, batch_size=2, max_retries=2, scenario=scenario)
assert result.results_by_work_item_id == {}
assert set(result.errors) == {"w1", "w2"}
assert result.provider_calls == 3
assert result.retries == 2
def test_run_batches_allows_partial_failure_by_item():
items = [_item("w1"), _item("w2"), _item("w3")]
provider = DeterministicFakeSemanticProvider(fail_work_item_ids={"w2"}, failure_scenario="TIMEOUT")
result = run_batches(items, provider, batch_size=1, max_retries=1)
assert set(result.results_by_work_item_id) == {"w1", "w3"}
assert set(result.errors) == {"w2"}
assert result.provider_calls == 4
assert result.retries == 1
@@ -0,0 +1,72 @@
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from central.app.blocked_products import list_blockers, resolve_product_blockers, retry_due, upsert_blocker
from central.app.models import Agent, Base, BlockedProduct, Installation, Tenant
from central.app.operational_state import blocker_fingerprint
def _session():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
session = Session(engine)
tenant = Tenant(name="tenant-blocked")
session.add(tenant)
session.flush()
return session, tenant
def test_same_blocker_is_upserted_not_duplicated():
session, tenant = _session()
payload = {"tenant": "t", "company": "c", "product_code": "P1", "field": "CFOP", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "reason_code": "MISSING_EVIDENCE", "input_fingerprint": "v1", "evidence_summary": {"source": "ERP"}}
first = upsert_blocker(session, tenant.id, payload)
second = upsert_blocker(session, tenant.id, payload)
assert first.id == second.id
assert session.query(BlockedProduct).count() == 1
def test_new_input_fingerprint_is_due_for_retry():
session, tenant = _session()
payload = {"tenant": "t", "company": "c", "product_code": "P1", "field": "CFOP", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "reason_code": "MISSING_EVIDENCE", "input_fingerprint": "v1", "evidence_summary": {}}
row = upsert_blocker(session, tenant.id, payload)
assert list_blockers(session, tenant.id, input_fingerprint="v2") == [row]
def test_product_re_evaluation_closes_previous_blocker():
session, tenant = _session()
row = upsert_blocker(session, tenant.id, {"company": "c", "product_code": "P1", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "v1"})
assert resolve_product_blockers(session, tenant.id, "P1") == 1
assert session.get(BlockedProduct, row.id).resolved_at is not None
def test_same_input_without_retry_schedule_is_cache_hit():
session, tenant = _session()
row = upsert_blocker(session, tenant.id, {"company": "c", "product_code": "P1", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "v1"})
assert retry_due(row, "v1") is False
assert retry_due(row, "v2") is True
def test_authenticated_installations_get_distinct_fingerprints_for_identical_blockers():
session, tenant = _session()
agent_a = Agent(tenant_id=tenant.id, name="agent-a", token_hash="agent-a".ljust(64, "x"))
agent_b = Agent(tenant_id=tenant.id, name="agent-b", token_hash="agent-b".ljust(64, "x"))
session.add_all([agent_a, agent_b])
session.flush()
installation_a = Installation(agent_id=agent_a.id, tenant_id=tenant.id, installation_id="00000000-0000-4000-8000-000000000201", display_name="Store A")
installation_b = Installation(agent_id=agent_b.id, tenant_id=tenant.id, installation_id="00000000-0000-4000-8000-000000000202", display_name="Store B")
session.add_all([installation_a, installation_b])
session.flush()
payload = {"tenant": "t", "company": "c", "product_code": "P1", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "v1"}
first = upsert_blocker(session, tenant.id, payload, installation_id=installation_a.id)
second = upsert_blocker(session, tenant.id, payload, installation_id=installation_b.id)
assert first.id != second.id
assert first.blocker_fingerprint != second.blocker_fingerprint
assert blocker_fingerprint("t", "c", "P1", "CFOP", "MISSING", "v1") == blocker_fingerprint("t", "c", "P1", "CFOP", "MISSING", "v1")
assert blocker_fingerprint("t", "c", "P1", "CFOP", "MISSING", "v1", installation_id=installation_a.id) != blocker_fingerprint("t", "c", "P1", "CFOP", "MISSING", "v1", installation_id=installation_b.id)
@@ -0,0 +1,311 @@
from fastapi.testclient import TestClient
import pytest
from sqlalchemy import select
from central.app.blocked_products import upsert_blocker
from central.app.blocker_resolution import BlockerResolutionUnavailable, resolve_blocker_with_context, validate_resolution_input
from central.app.main import create_app
from central.app.models import AdminUser, Agent, AuditEvent, BlockedProduct, FiscalContextEntry, Job, Installation, Tenant
from central.tests.test_control_plane import seed, setup_db
def _seed_blocker(session_factory):
agent_id, admin_id = seed(session_factory)
with session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
installation.company_code = "ACME-COMPANY"
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": "input-1",
"evidence_summary": {"source": "test"},
},
installation_id=installation.id,
)
return blocker.id, admin_id, installation.id
def test_validate_resolution_input_accepts_known_enum_values():
for key, value in (("entry_scope", "INTER"), ("product_role", "RESALE")):
normalized, error = validate_resolution_input(
{"scope": "PRODUCT", "scope_key": "P-001", "key": key, "value": value, "reason": "operator note"}
)
assert error is None
assert normalized == {
"scope": "PRODUCT",
"scope_key": "P-001",
"key": key,
"value": value,
"reason": "operator note",
}
def test_validate_resolution_input_rejects_unsafe_or_incomplete_values():
invalid_payloads = (
{"scope": "PRODUCT", "scope_key": "P-001", "key": "entry_scope", "value": "ARBITRARY", "reason": "note"},
{"scope": "PRODUCT", "scope_key": "P-001", "key": "not-a-key", "value": "x", "reason": "note"},
{"scope": "NCM", "scope_key": "22030000", "key": "entry_scope", "value": "INTER", "reason": "note"},
{"scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": ""},
{"scope": "PRODUCT", "key": "product_role", "value": "RESALE", "reason": "note"},
{"scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "note", "context": "free text"},
)
for payload in invalid_payloads:
normalized, error = validate_resolution_input(payload)
assert normalized is None
assert error
def test_resolve_blocker_with_context_persists_one_context_row_job_and_sanitized_audit():
session_factory = setup_db()
blocker_id, admin_id, installation_id = _seed_blocker(session_factory)
with session_factory() as session:
admin = session.get(AdminUser, admin_id)
result = resolve_blocker_with_context(
session,
blocker_id,
admin,
{
"scope": "PRODUCT",
"scope_key": "P-001",
"key": "product_role",
"value": "RESALE",
"reason": "reviewed token=secret-value",
},
request_ip="192.0.2.10",
)
entry = session.scalar(select(FiscalContextEntry).where(FiscalContextEntry.key == "product_role"))
job = session.get(Job, result["job_id"])
audit = session.get(AuditEvent, result["audit_id"])
assert result["blocker_id"] == blocker_id
assert entry.company_key == "ACME-COMPANY"
assert entry.scope == "PRODUCT"
assert entry.scope_key == "P-001"
assert entry.value == "RESALE"
assert entry.source == "OPERATOR"
assert job.agent_id == session.get(Installation, installation_id).agent_id
assert job.kind == "REPROCESS_PRODUCTS"
assert job.payload == {"codigos": ["P-001"]}
assert audit.action == "RESOLVE_BLOCKER_WITH_CONTEXT"
assert audit.target_type == "blocker"
assert audit.target_id == str(blocker_id)
assert audit.context["reprocess_job_id"] == job.id
assert audit.context["before"] is None
assert audit.context["after"]["value"] == "RESALE"
assert audit.context["request_ip"] == "192.0.2.10"
assert "secret-value" not in str(audit.context)
assert session.query(FiscalContextEntry).count() == 1
def test_legacy_blocker_without_installation_fails_closed_without_job_or_audit():
session_factory = setup_db()
blocker_id, admin_id, _ = _seed_blocker(session_factory)
with session_factory() as session:
blocker = session.get(BlockedProduct, blocker_id)
blocker.installation_id = None
session.commit()
admin = session.get(AdminUser, admin_id)
with pytest.raises(BlockerResolutionUnavailable, match="Blocker legado"):
resolve_blocker_with_context(session, blocker_id, admin, {"scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "reviewed"})
assert session.query(FiscalContextEntry).count() == 0
assert session.query(Job).count() == 0
assert session.query(AuditEvent).count() == 0
def test_resolution_rejects_company_key_over_database_limit_before_mutation():
session_factory = setup_db()
blocker_id, admin_id, installation_id = _seed_blocker(session_factory)
with session_factory() as session:
installation = session.get(Installation, installation_id)
installation.company_code = "C" * 65
session.commit()
admin = session.get(AdminUser, admin_id)
with pytest.raises(ValueError, match="company key.*64"):
resolve_blocker_with_context(
session,
blocker_id,
admin,
{"scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "reviewed"},
)
assert session.query(FiscalContextEntry).count() == 0
assert session.query(Job).count() == 0
assert session.query(AuditEvent).count() == 0
def test_cross_tenant_installation_pointer_fails_before_mutation():
session_factory = setup_db()
blocker_id, admin_id, _ = _seed_blocker(session_factory)
with session_factory() as session:
blocker = session.get(BlockedProduct, blocker_id)
other_tenant = Tenant(name="Other Client")
session.add(other_tenant)
session.flush()
other_agent = Agent(tenant_id=other_tenant.id, name="other-agent", token_hash="other-agent-token".ljust(64, "x"))
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", company_code="OTHER-COMPANY")
session.add(other_installation)
session.flush()
blocker.installation_id = other_installation.id
session.commit()
admin = session.get(AdminUser, admin_id)
with pytest.raises(LookupError, match="tenant"):
resolve_blocker_with_context(session, blocker_id, admin, {"scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "reviewed"})
assert session.query(FiscalContextEntry).count() == 0
assert session.query(Job).count() == 0
assert session.query(AuditEvent).count() == 0
def test_resolution_input_rejects_oversized_and_non_string_fields_cleanly():
base = {"scope": "PRODUCT", "scope_key": "P-001", "key": "service_cfop", "value": "5102", "reason": "reviewed"}
for field, value in (("value", "v" * 513), ("reason", "r" * 2001), ("reason", 123)):
payload = {**base, field: value}
normalized, error = validate_resolution_input(payload)
assert normalized is None
assert error
def test_resolution_input_rejects_multibyte_utf8_at_ascii_character_limits():
base = {"scope": "PRODUCT", "scope_key": "P-001", "key": "service_cfop", "value": "5102", "reason": "reviewed"}
for field, value in (("value", "é" * 512), ("reason", "é" * 2000)):
normalized, error = validate_resolution_input({**base, field: value})
assert normalized is None
assert error and "bytes" in error
def test_boundary_length_audit_keeps_required_fields_instead_of_truncating_context():
session_factory = setup_db()
blocker_id, admin_id, _ = _seed_blocker(session_factory)
with session_factory() as session:
admin = session.get(AdminUser, admin_id)
result = resolve_blocker_with_context(
session,
blocker_id,
admin,
{"scope": "PRODUCT", "scope_key": "P-001", "key": "service_cfop", "value": "v" * 512, "reason": "r" * 2000},
)
audit = session.get(AuditEvent, result["audit_id"])
required = {"admin_user_id", "tenant_id", "client", "company_key", "installation_id", "product_code", "blocker_id", "key", "scope", "scope_key", "value", "reason", "before", "after", "reprocess_job_id", "request_ip"}
assert required <= audit.context.keys()
assert "truncated" not in audit.context
def test_human_resolution_detail_requires_auth_csrf_and_renders_safe_form():
session_factory = setup_db()
blocker_id, _, _ = _seed_blocker(session_factory)
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
assert client.get(f"/blockers/{blocker_id}", follow_redirects=False).status_code == 303
client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
detail = client.get(f"/blockers/{blocker_id}")
assert detail.status_code == 200
assert "Salvar contexto e reprocessar" in detail.text
assert "Cliente" in detail.text and "Servidor" in detail.text and "Produto" in detail.text
assert "Evidência" in detail.text and "Valores aceitos" in detail.text
assert "data-confirm" not in detail.text
assert 'name="context"' not in detail.text
missing_csrf = client.post(
f"/blockers/{blocker_id}/resolve-context",
data={"scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "reviewed"},
)
assert missing_csrf.status_code == 403
success = client.post(
f"/blockers/{blocker_id}/resolve-context",
data={"csrf_token": client.cookies.get("gti_csrf"), "scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "reviewed"},
follow_redirects=False,
)
assert success.status_code == 303
assert success.headers["location"] == f"/blockers/{blocker_id}"
def test_human_and_json_resolution_routes_use_the_same_validated_workflow():
session_factory = setup_db()
blocker_id, _, _ = _seed_blocker(session_factory)
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
csrf = client.cookies.get("gti_csrf")
invalid = client.post(
f"/v1/blockers/{blocker_id}/resolve-human",
json={"csrf_token": csrf, "scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "free text", "reason": "reviewed"},
)
assert invalid.status_code == 422
api_result = client.post(
f"/v1/blockers/{blocker_id}/resolve-human",
json={"csrf_token": csrf, "scope": "PRODUCT", "scope_key": "P-001", "key": "entry_scope", "value": "INTER", "reason": "reviewed"},
)
assert api_result.status_code == 200
assert set(api_result.json()) == {"blocker_id", "job_id", "audit_id"}
unauthenticated = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None)).post(
f"/v1/blockers/{blocker_id}/resolve-human",
json={"scope": "PRODUCT", "scope_key": "P-001", "key": "entry_scope", "value": "INTER", "reason": "reviewed"},
)
assert unauthenticated.status_code == 401
def test_unbound_blocker_resolution_routes_return_conflict_but_missing_stays_not_found():
session_factory = setup_db()
blocker_id, _, _ = _seed_blocker(session_factory)
with session_factory() as session:
session.get(BlockedProduct, blocker_id).installation_id = None
session.commit()
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
payload = {"csrf_token": client.cookies.get("gti_csrf"), "scope": "PRODUCT", "scope_key": "P-001", "key": "product_role", "value": "RESALE", "reason": "reviewed"}
for route in (f"/blockers/{blocker_id}/resolve-context", f"/v1/blockers/{blocker_id}/resolve-human"):
response = client.post(route, data=payload) if route.startswith("/blockers/") else client.post(route, json=payload)
assert response.status_code == 409
assert "sem vínculo comprovado" in response.text.lower()
missing = client.post("/v1/blockers/999999/resolve-human", json=payload)
assert missing.status_code == 404
def test_invalid_resolution_rerenders_submitted_values_and_persisted_context():
session_factory = setup_db()
blocker_id, _, _ = _seed_blocker(session_factory)
with session_factory() as session:
blocker = session.get(BlockedProduct, blocker_id)
session.add(FiscalContextEntry(tenant_id=blocker.tenant_id, company_key="ACME-COMPANY", scope="PRODUCT", scope_key="P-001", key="entry_scope", value="INTER", source="OPERATOR", reason="already saved"))
session.commit()
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
response = client.post(
f"/blockers/{blocker_id}/resolve-context",
data={"csrf_token": client.cookies.get("gti_csrf"), "key": "product_role", "value": "NOT_VALID", "scope": "COMPANY", "scope_key": "ACME", "reason": "submitted reason"},
)
assert response.status_code == 422
assert 'option value="product_role" selected' in response.text
assert 'name="value" required maxlength="512" value="NOT_VALID"' in response.text
assert 'option value="COMPANY" selected' in response.text
assert 'name="scope_key" value="ACME"' in response.text
assert "submitted reason" in response.text
assert "entry_scope" in response.text and "INTER" in response.text
@@ -0,0 +1,113 @@
from datetime import datetime, timezone
from functools import lru_cache
from pathlib import Path
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.datasets import bootstrap_v1_extra as parsers
from central.app.datasets.service import DatasetService
from central.app.models import Base
ROOT = Path(__file__).parents[2] / "sources" / "raw" / "bootstrap_v1"
FILES = {
"cfop": ROOT / "IT_2023_002_v2_10_Tabela_CFOP_Descrição.xlsx",
"fcp": ROOT / "Tabela_Alíquotas_FCP_por_UF_2025-02_07.xlsx",
"anp": ROOT / "Tabela_Codigo_Produto_ANP-20241118.xlsx",
"ncm_utrib": ROOT / "IT2024.001v2.40 - Tabela NCM 2022 com Utrib_Comércio Exterior_vigência 01.10.26.xlsx",
"biofuel": ROOT / "IT 2025.004 v.1.20 - Tabela de Índice.xlsx",
"credpres": ROOT / "cCredPres_2026-06-22.xlsx",
}
pytestmark = pytest.mark.skipif(not all(p.exists() for p in FILES.values()), reason="bootstrap_v1 extra source files not present locally")
@lru_cache(maxsize=None)
def _payload(name):
parse_fn = {"cfop": parsers.parse_cfop, "fcp": parsers.parse_fcp, "anp": parsers.parse_anp,
"ncm_utrib": parsers.parse_ncm_utrib, "biofuel": parsers.parse_rtc_biofuel_index,
"credpres": parsers.parse_rtc_cred_pres}[name]
return parse_fn(FILES[name].read_bytes())
def _engine():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return engine
def test_cfop_parses_full_table_with_official_fields():
payload = _payload("cfop")
assert len(payload.entries) >= 500
sample = payload.entries[0]
assert set(sample) >= {"code", "title", "description", "effective_from", "ind_nfe", "ind_comb"}
def test_fcp_covers_all_states_and_df():
payload = _payload("fcp")
ufs = {e["uf"] for e in payload.entries}
assert len(ufs) == 27 # 26 states + DF
assert "BA" in ufs
def test_anp_has_no_invented_descriptions():
payload = _payload("anp")
assert len(payload.entries) >= 900
assert all(set(e) == {"cprod_anp"} for e in payload.entries) # source truly has no description column
def test_ncm_utrib_covers_thousands_of_ncms():
payload = _payload("ncm_utrib")
assert len(payload.entries) >= 10000
assert all(len(e["ncm"]) == 8 for e in payload.entries[:50])
def test_biofuel_index_separates_current_from_historical():
payload = _payload("biofuel")
current = [e for e in payload.entries if not e["is_historical"]]
historical = [e for e in payload.entries if e["is_historical"]]
assert current and historical
assert all(e["p_bio_percent"] is not None for e in current)
def test_credpres_preserves_rich_official_fields_not_just_code_description():
payload = _payload("credpres")
sample = payload.entries[0]
assert sample["legal_basis"]
assert "d_ini_vig_cbs" in sample and "aliquota_cbs_text" in sample
@pytest.mark.parametrize("domain,method,lookup", [
("cfop", "import_cfop", lambda s: s.cfop_lookup(code="1101")),
("fcp", "import_fcp", lambda s: [s.fcp_for_uf("BA")]),
("anp", "import_anp", lambda s: [s.anp_code_exists("110203073")]),
("ncm_utrib", "import_ncm_utrib", lambda s: [s.ncm_utrib_for_ncm("01012100")]),
("biofuel", "import_rtc_biofuel_index", lambda s: s.rtc_biofuel_for_anp("320102001")),
("credpres", "import_rtc_cred_pres", lambda s: s.rtc_cred_pres(code="1")),
])
def test_import_idempotent_and_lookup_smoke(domain, method, lookup):
engine = _engine()
with Session(engine) as session:
service = DatasetService(session)
payload = _payload(domain)
first = getattr(service, method)(payload, f"file://{domain}", datetime.now(timezone.utc))
second = getattr(service, method)(payload, f"file://{domain}", datetime.now(timezone.utc))
assert first.id == second.id
assert second.status == "ACTIVE"
assert second.record_count == len(payload.entries)
result = lookup(service)
assert result
assert all(item is not None and item is not False for item in result) or result == [True]
def test_dataset_versions_includes_all_new_kinds():
engine = _engine()
with Session(engine) as session:
service = DatasetService(session)
versions = service.dataset_versions()
for key in ("cfop", "fcp", "anp", "ncm_utrib", "rtc_biofuel_index", "rtc_ccredpres"):
assert key in versions
assert versions[key] is None
service.import_cfop(_payload("cfop"), "file://cfop", datetime.now(timezone.utc))
assert service.dataset_versions()["cfop"] is not None
@@ -0,0 +1,153 @@
from datetime import date
from central.app import rtc_reference_data as ref
from central.app.cclasstrib_resolver import (ANNEX_DEFAULT_CLASS, annex_matches, compare_catalogs, local_catalog_hash, resolve_cclasstrib, verify_annex_class_map)
TODAY = date(2026, 9, 24)
NORMAL, SIMPLES = {"crt": "3"}, {"crt": "1"}
ON = {"rtc_enabled": True, "local_catalog_valid": True}
def local_from_official(skip=(), tweak=None):
rows = {}
for r in ref.OFFICIAL_CCLASSTRIB:
if r["code"] in skip:
continue
rows[r["code"]] = {"cst": r["cst"], "tipo_aliquota": r["tipo_aliquota"], "p_red_ibs": r["p_red_ibs"], "p_red_cbs": r["p_red_cbs"], "ind_nfe": r["ind_nfe"], "ind_nfce": r["ind_nfce"], "start": r["start"], "end": r["end"]}
for code, change in (tweak or {}).items():
rows[code] = {**rows[code], **change}
return rows
def product(ncm, valid=True, code="1"):
return {"CODIGO": code, "CLASSIFICACAO_FISCAL": ncm, "ncm_valid": valid}
def resolve(ncm, company=NORMAL, ctx=ON, local=None, valid=True):
return resolve_cclasstrib(product(ncm, valid), company, ctx, local if local is not None else local_from_official(), today=TODAY)
def test_annex_default_class_map_is_verified_against_the_official_table():
assert verify_annex_class_map() == {annex: [] for annex in ANNEX_DEFAULT_CLASS}
def test_reference_data_carries_provenance_and_the_expected_shape():
assert len(ref.OFFICIAL_CCLASSTRIB) == 164 and ref.OFFICIAL_CATALOG["sha256"] and ref.LC214_SOURCE["sha256"] and ref.LC214_SOURCE["accessed_at"]
assert {"I", "VII", "VIII", "IX", "XV"} <= set(ref.LC214_ANNEXES) and ref.LC214_ANNEXES["XIV"]["revoked"] is True
assert len(ref.LC214_ANNEXES["I"]["items"]) == 26 and len(ref.LC214_ANNEXES["XV"]["items"]) == 6
def test_rtc_off_is_not_applicable_and_never_a_candidate():
r = resolve("10063021", ctx={"rtc_enabled": False, "local_catalog_valid": True})
assert r.status == "NOT_APPLICABLE" and r.rule_id == "RTC-R0" and not r.auto_apply_candidate
def test_simples_is_insufficient_until_the_official_orientation_exists():
r = resolve("10063021", company=SIMPLES)
assert r.status == "INSUFFICIENT_EVIDENCE" and r.rule_id == "RTC-R1" and not r.auto_apply_candidate and r.source_references[-1]["document"].startswith("Ato Conjunto RFB/CGIBS") and r.rule_class == "BLOCKED_BY_EVIDENCE"
assert resolve("10063021", company={"crt": None}).reason == "COMPANY_REGIME_UNKNOWN"
def test_invalid_or_unconfirmed_ncm_is_insufficient():
assert resolve("1234", valid=True).reason == "NO_VALID_NCM"
assert resolve("10063021", valid=False).reason == "NO_VALID_NCM"
def test_clean_annex_item_resolves_to_the_annex_default_class_with_evidence():
r = resolve("10063021") # rice: Anexo I item 1 (subposicao 1006.30)
assert r.status == "RESOLVED_WITH_EXPLICIT_RULE" and r.resolved_cclasstrib == "200003" and r.resolved_cst_ibscbs == "200" and r.rule_id == "RTC-R3" and r.auto_apply_candidate
assert r.evidence[0]["annex"] == "I" and r.evidence[0]["clean"] is True and r.confidence_type == "ANNEX_TEXT_MATCH"
assert r.source_references[0]["sha256"] == ref.LC214_SOURCE["sha256"] and r.inputs_used["ncm"] == "10063021"
def test_conditional_annex_item_is_ambiguous_not_guessed():
milk = next(i for i in ref.LC214_ANNEXES["I"]["items"] if not i["clean"] and i["prefixes"])
ncm = milk["prefixes"][0].ljust(8, "0")
r = resolve(ncm)
assert r.status == "AMBIGUOUS" and r.rule_id == "RTC-A1" and not r.auto_apply_candidate and r.evidence[0]["clean"] is False
def test_annex_without_a_unique_default_class_is_ambiguous():
item = next(i for i in ref.LC214_ANNEXES["IV"]["items"] if i["clean"]) # medical devices: 60% class vs zero-rated public-buyer class
r = resolve(item["prefixes"][0].ljust(8, "1"))
assert r.status == "AMBIGUOUS" and "NEEDS_OPERATION_OR_CONDITION" in r.reason
def test_revoked_annex_only_is_ambiguous():
prefix = next(p for i in ref.LC214_ANNEXES["XIV"]["items"] for p in i["prefixes"] if len(p) == 8 and not annex_matches_other_than_xiv(p))
assert resolve(prefix).rule_id == "RTC-A1" and resolve(prefix).reason == "LISTED_ONLY_IN_REVOKED_ANNEX"
def annex_matches_other_than_xiv(prefix):
return [h for h in annex_matches(prefix) if h["annex"] != "XIV"]
def test_nature_based_chapters_are_ambiguous():
for ncm in ("03028990", "30049099", "27101259", "49019900", "87032100", "90183100"):
r = resolve(ncm)
assert r.status == "AMBIGUOUS" and r.rule_id in ("RTC-A3", "RTC-A1", "RTC-A2"), ncm
def test_residual_ncm_outside_every_annex_is_only_a_suggestion_and_never_a_write_candidate():
r = resolve("39235000") # plastic closures: no annex, no nature-based regime
assert r.status == "SUGGESTION_ONLY" and r.resolved_cclasstrib == "000001" and r.suggested_cclasstrib == "000001" and r.rule_id == "RTC-R4" and r.rule_class == "SUGGESTION_ONLY"
assert r.auto_apply_candidate is False and annex_matches("39235000") == [] and r.confidence_type == "EXCLUSION_OF_ALL_SPECIAL_REGIMES"
assert "not positive evidence" in r.evidence[0]["note"] and r.as_dict()["auto_apply_candidate"] is False
def test_only_auto_apply_safe_rules_can_authorize_a_write():
from central.app.cclasstrib_resolver import RULE_CLASSES
assert [rid for rid, cls in RULE_CLASSES.items() if cls == "AUTO_APPLY_SAFE"] == ["RTC-R3"]
assert RULE_CLASSES["RTC-R4"] == "SUGGESTION_ONLY" and RULE_CLASSES["RTC-R0"] == "VALIDATION_ONLY"
assert {c for c in RULE_CLASSES.values()} == {"AUTO_APPLY_SAFE", "VALIDATION_ONLY", "SUGGESTION_ONLY", "BLOCKED_BY_EVIDENCE"}
def test_regime_other_than_normal_never_reaches_a_positive_rule():
for company in ({"crt": "2"}, {"crt": "4"}, {"crt": "9"}, {"crt": ""}):
assert not resolve("10063021", company=company).auto_apply_candidate
def test_a_class_missing_or_different_in_the_local_catalog_is_never_proposed():
r = resolve("10063021", local=local_from_official(skip=("200003",)))
assert r.status == "INSUFFICIENT_EVIDENCE" and r.reason == "CLASS_NOT_IN_LOCAL_CATALOG" and not r.auto_apply_candidate
r2 = resolve("10063021", local=local_from_official(tweak={"200003": {"cst": "010"}}))
assert r2.reason == "LOCAL_OFFICIAL_MISMATCH" and not r2.auto_apply_candidate
r3 = resolve("10063021", local=local_from_official(tweak={"200003": {"ind_nfe": False, "ind_nfce": False}}))
assert r3.reason == "LOCAL_OFFICIAL_MISMATCH"
def test_invalid_local_catalog_blocks_every_proposal():
assert resolve("10063021", ctx={"rtc_enabled": True, "local_catalog_valid": False}).reason == "LOCAL_RTC_CATALOG_INVALID"
def test_catalog_comparison_classifies_every_code_and_flags_material_problems():
full = compare_catalogs(local_from_official(), TODAY)
assert full["counts"]["MATCH_SAFE"] == 161 and full["counts"]["EXPIRED"] == 3 and full["compatibility"] == "PARTIAL_LOCAL_OUTDATED" # 220001-3 ended on 2026-01-01
assert compare_catalogs(local_from_official(skip=("220001", "220002", "220003")), TODAY)["compatibility"] == "FULL"
outdated = compare_catalogs(local_from_official(skip=("000005", "200053")), TODAY)
assert outdated["counts"]["MISSING_LOCAL"] == 2 and outdated["compatibility"] == "PARTIAL_LOCAL_OUTDATED" and outdated["counts"]["MISMATCH"] == 0
local = local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
local["999999"] = {"cst": "000", "p_red_ibs": "0", "p_red_cbs": "0", "ind_nfe": True, "ind_nfce": True}
bad = compare_catalogs(local, TODAY)
assert bad["per_code"]["200003"] == "MISMATCH" and bad["per_code"]["999999"] == "MISSING_OFFICIAL" and bad["compatibility"] == "MATERIAL_MISMATCH"
def test_non_material_differences_are_classified_but_not_treated_as_a_mismatch():
earlier = compare_catalogs(local_from_official(tweak={"200003": {"start": "2025-05-05"}}), TODAY) # SGV imports an earlier start: both in force today => same emission effect
assert earlier["per_code"]["200003"] == "MATCH_SAFE" and earlier["details"]["200003"] == ["VIGENCY_START_DIFFERS_BOTH_IN_FORCE"]
assert compare_catalogs(local_from_official(tweak={"200003": {"start": "2027-01-01"}}), TODAY)["per_code"]["200003"] == "MISMATCH"
assert compare_catalogs(local_from_official(tweak={"200003": {"end": "2026-01-01"}}), TODAY)["per_code"]["200003"] == "MISMATCH"
local = local_from_official(tweak={"200003": {"end": "2027-12-31"}}) # a scheduled local end the official table does not have
c = compare_catalogs(local, TODAY)
assert c["per_code"]["200003"] == "MATCH_WITH_NON_MATERIAL_DIFFERENCE" and c["counts"]["MISMATCH"] == 0
assert resolve("10063021", local=local).status == "INSUFFICIENT_EVIDENCE" # auto-apply requires MATCH_SAFE: a non-material difference still blocks the WRITE
def test_local_catalog_hash_is_stable_and_sensitive():
a, b = local_from_official(), local_from_official(tweak={"200003": {"p_red_ibs": "60"}})
assert local_catalog_hash(a) == local_catalog_hash(local_from_official()) and local_catalog_hash(a) != local_catalog_hash(b)
def test_resolution_is_deterministic_and_auditable():
a, b = resolve("10063021"), resolve("10063021")
assert a.as_dict() == b.as_dict() and {"rule_id", "inputs_used", "source_references", "evidence", "reason"} <= set(a.as_dict())
@@ -0,0 +1,38 @@
from central.app.classification import Classifier
class _Repository:
def __init__(self, cest_entries):
self._cest_entries = cest_entries
def active_entries(self):
return [type("Entry", (), {"code": "19022000", "description": "Massas"})()]
def cest_for_ncm(self, code):
return self._cest_entries if code == "19022000" else []
def legacy_ncm(self, code):
return None
def dataset_versions(self):
return {"ncm": "n", "cest": "c"}
def test_stopwords_do_not_inflate_description_score():
# "RISOLE DE CAMARAO" vs an official text that only shares the stopword "DE"
# ("... preparadas de outro modo") must score 0, not a false-positive overlap.
cest = type("Cest", (), {
"code": "17.048.02",
"description": "Massas alimenticias recheadas (mesmo cozidas ou preparadas de outro modo)",
})()
result = Classifier(_Repository([cest])).classify({"current_ncm": "19022000", "description": "RISOLE DE CAMARAO"})
candidate = result["classification"]["cest_candidates"][0]
assert candidate["description_score"] == 0.0
assert result["cest_status"] == "CEST_DESCRIPTION_MISMATCH"
def test_genuine_token_overlap_still_scores_above_zero():
cest = type("Cest", (), {"code": "17.024.01", "description": "Queijo mucarela"})()
result = Classifier(_Repository([cest])).classify({"current_ncm": "19022000", "description": "BOLINHO DE QUEIJO"})
candidate = result["classification"]["cest_candidates"][0]
assert candidate["description_score"] > 0
@@ -0,0 +1,105 @@
"""Missão 4 / Bloco E.1-E.2: Hub-side plumbing for the CFOP_IPPT_WRITE_V1 capability - per-installation
mode (ADMIN-only, CSRF, audited, DEFAULT OFF), delivered inside the authenticated job, and the
append-only audit trail recorded when the job completes. Mirrors central/tests/test_rtc_security.py's
security posture for a separate, narrower capability."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.models import AuditEvent, CfopIpptWriteAudit, Installation
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_rtc_security import _become, _post
from central.tests.test_zero_touch_hub import H, OPERATIONAL, _client, finish, hb
def test_default_mode_is_disabled_everywhere():
client, engine = _client()
hb(client)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().cfop_ippt_write_mode == "DISABLED"
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["cfop_ippt_write_mode"] == "DISABLED"
body = client.get("/v1/agents/me/authorizations", headers=H).json()
assert body["cfop_ippt_write_mode"] == "DISABLED"
def test_a_forged_completion_payload_cannot_change_the_mode_or_fabricate_an_audit_row():
"""Flag OFF => zero jobs carry write permission, and nothing the Agent sends back can forge one -
the mode is read fresh from the Installation row, never from the job payload."""
client, engine = _client()
hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
forged = {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [],
"authorizations": {"cfop_ippt_write_mode": "ENABLED"},
"cfop_ippt_apply": {"applied": [{"product_code": "X", "field": "CFOP", "outcome": "APPLIED", "old_value": None, "new_value": "5102"}]}}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": forged}, headers=H)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().cfop_ippt_write_mode == "DISABLED"
# the audit row IS stored (it's just evidence of what the Agent reported for that job) but the
# mode itself never moved - a forged "ENABLED" in the payload changes nothing authoritative.
row = s.scalars(select(CfopIpptWriteAudit)).one()
assert row.outcome == "APPLIED" and row.field == "CFOP" and row.new_value == "5102"
def test_write_mode_is_restricted_to_gerenciador_admins_and_every_denial_is_audited():
client, agent_id, _ = _panel_client()
_become(client, "PADRAO")
assert _post(client, f"/agents/{agent_id}/cfop-ippt-write", mode="ENABLED", confirm="GRAVAR").status_code == 403
with client._session_factory() as s:
acts = [e.action for e in s.scalars(select(AuditEvent))]
assert "CFOP_IPPT_WRITE_MODE" not in acts
def test_admin_can_confirm_a_write_mode_and_it_is_audited():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
assert client.post(f"/agents/{agent_id}/cfop-ippt-write", data={"csrf_token": "bad", "mode": "ENABLED", "confirm": "GRAVAR"}, follow_redirects=False).status_code in (400, 403)
resp = _post(client, f"/agents/{agent_id}/cfop-ippt-write", mode="ENABLED", confirm="GRAVAR")
assert resp.status_code == 303
with client._session_factory() as s:
inst = s.scalars(select(Installation)).one()
assert inst.cfop_ippt_write_mode == "ENABLED"
acts = [e.action for e in s.scalars(select(AuditEvent))]
assert "CFOP_IPPT_WRITE_MODE" in acts
def test_enabling_without_typing_gravar_is_refused():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
resp = _post(client, f"/agents/{agent_id}/cfop-ippt-write", mode="ENABLED")
assert resp.status_code == 422
with client._session_factory() as s:
assert s.scalars(select(Installation)).one().cfop_ippt_write_mode == "DISABLED"
def test_disabling_never_requires_the_gravar_confirmation():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
assert _post(client, f"/agents/{agent_id}/cfop-ippt-write", mode="ENABLED", confirm="GRAVAR").status_code == 303
assert _post(client, f"/agents/{agent_id}/cfop-ippt-write", mode="DISABLED").status_code == 303
with client._session_factory() as s:
assert s.scalars(select(Installation)).one().cfop_ippt_write_mode == "DISABLED"
def test_invalid_mode_is_rejected():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
resp = _post(client, f"/agents/{agent_id}/cfop-ippt-write", mode="NOT_A_REAL_MODE", confirm="GRAVAR")
assert resp.status_code == 422
def test_record_cfop_ippt_write_audits_only_accepts_cfop_or_ippt_and_ignores_other_fields():
from central.app.cfop_ippt_audit import record_cfop_ippt_write_audits
client, agent_id, _ = _panel_client()
with client._session_factory() as s:
installation = s.scalars(select(Installation)).one()
from central.app.models import Agent
agent = s.scalars(select(Agent)).one()
n = record_cfop_ippt_write_audits(s, tenant_id=installation.tenant_id, installation=installation, agent=agent, job_id=1, cfop_ippt_apply={
"applied": [{"product_code": "A1", "field": "CFOP", "outcome": "APPLIED", "old_value": None, "new_value": "5102"}],
"aborted": [{"product_code": "A2", "field": "IPPT", "outcome": "ABORTED", "reason": "STALE_VALUE_CHANGED_SINCE_DECISION"},
{"product_code": "A3", "field": "CCLASSTRIB", "outcome": "APPLIED", "new_value": "999999"}], # not CFOP/IPPT: must be ignored
})
assert n == 2
rows = {r.product_code: r for r in s.scalars(select(CfopIpptWriteAudit))}
assert set(rows) == {"A1", "A2"}
assert rows["A1"].outcome == "APPLIED" and rows["A2"].reason == "STALE_VALUE_CHANGED_SINCE_DECISION"
@@ -0,0 +1,62 @@
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app import api as api_module
from central.app.main import create_app
from central.app.models import Base
HEADERS = {"Authorization": "Bearer test-token"}
PAYLOAD = {"current_ncm": "22030000", "description": "CERVEJA LATA"}
def make_client(monkeypatch, calls, engine_version="0.3.0", datasets=None):
versions = datasets if datasets is not None else {"ncm": "aaa"}
class FakeClassifier:
def __init__(self, repository): pass
def classify(self, product):
calls.append(product)
return {"status": "EXACT_NCM", "engine_version": api_module.ENGINE_VERSION, "decision_version": "x", "dataset_versions": dict(versions), "classification": {"ncm": product["current_ncm"]}}
monkeypatch.setattr(api_module, "Classifier", FakeClassifier)
monkeypatch.setattr(api_module, "ENGINE_VERSION", engine_version)
monkeypatch.setattr(api_module.DatasetService, "dataset_versions", lambda self: dict(versions))
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return TestClient(create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")), versions
def test_cache_hit_does_not_run_the_classifier(monkeypatch):
calls = []
client, _ = make_client(monkeypatch, calls)
first = client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()
assert first["provenance"]["cache_hit"] is False and len(calls) == 1
for _ in range(3):
again = client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()
assert again["provenance"]["cache_hit"] is True
assert {k: v for k, v in again.items() if k != "provenance"} == {k: v for k, v in first.items() if k != "provenance"} # same fiscal result
assert len(calls) == 1 # CLASSIFIER_EXECUTIONS_ON_HIT = 0
def test_batch_hits_skip_the_classifier_and_new_inputs_miss(monkeypatch):
calls = []
client, _ = make_client(monkeypatch, calls)
products = [PAYLOAD, {"current_ncm": "19022000", "description": "MASSA"}]
client.post("/v1/classify/batch", json={"products": products}, headers=HEADERS)
assert len(calls) == 2
res = client.post("/v1/classify/batch", json={"products": products + [{"current_ncm": "20099000", "description": "SUCO"}]}, headers=HEADERS).json()["results"]
assert [r["provenance"]["cache_hit"] for r in res] == [True, True, False] and len(calls) == 3
def test_engine_or_dataset_version_change_invalidates_naturally(monkeypatch):
calls = []
client, versions = make_client(monkeypatch, calls)
client.post("/v1/classify", json=PAYLOAD, headers=HEADERS)
versions["ncm"] = "bbb" # a new ACTIVE dataset version changes dataset_versions()
assert client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()["provenance"]["cache_hit"] is False
monkeypatch.setattr(api_module, "ENGINE_VERSION", "0.4.0")
assert client.post("/v1/classify", json=PAYLOAD, headers=HEADERS).json()["provenance"]["cache_hit"] is False
assert len(calls) == 3
@@ -0,0 +1,31 @@
from types import SimpleNamespace
from central.app.classification import classify_tipi_rate
def _entry(ex=None, ipi_rate=None, ipi_rate_text=None):
return SimpleNamespace(ex=ex, ipi_rate=ipi_rate, ipi_rate_text=ipi_rate_text)
def test_ex_specific():
assert classify_tipi_rate(_entry(ex="01")) == "EX_SPECIFIC"
def test_source_blank():
assert classify_tipi_rate(_entry(ipi_rate_text=None)) == "SOURCE_BLANK"
def test_nt_or_non_taxed():
assert classify_tipi_rate(_entry(ipi_rate_text="NT")) == "NT_OR_NON_TAXED"
def test_explicit_zero_text():
assert classify_tipi_rate(_entry(ipi_rate_text="0,00")) == "EXPLICIT_ZERO"
def test_explicit_zero_numeric():
assert classify_tipi_rate(_entry(ipi_rate=0, ipi_rate_text="0")) == "EXPLICIT_ZERO"
def test_other_unrecognized_marker():
assert classify_tipi_rate(_entry(ipi_rate_text="ISENTO")) == "OTHER"
+87
View File
@@ -0,0 +1,87 @@
"""Client mirror from GTI Gerenciador (central/app/client_sync.py, web.py's
/agents/new). GTI Gerenciador is the single source of truth: Tenant rows
with source='GTI_GERENCIADOR' are upserted from it; source='LOCAL' rows
(pre-existing dev/test tenants) are never touched or offered."""
import dataclasses
import pytest
from sqlalchemy import select
from central.app import client_sync, config
from central.app.models import Tenant
from central.tests.test_control_plane import setup_db
CLIENTS = [
{"external_client_id": 1, "code": "000001", "display_name": "Mercado Ativo", "active": True},
{"external_client_id": 2, "code": "000002", "display_name": "Loja Inativa", "active": False},
]
@pytest.fixture
def factory():
return setup_db()
def test_sync_creates_tenants_from_gerenciador(factory):
with factory() as session:
result = client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
assert result.created == 2 and result.updated == 0 and result.fetched == 2
assert result.active == 1 and result.inactive == 1
rows = {row.external_client_id: row for row in session.scalars(select(Tenant)).all()}
assert rows[1].name == "Mercado Ativo" and rows[1].source == "GTI_GERENCIADOR" and rows[1].active is True
assert rows[2].active is False and rows[2].external_code == "000002"
def test_sync_is_idempotent_and_updates_changed_fields(factory):
with factory() as session:
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
result = client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
assert result.created == 0 and result.unchanged == 2
renamed = [{**CLIENTS[0], "display_name": "Mercado Ativo Renomeado"}, CLIENTS[1]]
result2 = client_sync.sync_from_gerenciador(session, renamed)
session.commit()
assert result2.updated == 1 and result2.unchanged == 1
row = session.scalar(select(Tenant).where(Tenant.external_client_id == 1))
assert row.name == "Mercado Ativo Renomeado"
def test_sync_never_touches_local_tenants(factory):
with factory() as session:
session.add(Tenant(name="Acme", source="LOCAL"))
session.commit()
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
local = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
assert local.source == "LOCAL" and local.external_client_id is None
def test_sync_disambiguates_a_name_collision_with_an_existing_tenant(factory):
with factory() as session:
session.add(Tenant(name="Mercado Ativo", source="LOCAL"))
session.commit()
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
new_row = session.scalar(select(Tenant).where(Tenant.external_client_id == 1))
assert new_row.name == "Mercado Ativo (000001)" # the LOCAL tenant keeps the plain name
def test_deactivating_a_client_in_gerenciador_is_reflected_without_deleting_history(factory):
with factory() as session:
client_sync.sync_from_gerenciador(session, CLIENTS)
session.commit()
now_inactive = [{**CLIENTS[0], "active": False}, CLIENTS[1]]
client_sync.sync_from_gerenciador(session, now_inactive)
session.commit()
row = session.scalar(select(Tenant).where(Tenant.external_client_id == 1))
assert row.active is False # row (and every FK pointing at it) is kept — never deleted
def test_disabled_without_configured_url_or_token(monkeypatch):
monkeypatch.setattr(config, "settings", dataclasses.replace(config.settings, gerenciador_internal_url="", fiscal_internal_api_token=""))
assert client_sync.enabled() is False
with pytest.raises(client_sync.ClientSyncUnavailable):
client_sync.fetch_from_gerenciador()
@@ -0,0 +1,13 @@
def test_company_session_is_company_first_review_only_and_has_ipi_impact(tmp_path):
from scripts.run_company_decision_session import build_session
session = build_session(tmp_path, "docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json") if (tmp_path / "docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json").exists() else None
# Use a small local fixture because the production artifact is intentionally large.
if session is None:
(tmp_path / "docs/runs/multi_db").mkdir(parents=True)
(tmp_path / "docs").mkdir(exist_ok=True)
(tmp_path / "docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json").write_text('{"questions":[{"QUESTION_ID":"Q1","DATABASE":"A","SCOPE":"COMPANY","KEY":"entry_scope","VALID_OPTIONS":["INTRA","INTER","MIXED_OR_DYNAMIC"],"QUESTION":"Escopo?"}]}', encoding="utf-8")
(tmp_path / "docs/FISCAL_COVERAGE_FULL_V3.json").write_text('{"total_products":1,"ipi":{"numeric":1,"non_numeric":0,"ambiguous":0}}', encoding="utf-8")
session = build_session(tmp_path)
assert all(question["scope"] == "COMPANY" for question in session["questions"])
assert session["persisted"] is False and session["firebird_writes"] == 0
+11
View File
@@ -0,0 +1,11 @@
import importlib
def test_database_url_encodes_password_from_component_environment(monkeypatch):
monkeypatch.delenv("DATABASE_URL", raising=False)
monkeypatch.setenv("POSTGRES_PASSWORD", "a@b/c")
import central.app.config as config
module = importlib.reload(config)
assert module.settings.database_url == "postgresql+psycopg://gti:a%40b%2Fc@postgres:5432/gti_fiscal"
@@ -0,0 +1,45 @@
import uuid
from central.scripts.generate_control_panel_fixture import generate_fixture
from central.app.models import AgentEvent, AgentSnapshot, BlockedProduct, Installation, Job
from central.tests.test_control_plane import setup_db
def test_fixture_creates_one_hundred_varied_installations_and_events():
session_factory = setup_db()
with session_factory() as session:
result = generate_fixture(session, agent_count=100, seed=11)
assert result == {"agents": 100, "events": 500, "blockers": 51, "jobs": 100}
assert session.query(Installation).count() == 100
assert session.query(AgentEvent).count() == 500
assert session.query(Job).count() == 100
snapshots = session.query(AgentSnapshot).all()
assert {row.agent_health for row in snapshots} == {"ONLINE", "DEGRADED", "OFFLINE", "FAILED", "UNKNOWN"}
assert {row.version_drift for row in snapshots} == {"CURRENT", "OUTDATED", "UNKNOWN"}
assert {row.installation_id for row in session.query(Installation).all()} == {str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fixture-{index}")) for index in range(100)}
assert all(row.fiscally_complete + row.safe_blocked + row.failed_products == row.total_products for row in snapshots)
assert all(row.coverage_percent == round(row.fiscally_complete * 100 / row.total_products, 1) for row in snapshots)
blockers = session.query(BlockedProduct).all()
assert len({row.blocker_fingerprint for row in blockers}) == len(blockers)
for snapshot in snapshots:
product_codes = {row.product_code for row in blockers if row.installation_id == snapshot.installation_id and row.resolved_at is None}
assert snapshot.safe_blocked == len(product_codes)
repeated_product = next(row for row in blockers if row.product_code == "P000000")
same_product_blockers = [row for row in blockers if row.tenant_id == repeated_product.tenant_id and row.installation_id == repeated_product.installation_id and row.product_code == repeated_product.product_code]
assert len(same_product_blockers) >= 2
assert all(row.resolved_at is None for row in same_product_blockers)
assert all(row.tenant_id == session.query(Installation).filter_by(id=row.installation_id).one().tenant_id for row in blockers)
failed_jobs = session.query(Job).filter_by(status="FAILED").all()
assert failed_jobs and all(job.started_at is not None and job.finished_at is not None and job.finished_at >= job.started_at for job in failed_jobs)
linked_events = session.query(AgentEvent).filter(AgentEvent.job_id.is_not(None), AgentEvent.blocker_id.is_not(None)).all()
assert linked_events
for event in linked_events:
job = session.get(Job, event.job_id)
blocker = session.get(BlockedProduct, event.blocker_id)
installation = session.query(Installation).filter_by(id=blocker.installation_id).one()
assert event.installation_id == installation.id == session.query(Installation).filter_by(agent_id=job.agent_id).one().id
assert event.tenant_id == blocker.tenant_id == installation.tenant_id
assert event.product_code == blocker.product_code
assert all("password" not in event.message.lower() and "token" not in event.message.lower() for event in session.query(AgentEvent).all())
@@ -0,0 +1,785 @@
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&amp;health=ONLINE"' in home
assert 'href="/blockers?scope=active&amp;status=OPEN&amp;field=NCM"' in home
assert 'href="/jobs?scope=active&amp;status=FAILED"' in home
assert 'href="/agents?scope=active&amp;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&amp;status=FAILED&amp;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
# ---------------------------------------------------------------- RTC auto-apply authorization (central, default OFF)
def _installation(client, agent_id):
from central.app.models import Installation as Inst
with client._session_factory() as session:
row = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
return row.rtc_write_mode
def test_rtc_write_mode_is_disabled_by_default_and_needs_csrf_valid_mode_and_a_typed_confirmation_for_automatic():
client, agent_id, _ = _panel_client()
assert _installation(client, agent_id) == "DISABLED"
csrf = client.cookies.get("gti_csrf")
url = f"/agents/{agent_id}/rtc-mode"
assert client.post(url, data={"csrf_token": "wrong", "mode": "DRY_RUN"}).status_code == 403
assert client.post(url, data={"csrf_token": csrf, "mode": "ENABLED"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "mode": "ENABLED", "confirm": "sim"}).status_code == 422
assert client.post(url, data={"csrf_token": csrf, "mode": "bogus"}).status_code == 422
assert _installation(client, agent_id) == "DISABLED"
for mode, extra in (("DRY_RUN", {}), ("APPROVAL_REQUIRED", {}), ("ENABLED", {"confirm": "preencher"}), ("DISABLED", {})):
assert client.post(url, data={"csrf_token": csrf, "mode": mode, **extra}, follow_redirects=False).status_code == 303
assert _installation(client, agent_id) == mode
from central.app.models import AuditEvent
with client._session_factory() as session:
actions = [a.action for a in session.scalars(select(AuditEvent).order_by(AuditEvent.id)).all() if a.action.startswith("RTC_")]
assert actions == ["RTC_WRITE_MODE"] * 4
def _seed_rtc(client, agent_id, n=120):
from central.app.models import Installation as Inst, RtcCandidate, RtcWriteAudit
with client._session_factory() as session:
inst = session.scalar(select(Inst).where(Inst.agent_id == agent_id))
inst.rtc_write_mode = "APPROVAL_REQUIRED"
for i in range(n):
session.add(RtcCandidate(tenant_id=inst.tenant_id, installation_id=inst.id, product_code=f"C{i:04d}", description=f"PRODUTO {i}", ncm="10063021", proposed="200003", cst_ibscbs="200", rule_id="RTC-R3", basis="RTC-R3:ANNEX", status="PENDING"))
audit = RtcWriteAudit(tenant_id=inst.tenant_id, installation_id=inst.id, agent_id=agent_id, product_code="A1", field="CCLASSTRIB", outcome="APPLIED", old_value=None, new_value="200003", rule_id="RTC-R3", action="APPLY")
session.add(audit)
session.commit()
return audit.id
def test_rtc_control_page_paginates_candidates_on_the_server_and_shows_modes_counts_and_audit():
client, agent_id, _ = _panel_client()
_seed_rtc(client, agent_id)
page1 = client.get(f"/agents/{agent_id}/rtc").text
assert "Somente cClassTrib com regra fiscal determinística e catálogo validado pode ser preenchido automaticamente." in page1
for label in ("Desativada", "Dry-run", "Aprovação manual", "Automática"):
assert label in page1
assert "Pendentes (120)" in page1 and "C0000" in page1 and "C0049" in page1 and "C0050" not in page1 and "Página 1 de 3" in page1 and "Aplicar aprovados agora" in page1
page3 = client.get(f"/agents/{agent_id}/rtc?page=3").text
assert "C0119" in page3 and "C0099" not in page3 and "Página 3 de 3" in page3
assert "Reverter" in page3
def test_rtc_candidate_actions_approve_reject_apply_and_rollback_are_csrf_protected_and_scoped():
from central.app.models import Job, RtcCandidate, RtcWriteAudit
client, agent_id, _ = _panel_client()
audit_id = _seed_rtc(client, agent_id, n=10)
csrf = client.cookies.get("gti_csrf")
url = f"/agents/{agent_id}/rtc-candidates"
assert client.post(url, data={"csrf_token": "wrong", "action": "approve"}).status_code == 403
with client._session_factory() as session:
ids = [c.id for c in session.scalars(select(RtcCandidate).order_by(RtcCandidate.id)).all()]
assert client.post(url, content=f"csrf_token={csrf}&action=approve&ids={ids[0]}&ids={ids[1]}", headers={"content-type": "application/x-www-form-urlencoded"}, follow_redirects=False).status_code == 303
assert client.post(f"{url}?all=1", data={"csrf_token": csrf, "action": "approve"}, follow_redirects=False).status_code == 303
assert client.post(url, content=f"csrf_token={csrf}&action=reject&ids={ids[2]}", headers={"content-type": "application/x-www-form-urlencoded"}, follow_redirects=False).status_code == 303
with client._session_factory() as session:
counts = {c.product_code: c.status for c in session.scalars(select(RtcCandidate)).all()}
assert list(counts.values()).count("REJECTED") == 1 and list(counts.values()).count("APPROVED") == 9
jobs_before = len(session.scalars(select(Job)).all())
assert client.post(url, data={"csrf_token": csrf, "action": "apply_approved"}, follow_redirects=False).status_code == 303
with client._session_factory() as session:
job = session.scalars(select(Job).order_by(Job.id.desc())).first()
assert len(session.scalars(select(Job)).all()) == jobs_before + 1 and job.kind == "REPROCESS_PRODUCTS" and job.payload["reason"] == "RTC_APPLY_APPROVED" and len(job.payload["codigos"]) == 9
assert client.post(url, data={"csrf_token": csrf, "action": "nope"}).status_code == 422
roll = f"/agents/{agent_id}/rtc-rollback"
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id}).status_code == 422 # typed confirmation required
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id, "confirm": "reverter"}, follow_redirects=False).status_code == 303
assert client.post(roll, data={"csrf_token": csrf, "audit_id": audit_id, "confirm": "REVERTER"}).status_code == 409 # already requested: no duplicate
with client._session_factory() as session:
assert session.get(RtcWriteAudit, audit_id).rollback_state == "REQUESTED"
def test_apply_approved_is_refused_outside_the_approval_mode():
client, agent_id, _ = _panel_client()
csrf = client.cookies.get("gti_csrf")
assert client.post(f"/agents/{agent_id}/rtc-candidates", data={"csrf_token": csrf, "action": "apply_approved"}).status_code == 409
def test_panel_shows_rtc_products_summary_and_the_pending_message_only_when_rtc_is_on():
client, agent_id, _ = _panel_client()
_complete_sync(client, agent_id, {"rtc_enabled": False, "rtc_supported": True, "rtc_resolution": None})
off = client.get(f"/agents/{agent_id}").text
assert "Produtos RTC" not in off and "cClassTrib pendente" not in off and "Preenchimento automático" not in off
resolution = {"total": 521, "valid_existing": 0, "ready_to_apply": 280, "ambiguous": 215, "insufficient": 26, "catalog_compatibility": {"compatibility": "PARTIAL_LOCAL_OUTDATED"}}
_complete_sync(client, agent_id, {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True, "rtc_resolution": resolution})
on = client.get(f"/agents/{agent_id}").text
assert "Reforma Tributária ativa — cClassTrib pendente em 521 produtos" in on and "prontos para preenchimento 280" in on and "ambíguos/bloqueados 241" in on
assert "Preenchimento automático" in on and "Desativada" in on and f"/agents/{agent_id}/rtc" in on
for legacy in ("Escopo anterior", "Escopo ampliado"):
assert legacy not in on
_complete_sync(client, agent_id, {
"rtc_enabled": True,
"rtc_supported": True,
"rtc_catalog_valid": True,
"cclasstrib_applicability": {"state": "NOT_APPLICABLE", "basis": "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE"},
"rtc_resolution": {"total": 521, "valid_existing": 0, "ready_to_apply": 0, "insufficient": 521},
})
not_applicable = client.get(f"/agents/{agent_id}").text
assert "cClassTrib: Não utilizado neste cliente" in not_applicable
assert "cClassTrib pendente em 521 produtos" not in not_applicable
rtc_page = client.get(f"/agents/{agent_id}/rtc").text
assert "cClassTrib: Não utilizado neste cliente" in rtc_page
assert "cClassTrib pendente em 521 produtos" not in rtc_page
def test_rtc_control_page_shows_human_labels_and_a_catalog_column_never_raw_enums():
from central.app.panel_presentation import abort_reason_label, audit_outcome_label, candidate_status_label, catalog_state_label, blocker_reason_label, blocker_type_label, rtc_rule_label
assert candidate_status_label("APPROVED") == "Aprovado" and audit_outcome_label("ROLLED_BACK") == "Revertido"
assert rtc_rule_label("RTC-R4") == "Sugestão sem autorização de preenchimento (RTC-R4)"
assert blocker_reason_label("INSUFFICIENT_EVIDENCE") == "Evidência fiscal insuficiente"
assert blocker_type_label("CCLASSTRIB_CONFLICT") == "cClassTrib existente diverge da classificação proposta"
assert catalog_state_label("MISSING_LOCAL") == "Classe oficial ausente no catálogo local do SGV"
assert abort_reason_label("VALUE_ALREADY_SET_NEVER_OVERWRITTEN") == "cClassTrib existente diverge da classificação proposta"
client, agent_id, _ = _panel_client()
page = client.get(f"/agents/{agent_id}/rtc").text
for header in ("cClassTrib atual", "cClassTrib proposto", "Fundamento", "Catálogo", "Situação", "Descrição"):
assert header in page
for raw in ("PENDING</td>", "APPLIED</td>", "ABORTED"):
assert raw not in page
def test_rtc_no_r3_candidates_shows_simples_explanation_and_filtered_blocker_link():
from central.app.models import BlockedProduct, RtcCandidate, RtcWriteAudit
client, agent_id, _ = _panel_client()
with client._session_factory() as session:
installation = session.scalar(select(Installation).where(Installation.agent_id == agent_id))
installation.rtc_write_mode = "ENABLED"
session.add(RtcCandidate(tenant_id=installation.tenant_id, installation_id=installation.id, product_code="R4-SUGGESTION", description="Sugestão", ncm="10063021", proposed="000001", cst_ibscbs="000", rule_id="RTC-R4", basis="suggestion only", status="PENDING"))
session.add_all([
BlockedProduct(
tenant_id=installation.tenant_id,
installation_id=installation.id,
company="Acme",
product_code=f"RTC-{index:04d}",
description=f"Produto RTC {index}",
blocker_type="BLOCKED_FISCAL_DATA",
field="RTC",
reason_code="CCLASSTRIB_MISSING_UNRESOLVED",
evidence_summary={"reason": "RTC-R1:SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"},
input_fingerprint=f"rtc-input-{index}",
blocker_fingerprint=f"rtc-blocker-{index}",
first_seen_at=datetime.now(timezone.utc),
last_seen_at=datetime.now(timezone.utc),
)
for index in range(521)
])
session.add(Job(agent_id=agent_id, kind="REPROCESS_ALL", status="COMPLETED", payload={}, result_summary={
"fiscal_context": {
"rtc_enabled": True,
"rtc_supported": True,
"rtc_catalog_valid": True,
"crt": "1",
"legal_effective_context": {"cclasstrib_orientation": "PENDING_OFFICIAL_NT"},
"rtc_resolution": {
"total": 521, "valid_existing": 0, "ready_to_apply": 0, "suggestion_only": 0,
"ambiguous": 0, "insufficient": 521,
"by_reason": {"SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 521},
},
},
"rtc_auto_apply": {"mode": "ENABLED", "ready_total": 0, "preview": [], "write_attempts": 0, "write_successes": 0},
}, created_at=datetime.now(timezone.utc), finished_at=datetime.now(timezone.utc)))
session.commit()
page = client.get(f"/agents/{agent_id}/rtc")
assert page.status_code == 200
assert "Não há produtos elegíveis para preenchimento automático." in page.text
assert "521 produtos precisam de atenção fiscal, mas não podem ser preenchidos automaticamente pelas regras atuais." in page.text
assert "Esta empresa está no Simples Nacional." in page.text
assert "SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT" not in page.text
assert "R4-SUGGESTION" not in page.text
assert 'href="/blockers?scope=all&amp;agent_id=' + str(agent_id) + '&amp;field=RTC&amp;status=OPEN"' in page.text
assert "Nenhum candidato neste filtro." in page.text
blockers = client.get(f"/blockers?scope=all&agent_id={agent_id}&field=RTC&status=OPEN")
assert blockers.status_code == 200
assert "521 blockers" in blockers.text
with client._session_factory() as session:
assert session.scalar(select(Installation.rtc_write_mode).where(Installation.agent_id == agent_id)) == "ENABLED"
assert session.query(RtcCandidate).filter_by(installation_id=installation.id).count() == 1
assert session.query(RtcCandidate).filter_by(installation_id=installation.id, rule_id="RTC-R4").count() == 1
assert session.query(RtcWriteAudit).filter_by(installation_id=installation.id).count() == 0
+114
View File
@@ -0,0 +1,114 @@
from datetime import datetime, timedelta, timezone
import importlib
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.pool import StaticPool
from sqlalchemy.orm import sessionmaker
from central.app.human_auth import create_admin, create_session, hash_password, verify_password
from central.app.main import create_app
from central.app.models import Agent, AgentEvent, AgentSnapshot, Base, Installation, Tenant
from central.app.events import purge_expired_events, record_event, sanitize_event
from central.app.installation_identity import compute_agent_health
from types import SimpleNamespace
def setup_db():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return sessionmaker(bind=engine, expire_on_commit=False)
def seed(session_factory):
with session_factory() as session:
tenant = Tenant(name="Acme"); session.add(tenant); session.flush()
agent = Agent(tenant_id=tenant.id, name="agent", token_hash="x" * 64); session.add(agent); session.flush()
installation = Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id="11111111-1111-4111-8111-111111111111", display_name="Acme ERP", hostname="srv-01", last_seen_at=datetime.now(timezone.utc)); session.add(installation); session.flush()
session.add(AgentSnapshot(installation_id=installation.id, total_products=10, fiscally_complete=8, safe_blocked=2, blockers={"CFOP": 2}))
admin = create_admin(session, "operator", "a-secure-password")
return agent.id, admin.id
def test_password_and_event_redaction_and_retention():
assert verify_password("a-secure-password", hash_password("a-secure-password"))
message, context = sanitize_event("password=bad token=secret", {"password": "bad", "nested": {"api_key": "key"}})
assert "bad" not in message and context["password"] == "[REDACTED]" and context["nested"]["api_key"] == "[REDACTED]"
session_factory = setup_db()
with session_factory() as session:
agent_id, _ = seed(session_factory)
installation = session.query(Installation).first()
old = record_event(session, tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="INFO", event_type="HEARTBEAT", message="old", occurred_at=datetime.now(timezone.utc) - timedelta(days=100))
keep = record_event(session, tenant_id=installation.tenant_id, installation_id=installation.id, agent_id=agent_id, severity="CRITICAL", event_type="CONNECTION_ERROR", message="keep", occurred_at=datetime.now(timezone.utc) - timedelta(days=100), is_audit=True)
session.commit()
assert purge_expired_events(session, retention_days=90) == 1
assert session.get(AgentEvent, old.id) is None and session.get(AgentEvent, keep.id) is not None
def test_short_local_password_requires_explicit_setting(monkeypatch):
session_factory = setup_db()
with session_factory() as session:
monkeypatch.delenv("ALLOW_WEAK_DEV_PASSWORD", raising=False)
with pytest.raises(ValueError):
create_admin(session, "admin", "admin")
monkeypatch.setenv("ALLOW_WEAK_DEV_PASSWORD", "1")
admin = create_admin(session, "admin", "admin")
assert admin.username == "admin"
assert verify_password("admin", admin.password_hash)
def test_removed_weak_password_flag_is_fail_closed_after_import(monkeypatch):
monkeypatch.setenv("ALLOW_WEAK_DEV_PASSWORD", "1")
import central.app.config as config_module
import central.app.human_auth as human_auth_module
importlib.reload(config_module)
importlib.reload(human_auth_module)
session_factory = setup_db()
with session_factory() as session:
assert human_auth_module.create_admin(session, "admin", "admin").username == "admin"
monkeypatch.delenv("ALLOW_WEAK_DEV_PASSWORD", raising=False)
with pytest.raises(ValueError):
human_auth_module.create_admin(session, "admin2", "admin")
def test_production_admin_keeps_strong_password_minimum(monkeypatch):
session_factory = setup_db()
with session_factory() as session:
monkeypatch.setenv("APP_ENV", "production")
monkeypatch.setenv("ALLOW_WEAK_DEV_PASSWORD", "1")
with pytest.raises(ValueError):
create_admin(session, "admin", "admin")
def test_health_model_separates_safe_blocked_from_technical_failure():
now = datetime.now(timezone.utc)
recent = SimpleNamespace(last_seen_at=now, failed_products=0, last_job_status="COMPLETED")
blocked = SimpleNamespace(last_seen_at=now, failed_products=0, last_job_status="COMPLETED")
failed = SimpleNamespace(last_seen_at=now, failed_products=1, last_job_status="FAILED", agent_health="FAILED")
stale = SimpleNamespace(last_seen_at=now - timedelta(hours=2), failed_products=0, last_job_status="COMPLETED")
assert compute_agent_health(recent)[0] == "ONLINE"
assert compute_agent_health(blocked, supplemental=blocked)[0] == "ONLINE"
assert compute_agent_health(failed)[0] == "FAILED"
assert compute_agent_health(stale)[0] == "OFFLINE"
assert compute_agent_health(SimpleNamespace(last_seen_at=None))[0] == "UNKNOWN"
def test_human_login_and_dashboard_are_separate_from_agent_bearer():
session_factory = setup_db(); seed(session_factory)
client = TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None))
assert client.get("/", follow_redirects=False).status_code == 303
response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
assert response.status_code == 303
assert client.get("/v1/dashboard/summary").status_code == 200
assert client.get("/v1/jobs/global").status_code == 200
assert TestClient(create_app(database_probe=lambda: True, session_factory=session_factory, bootstrap_token=None)).get("/v1/dashboard/summary", headers={"Authorization": "Bearer x"}).status_code == 401
assert client.get("/v1/agents").json()["items"][0]["installation_id"]
csrf = client.cookies.get("gti_csrf")
action = client.post("/v1/agents/1/actions/reprocess", json={"confirm": True, "csrf_token": csrf, "kind": "REPROCESS_ALL"})
assert action.status_code == 200 and action.json()["status"] == "PENDING"
denied = client.post("/v1/agents/1/actions/reprocess", json={"confirm": True, "csrf_token": "wrong", "kind": "REPROCESS_ALL"})
assert denied.status_code == 403
assert client.post("/logout").status_code == 403
assert client.post("/logout", data={"csrf_token": csrf}, follow_redirects=False).status_code == 303
@@ -0,0 +1,16 @@
from central.app.deterministic_resolution import resolve_entry_scope, resolve_product_role, resolve_st_status
def test_product_role_requires_two_independent_strong_signals():
assert resolve_product_role({"recipe": True, "production": True})["value"] == "OWN_PRODUCTION"
assert resolve_product_role({"single_flag": True})["value"] is None
def test_st_distinguishes_subject_from_withheld():
assert resolve_st_status({"official_cest": True, "purchase_retained": True})["value"] == "ST_ALREADY_WITHHELD_IN_PURCHASE"
assert resolve_st_status({"official_cest": True, "purchase_retained": None})["status"] == "PENDING_ST_WITHHELD"
def test_entry_scope_is_dynamic_when_both_ufs_are_observed():
assert resolve_entry_scope(["BA", "SP"], "BA")["value"] == "MIXED_OR_DYNAMIC"
assert resolve_entry_scope(["BA"], "BA")["value"] == "INTRA"
@@ -0,0 +1,180 @@
import pytest
from central.app.dry_run_planner import (
build_field_proposals,
execute,
mva_evidence,
split_by_eligibility,
)
def _decision(**overrides):
base = {
"status": "EXACT_NCM",
"cest_status": "NO_CEST_MAPPING_FOUND",
"ipi_status": "NO_TIPI_MAPPING_FOUND",
"ba_st_mva_status": "NO_MVA_MAPPING_FOUND",
"classification": {
"ncm": "07069000",
"cest": [],
"ipi": None,
"ba_st_mva": None,
"ba_st_mva_candidates": [],
},
"dataset_versions": {"ncm": "n1", "cest": "c1", "tipi_ipi": "t1", "ba_st_mva": "m1"},
"engine_version": "0.1.0",
"decision_version": "mvp",
"request_fingerprint": "fp",
}
base.update(overrides)
return base
def test_ipi_resolved_numeric_current_unknown():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
proposals = build_field_proposals("000026", decision, {})
ipi = next(p for p in proposals if p["field"] == "IPI")
assert ipi["status"] == "CURRENT_UNKNOWN"
assert ipi["write_eligible"] is False
assert ipi["proposed_value"] == 3.25
def test_ipi_resolved_numeric_same_value():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
proposals = build_field_proposals("000026", decision, {"ipi": 3.25, "ex_tipi": None})
ipi = next(p for p in proposals if p["field"] == "IPI")
assert ipi["status"] == "SAME_VALUE"
assert ipi["write_eligible"] is False
def test_ipi_resolved_numeric_change_required():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
proposals = build_field_proposals("000026", decision, {"ipi": None, "ex_tipi": None})
ipi = next(p for p in proposals if p["field"] == "IPI")
assert ipi["status"] == "CHANGE_REQUIRED"
assert ipi["write_eligible"] is True
assert ipi["proposed_value"] == 3.25
def test_ipi_non_numeric_marker_never_proposes_a_write():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "NT", "rate_numeric": None, "rate_kind": "NON_NUMERIC", "rate_classification": "NT_OR_NON_TAXED", "ex": None}})
proposals = build_field_proposals("000004", decision, {"ipi": None, "ex_tipi": None})
ipi = next(p for p in proposals if p["field"] == "IPI")
assert ipi["status"] == "NON_NUMERIC_IPI_STATUS"
assert ipi["proposed_value"] is None
assert ipi["write_eligible"] is False
assert ipi["reason_code"] == "TIPI_RATE_NT_OR_NON_TAXED"
def test_ipi_resolved_without_concrete_rate_still_produces_a_row():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": None, "rate_numeric": None, "rate_kind": "BLANK", "rate_classification": "SOURCE_BLANK", "ex": None}})
proposals = build_field_proposals("000018", decision, {"ipi": 5, "ex_tipi": None})
ipi = next(p for p in proposals if p["field"] == "IPI")
assert ipi["status"] == "SOURCE_BLANK"
assert ipi["proposed_value"] is None
assert ipi["write_eligible"] is False
assert ipi["reason_code"] == "TIPI_RATE_SOURCE_BLANK"
def test_no_ex_tipi_row_when_no_ex_candidates():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}, "ipi_candidates": []})
proposals = build_field_proposals("000026", decision, {"ipi": None, "ex_tipi": None})
assert all(p["field"] != "EX_TIPI" for p in proposals)
def test_ex_tipi_concrete_single_candidate_change_required():
decision = _decision(
ipi_status="IPI_CANDIDATES",
classification={**_decision()["classification"], "ipi": None, "ipi_candidates": [{"ncm": "07069000", "ex": "01", "description": "specific", "rate": "5"}]},
)
proposals = build_field_proposals("000004", decision, {"ex_tipi": None})
ex = next(p for p in proposals if p["field"] == "EX_TIPI")
assert ex["status"] == "CHANGE_REQUIRED"
assert ex["proposed_value"] == "01"
assert ex["write_eligible"] is True
def test_ex_tipi_multiple_candidates_is_candidate_not_eligible():
decision = _decision(
ipi_status="IPI_CANDIDATES",
classification={**_decision()["classification"], "ipi": None, "ipi_candidates": [
{"ncm": "07069000", "ex": "01", "description": "a", "rate": "5"},
{"ncm": "07069000", "ex": "02", "description": "b", "rate": "7"},
]},
)
proposals = build_field_proposals("000004", decision, {"ex_tipi": None})
ex = next(p for p in proposals if p["field"] == "EX_TIPI")
assert ex["status"] == "CANDIDATE"
assert ex["proposed_value"] is None
assert ex["write_eligible"] is False
def test_cest_confirmed_change_required():
decision = _decision(cest_status="CEST_CONFIRMED", classification={**_decision()["classification"], "cest": ["17.087.02"]})
proposals = build_field_proposals("000018", decision, {"cest": ""})
cest = next(p for p in proposals if p["field"] == "CEST")
assert cest["status"] == "CHANGE_REQUIRED"
assert cest["write_eligible"] is True
assert cest["proposed_value"] == "17.087.02"
def test_cest_candidates_not_eligible():
decision = _decision(cest_status="CEST_CANDIDATES")
proposals = build_field_proposals("000024", decision, {"cest": ""})
cest = next(p for p in proposals if p["field"] == "CEST")
assert cest["status"] == "CANDIDATE"
assert cest["proposed_value"] is None
assert cest["write_eligible"] is False
def test_cest_description_mismatch_not_eligible():
decision = _decision(cest_status="CEST_DESCRIPTION_MISMATCH")
proposals = build_field_proposals("000018", decision, {"cest": ""})
cest = next(p for p in proposals if p["field"] == "CEST")
assert cest["status"] == "UNRESOLVED"
assert cest["reason_code"] == "CEST_DESCRIPTION_SCORE_BELOW_THRESHOLD"
assert cest["write_eligible"] is False
def test_legacy_ncm_review_no_invented_successor():
decision = _decision(status="LEGACY_NCM", classification={**_decision()["classification"], "ncm": "02071400"})
proposals = build_field_proposals("000016", decision, {"ipi": "3.25", "cest": ""})
assert len(proposals) == 1
row = proposals[0]
assert row["field"] == "CLASSIFICACAO_FISCAL"
assert row["status"] == "LEGACY_NCM_REVIEW"
assert row["proposed_value"] is None
assert row["write_eligible"] is False
def test_mva_never_produces_a_write_proposal():
decision = _decision(ba_st_mva_status="MVA_CANDIDATES", classification={**_decision()["classification"], "ba_st_mva_candidates": [{"item": "1"}]})
proposals = build_field_proposals("000024", decision, {"cest": ""})
assert all(p["field"] != "BA_ST_MVA" for p in proposals)
evidence = mva_evidence("000024", decision)
assert evidence["status"] == "MVA_CANDIDATES"
assert evidence["candidates"]
def test_split_by_eligibility():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
proposals = build_field_proposals("000026", decision, {})
split = split_by_eligibility(proposals)
assert split["blocked"] == proposals
assert split["eligible"] == []
def test_execute_refuses_non_eligible_proposal():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
proposals = build_field_proposals("000026", decision, {})
with pytest.raises(PermissionError):
execute(proposals)
def test_execute_never_writes_even_when_eligible():
decision = _decision(ipi_status="IPI_RESOLVED", classification={**_decision()["classification"], "ipi": {"rate": "3.25", "rate_numeric": 3.25, "rate_kind": "NUMERIC", "ex": None}})
proposals = build_field_proposals("000026", decision, {"ipi": "0"})
eligible_only = split_by_eligibility(proposals)["eligible"]
assert eligible_only
with pytest.raises(NotImplementedError):
execute(eligible_only)
@@ -0,0 +1,146 @@
"""Historical evidence -> KB: temporal validity, lineage independence, conflicts, promotion audit, idempotent import. No provider is ever contacted."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.enrichment import history, kb
from central.app.enrichment.resolver import resolve
from central.app.enrichment.quota import ProviderGateway
from central.app.models import KbKnowledge, KbProduct, KbPromotionAudit
from central.tests.test_product_enrichment_resolver import COSMOS, Fake, G1, G2, G3, env, gtin, seed # noqa: F401
G = "0" + G1 # 14-digit key
def staging(client, gtin13, ncm, *, master=True, years=None, nfe=0, desc="REFRIGERANTE COLA LATA 350ML", code="1"):
return {"client": client, "master": [{"code": code, "desc": desc, "gtin": gtin13, "codbarra": "", "ncm": ncm if master else "", "cest": ""}],
"nfce": [{"code": code, "year": y, "ncm": ncm, "cest": "", "n": n} for y, n in (years or {}).items()], "nfe": [{"code": code, "codbarra": gtin13, "ncm": ncm, "desc": desc, "n": nfe}] if nfe else [], "barcodes": []}
def run(session, stagings, dry=False):
obs = history.build_observations(stagings)
cur = history.official_sets(session)
return history.apply(session, obs, lambda v: v in cur, dry_run=dry)
def status(session, key=G):
p = session.scalar(select(KbProduct).where(KbProduct.gtin == key))
cur = history.official_sets(session)
return kb.evaluate(session, p, "ncm", lambda v: v in cur)
def test_single_client_multi_year_history_is_corroborated_never_verified(env):
r = run(env, [staging("A", G1, "22021000", years={2023: 300, 2024: 700, 2025: 900})])
assert r["by_status"] == {"CORROBORATED": 1} and r["WOULD_VERIFY"] == 0
assert env.scalars(select(KbPromotionAudit)).all() == []
def test_master_plus_documents_of_same_erp_are_one_family(env):
run(env, [staging("A", G1, "22021000", master=True, years={2025: 5})]) # few docs, one year: weak
assert status(env)["status"] == kb.OBSERVED and status(env)["families"] == []
def test_two_clients_corroborate_but_three_clients_alone_do_not_verify(env):
r = run(env, [staging(c, G1, "22021000", years={2024: 2}) for c in ("A", "B", "C")])
assert r["by_status"] == {"CORROBORATED": 1} and r["multi_client"] == 1 and r["WOULD_VERIFY"] == 0
def test_history_plus_independent_provider_verifies_and_is_audited(env):
run(env, [staging("A", G1, "22021000", years={2023: 50, 2024: 60})])
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
kb.add_evidence(env, p, "ncm", "22021000", "provider:oscbr", family="PROVIDER:oscbr")
ev = status(env)
assert ev["status"] == kb.VERIFIED_KB and set(ev["families"]) == {"PROVIDER:oscbr", "CLIENT_DATA"}
audit = env.scalars(select(KbPromotionAudit)).one()
assert audit.new_status == "VERIFIED_KB" and audit.rule_version == kb.RULE_VERSION and audit.official_dataset["sha256"] and audit.sources["documents"] == 110
status(env)
assert len(env.scalars(select(KbPromotionAudit)).all()) == 1 # no duplicate audit
def test_different_current_ncms_are_a_conflict_not_majority(env):
r = run(env, [staging("A", G1, "22021000", years={2024: 900, 2025: 900}), staging("B", G1, "19059090", years={2024: 5, 2025: 5}), staging("C", G1, "22021000", years={2024: 900})])
assert r["CONFLICTS"] == 1
res = resolve(env, ProviderGateway(env, []), gtin_raw=G1, description="REFRIGERANTE COLA LATA 350ML", current={}, allow_external=False)
assert res["state"] == "CONFLICT" and res["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "CONFLICT"
def test_expired_ncm_is_history_not_a_candidate(env):
run(env, [staging("A", G1, "11111111", years={2019: 900, 2020: 900})]) # expired in the fixture dataset
ev = status(env)
assert ev["value"] is None and ev["status"] == kb.OBSERVED
row = env.scalar(select(KbKnowledge).where(KbKnowledge.knowledge_key == "ncm"))
assert row.evidence["info"]["historical_state"] == "HISTORICAL_EXPIRED" and row.status == kb.STALE
def test_ncm_changed_over_time_prefers_current(env):
run(env, [staging("A", G1, "11111111", years={2019: 900, 2020: 900}), staging("A", G1, "22021000", master=False, years={2024: 50, 2025: 60})])
ev = status(env)
assert ev["value"] == "22021000" and ev["status"] == kb.CORROBORATED
def test_stale_ncm_with_equal_recency_does_not_count_as_stable(env):
run(env, [staging("A", G1, "22021000", master=False, years={2024: 50, 2025: 60}), staging("B", G1, "11111111", master=False, years={2025: 500})])
assert status(env)["status"] in (kb.OBSERVED,) # the old code is as recent: not stable, so not even corroborated
def test_import_is_idempotent_and_dry_run_writes_nothing(env):
st = [staging("A", G1, "22021000", years={2023: 300, 2024: 700}), staging("B", G3, "19059090", desc="BISCOITO RECHEADO 130G", code="9", years={2024: 30, 2025: 30})]
dry = run(env, st, dry=True)
assert env.scalars(select(KbProduct)).all() == []
a, b = run(env, st), run(env, st)
assert a["by_status"] == b["by_status"] == dry["by_status"]
assert len(env.scalars(select(KbKnowledge).where(KbKnowledge.knowledge_key == "ncm")).all()) == 2
row = env.scalar(select(KbKnowledge).where(KbKnowledge.knowledge_key == "ncm", KbKnowledge.value == "22021000"))
assert row.evidence["lineages"]["A"]["docs"] == 1000 # replaced, not doubled
def test_aliases_and_no_company_tax_data_stored(env):
run(env, [staging("A", G1, "22021000", years={2024: 30, 2025: 30}, desc="COCA COLA LT 350"), staging("B", G1, "22021000", master=False, desc="REFRIG COCA-COLA 350ML", code="2", years={2025: 5})])
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
aliases = {r.value for r in env.scalars(select(KbKnowledge).where(KbKnowledge.kb_product_id == p.id, KbKnowledge.knowledge_key == "alias"))}
assert len(aliases) >= 2
blob = repr([r.evidence for r in env.scalars(select(KbKnowledge))]).upper()
assert "CFOP" not in blob and "CSOSN" not in blob and "CRT" not in blob
def test_kb_hit_makes_zero_external_requests(env):
run(env, [staging("A", G1, "22021000", years={2023: 50, 2024: 60})])
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
kb.add_evidence(env, p, "ncm", "22021000", "provider:oscbr", family="PROVIDER:oscbr")
env.commit()
spy = Fake("cosmos", COSMOS(), True)
r = resolve(env, ProviderGateway(env, [spy]), gtin_raw=G1, description="REFRIGERANTE COLA LATA 350ML", current={})
assert r["state"] == "EXACT_GTIN_VERIFIED" and spy.calls == 0 and r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE"
def test_revalidation_marks_stale_review_and_keeps_knowledge(env):
run(env, [staging("A", G1, "22021000", years={2023: 50, 2024: 60})])
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
kb.add_evidence(env, p, "ncm", "22021000", "provider:oscbr", family="PROVIDER:oscbr")
assert status(env)["status"] == kb.VERIFIED_KB
out = kb.revalidate(env, lambda v: True)
assert out["stale"] == 0 and status(env)["status"] == kb.VERIFIED_KB # NCM still vigente: stays valid
out = kb.revalidate(env, lambda v: False) # the official dataset dropped it
row = env.scalar(select(KbKnowledge).where(KbKnowledge.knowledge_key == "ncm"))
assert out["stale"] == 1 and row.status == kb.STALE and row.evidence["info"]["stale_reason"] == "STALE_REVIEW_REQUIRED"
assert [a.new_status for a in env.scalars(select(KbPromotionAudit)).all()] == ["VERIFIED_KB", "STALE"]
def test_provider_disagreeing_with_verified_kb_creates_conflict_not_overwrite(env):
run(env, [staging("A", G1, "22021000", years={2023: 50, 2024: 60})])
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
kb.add_evidence(env, p, "ncm", "22021000", "provider:oscbr", family="PROVIDER:oscbr")
assert status(env)["status"] == kb.VERIFIED_KB
kb.add_evidence(env, p, "ncm", "19059090", "provider:cosmos", family="PROVIDER:cosmos")
ev = status(env)
assert ev["status"] == kb.CONFLICT and ev["value"] is None # write blocked until a human resolves it
def test_aliases_from_history_enable_name_only_match_and_generic_names_stay_ambiguous(env):
run(env, [staging("A", G1, "22021000", years={2023: 50, 2024: 60}, desc="COCA-COLA ORIGINAL LATA 350 ML")])
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
kb.add_evidence(env, p, "ncm", "22021000", "provider:oscbr", family="PROVIDER:oscbr")
env.commit()
r = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA COLA LATA 350ML", current={})
assert r["state"] == "EXACT_KB_NAME_VERIFIED" and r["fields"]["CLASSIFICACAO_FISCAL"]["value"] == "22021000"
g = resolve(env, ProviderGateway(env, []), gtin_raw="", description="REFRIGERANTE", current={})
assert g["state"] == "AMBIGUOUS" and all(f["status"] != "AUTO_FILL_SAFE" for f in g["fields"].values())
@@ -0,0 +1,141 @@
"""Visible status, re-evaluation (panel / Reprocessar / Sincronizar / KB decision), operator attestation, deterministic name-only match, RTC != Enrichment."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.enrichment import api as enrich_api, history, kb, status
from central.app.enrichment.quota import ProviderGateway
from central.app.enrichment.resolver import resolve
from central.app.jobs import create_job
from central.app.models import Agent, AuditEvent, Installation, KbKnowledge, KbProduct, KbPromotionAudit, ProductEnrichment
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_enrichment_history import G, run as seed_history, staging
from central.tests.test_product_enrichment_api import H, _second_agent, resolve as api_resolve, setup
from central.tests.test_product_enrichment_resolver import COSMOS, Fake, G1, env, gtin # noqa: F401
from central.tests.test_rtc_security import _become, _post
def _kb_conflict(env):
seed_history(env, [staging("RINCAO", G1, "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"), staging("UNIAO", G1, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")])
def test_attestation_resolves_a_conflict_with_audit_and_only_among_candidates(env):
_kb_conflict(env)
cur = history.official_sets(env)
p = env.scalar(select(KbProduct).where(KbProduct.gtin == G))
assert kb.evaluate(env, p, "ncm", lambda v: v in cur)["status"] == kb.CONFLICT
for bad, why in (("21069090", "NCM_IS_NOT_A_CANDIDATE_OF_THIS_GTIN"), ("22030000x", "NCM_IS_NOT_A_CANDIDATE_OF_THIS_GTIN")):
try:
kb.attest(env, G, bad, "Rafael", lambda v: v in cur)
assert False
except ValueError as error:
assert str(error) == why
res = kb.attest(env, G, "22021000", "Rafael", lambda v: v in cur, note="lata 350ml = refrigerante")
assert res["status"] == kb.VERIFIED_KB and res["value"] == "22021000" and set(res["families"]) == {"OPERATOR_ATTESTATION", "CLIENT_DATA"}
audit = env.scalars(select(KbPromotionAudit)).one()
assert audit.sources["operator"] == "Rafael" and audit.new_status == "VERIFIED_KB"
rejected = env.scalar(select(KbKnowledge).where(KbKnowledge.knowledge_key == "ncm", KbKnowledge.value == "19059090"))
assert rejected.status == kb.STALE and rejected.evidence["info"]["historical_state"] == "REJECTED_BY_OPERATOR"
def test_name_only_match_is_deterministic_and_conservative(env):
_kb_conflict(env)
cur = history.official_sets(env)
kb.attest(env, G, "22021000", "Rafael", lambda v: v in cur)
zero = gtin("789490070001")
seed_history(env, [staging("UNIAO", zero, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA ZERO LATA 350 ML", code="9")])
env.commit()
r = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA-COLA ORIGINAL LATA 350 ML", current={"CLASSIFICACAO_FISCAL": "00000000"})
assert r["state"] == "EXACT_KB_NAME_VERIFIED" and r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE" and r["fields"]["CLASSIFICACAO_FISCAL"]["value"] == "22021000"
ambiguous = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA COLA ZERO LATA 350 ML", current={})
assert ambiguous["fields"]["CLASSIFICACAO_FISCAL"]["status"] != "AUTO_FILL_SAFE" # ZERO is not a neutral extra word: no cross-match
generic = resolve(env, ProviderGateway(env, []), gtin_raw="", description="REFRIGERANTE", current={})
assert generic["state"] == "AMBIGUOUS"
assert not kb.alias_match(frozenset({"COCA", "COLA", "LATA", "350", "ML"}), frozenset({"COCA", "COLA", "ZERO", "LATA", "350", "ML"}))
assert kb.alias_match(frozenset({"COCA", "COLA", "LATA", "350", "ML"}), frozenset({"REFRIGERANTE", "COCA", "COLA", "LATA", "350", "ML"}))
def test_every_tracked_product_has_a_visible_status(monkeypatch):
client, engine = setup(monkeypatch)
out = api_resolve(client, "N1", "", "COCA-COLA ORIGINAL LATA 350 ML", {"CLASSIFICACAO_FISCAL": "00000000"})
with Session(engine) as s:
row = s.scalars(select(ProductEnrichment)).one()
assert row.ui_status == "EAN_AUSENTE" and row.reason_code == "NAME_ONLY_NO_VERIFIED_MATCH" and row.last_attempt_at is not None
assert out["fields"]["CLASSIFICACAO_FISCAL"]["technical_placeholder"] == "00000000"
generic = api_resolve(client, "N2", "", "REFRIGERANTE", {})
invalid = api_resolve(client, "N3", "7891000100104", "PRODUTO X", {})
with Session(engine) as s:
st = {r.product_code: r.ui_status for r in s.scalars(select(ProductEnrichment))}
assert st["N2"] == "NOME_AMBIGUO" and st["N3"] == "EAN_INVALIDO"
assert all(status.LABELS.get(v) for v in st.values())
from central.tests.test_product_enrichment_resolver import G1 as g
done = api_resolve(client, "N4", g, "REFRIGERANTE COLA LATA 350ML", {})
with Session(engine) as s:
assert s.scalars(select(ProductEnrichment).where(ProductEnrichment.product_code == "N4")).one().ui_status == "PRONTO_PARA_PREENCHER"
client.post("/v1/products/enrichment/applied", json={"product_code": "N4", "fields": [{"field": "CLASSIFICACAO_FISCAL", "outcome": "APPLIED", "old": "00000000", "new": "22021000"}]}, headers=H)
client.post("/v1/products/enrichment/applied", json={"product_code": "N1", "fields": [{"field": "CLASSIFICACAO_FISCAL", "outcome": "ABORTED", "reason": "STALE_CONFLICT"}]}, headers=H)
with Session(engine) as s:
st = {r.product_code: (r.ui_status, r.reason_code) for r in s.scalars(select(ProductEnrichment))}
assert st["N4"][0] == "ENRIQUECIDO" and st["N1"] == ("WRITE_FAILED", "STALE_CONFLICT")
def test_reprocess_sync_and_panel_flag_only_unresolved_products_for_reevaluation(monkeypatch):
client, engine = setup(monkeypatch)
api_resolve(client, "U1", "", "COCA-COLA ORIGINAL LATA 350 ML", {"CLASSIFICACAO_FISCAL": "00000000"})
api_resolve(client, "E1", G1[:0] or gtin("789100010010"), "REFRIGERANTE COLA LATA 350ML", {})
client.post("/v1/products/enrichment/applied", json={"product_code": "E1", "fields": [{"field": "CLASSIFICACAO_FISCAL", "outcome": "APPLIED", "old": None, "new": "22021000"}]}, headers=H)
assert client.get("/v1/products/enrichment/pending", headers=H).json() == {"codes": []}
with Session(engine) as s:
agent = s.scalars(select(Agent)).first()
for kind in ("REPROCESS_ALL", "SYNC_INCREMENTAL"):
create_job(s, agent, kind, {})
assert s.scalars(select(ProductEnrichment).where(ProductEnrichment.reevaluate.is_(True))).all()[0].product_code == "U1"
for r in s.scalars(select(ProductEnrichment)):
r.reevaluate = False
s.commit()
for j in s.scalars(select(__import__("central.app.models", fromlist=["Job"]).Job)):
j.status = "COMPLETED"
s.commit()
with Session(engine) as s:
create_job(s, s.scalars(select(Agent)).first(), "REPROCESS_PRODUCTS", {"codigos": ["U1"]})
assert client.get("/v1/products/enrichment/pending", headers=H).json() == {"codes": ["U1"]} # the enriched product E1 is never re-evaluated
api_resolve(client, "U1", "", "COCA-COLA ORIGINAL LATA 350 ML", {"CLASSIFICACAO_FISCAL": "00000000"})
assert client.get("/v1/products/enrichment/pending", headers=H).json() == {"codes": []} # consumed by the new evaluation
def test_panel_reevaluate_and_attest_routes_are_csrf_admin_and_audited(monkeypatch):
client, agent_id, _ = _panel_client()
with client._session_factory() as s:
seed_history(s, [staging("RINCAO", G1, "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"), staging("UNIAO", G1, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")])
from central.tests.test_product_enrichment_resolver import seed
seed(s)
i = s.scalars(select(Installation)).one()
i.enrichment_mode, i.features_configured = "ENABLED", True
s.commit()
_become(client, "PADRAO")
assert _post(client, "/kb/attest", gtin="0" + G1, ncm="22021000").status_code == 403
_become(client, "ADMIN")
assert client.post("/kb/attest", data={"csrf_token": "bad", "gtin": "0" + G1, "ncm": "22021000"}, follow_redirects=False).status_code in (400, 403)
assert _post(client, "/kb/attest", gtin="0" + G1, ncm="21069090").status_code == 422
assert _post(client, "/kb/attest", gtin="0" + G1, ncm="22021000").status_code == 303
assert _post(client, f"/agents/{agent_id}/enrichment/reevaluate", code="").status_code == 303
with client._session_factory() as s:
acts = [e.action for e in s.scalars(select(AuditEvent))]
assert "KB_NCM_ATTESTED" in acts and "KB_ATTEST_DENIED" in acts and "ENRICHMENT_REEVALUATE" in acts
page = client.get("/?kb_q=COCA COLA LATA 350 ML").text
assert "VERIFIED_KB" in page and "decidido por" in page
def test_rtc_mode_and_enrichment_mode_are_independent_switches_with_distinct_labels():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
assert _post(client, f"/agents/{agent_id}/features", enrichment_mode="DRY_RUN", corrector_enabled="1").status_code == 303
with client._session_factory() as s:
i = s.scalars(select(Installation)).one()
assert i.enrichment_mode == "DRY_RUN" and i.rtc_write_mode == "DISABLED"
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="DRY_RUN").status_code == 303
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="ENABLED", confirm="PREENCHER").status_code == 303
with client._session_factory() as s:
i = s.scalars(select(Installation)).one()
assert i.rtc_write_mode == "ENABLED" and i.enrichment_mode == "DRY_RUN" # turning cClassTrib to Automatic never touches the enrichment
page = client.get(f"/agents/{agent_id}").text
assert "NÃO é o preenchimento RTC" in page and "Enriquecimento de produtos novos" in page
@@ -0,0 +1,38 @@
from central.app.evidence_aggregation import Observation, aggregate_observations
from scripts.analyze_inbound_observations import summarize_rows
def test_two_suppliers_agree_without_conflict():
rows = [
Observation("P1", "origin", "0", "S1", "D1", "2026-01-01"),
Observation("P1", "origin", "0", "S2", "D2", "2026-02-01"),
]
result = aggregate_observations(rows)[("P1", "origin")]
assert result["observation_count"] == 2
assert result["supplier_count"] == 2
assert result["agreement_rate"] == 1.0
assert result["strong_evidence"] is True
assert result["conflict_count"] == 0
def test_supplier_disagreement_is_conflict_not_majority():
rows = [
Observation("P1", "origin", "0", "S1", "D1", "2026-01-01"),
Observation("P1", "origin", "0", "S2", "D2", "2026-02-01"),
Observation("P1", "origin", "1", "S3", "D3", "2026-03-01"),
]
result = aggregate_observations(rows)[("P1", "origin")]
assert result["conflict_count"] == 1
assert result["strong_evidence"] is False
assert result["value"] is None
def test_inbound_rows_become_supplier_observations_without_raw_identity():
result = summarize_rows([("P1", "S1", "2026-01-01", "12345678", "1102", "060")])
assert result["observation_count"] == 3
assert result["sources"] == ["SUPPLIER_OBSERVATION"]
@@ -0,0 +1,67 @@
import copy
import pytest
from central.app.ai.fake_provider import DeterministicFakeSemanticProvider
from central.app.ai.llm_provider import LlmAnswerError, LlmEvalItem
from central.app.ai.provider import ProviderContractError, ProviderTransportError
def _item(work_item_id="w1"):
return LlmEvalItem(
work_item_id=work_item_id,
description="AGUA MINERAL 500ML",
normalized_description="AGUA MINERAL 500ML",
brand=None,
unit="ML",
segment="mercado",
candidates=[("22011000", "AGUAS MINERAIS", 0.9), ("22019000", "OUTRAS AGUAS", 0.4)],
)
@pytest.mark.parametrize(
("scenario", "selected", "abstain"),
[
("CORRECT", "22011000", False),
("WRONG", "22019000", False),
("ABSTAIN", None, True),
],
)
def test_fake_provider_returns_deterministic_valid_answers(scenario, selected, abstain):
provider = DeterministicFakeSemanticProvider()
answers = provider.select_batch([_item()], scenario=scenario)
assert answers == provider.select_batch([_item()], scenario=scenario)
assert answers[0]["work_item_id"] == "w1"
assert answers[0]["selected_ncm"] == selected
assert answers[0]["abstain"] is abstain
assert provider.calls == 2
@pytest.mark.parametrize("scenario", ["INVALID_JSON", "MISSING_ITEM", "DUPLICATE_ITEM", "OUT_OF_CANDIDATE"])
def test_fake_provider_rejects_contract_scenarios_without_mutating_items(scenario):
provider = DeterministicFakeSemanticProvider()
items = [_item("w1"), _item("w2")]
original = copy.deepcopy(items)
with pytest.raises((LlmAnswerError, ProviderContractError)):
provider.select_batch(items, scenario=scenario)
assert items == original
assert provider.calls == 1
@pytest.mark.parametrize("scenario", ["TIMEOUT", "429", "500"])
def test_fake_provider_raises_typed_transport_errors(scenario):
provider = DeterministicFakeSemanticProvider()
with pytest.raises(ProviderTransportError):
provider.select_batch([_item()], scenario=scenario)
assert provider.calls == 1
def test_fake_provider_rejects_unknown_scenarios():
with pytest.raises(ValueError):
DeterministicFakeSemanticProvider().select_batch([_item()], scenario="UNKNOWN")
+349
View File
@@ -0,0 +1,349 @@
import importlib.util
import sys
from pathlib import Path
import pytest
from central.app.field_contract import (ALREADY, CHANGE, CONTRACT, MANUAL, NA, WRITE_FIELDS, Context, infer_role, resolve_product)
ROOT = Path(__file__).parents[2]
REFS = {"cfop_valid": {"5101", "5102", "5401", "5405", "1102", "1101", "1403", "2102"}, "csosn": {"101", "102", "103", "300", "400", "500"}, "cst_icms": {"00", "20", "40", "41", "60"},
"cst_pis": {"01", "49", "98"}, "cst_cofins": {"01", "49", "98"}, "origem": set("012345678"), "fcp_uf": {}, "cbs_rate_2026": None, "cclasstrib_count": 0}
CO1 = {"uf": "BA", "crt": "1", "contribuinte_ipi": "N", "company_code": "000001"}
CO3 = {"uf": "BA", "crt": "3", "contribuinte_ipi": "S", "company_code": "000001"}
def product(**kw):
p = {f: None for f in WRITE_FIELDS}
p.update({"CODIGO": "000001", "PRODUTO": "CERVEJA LATA", "CODGRUPO": "000019", "TIPO": "VENDA", "PROD_COZINHA": None, "PRODUTO_SERVICO": None, "CLASSIFICACAO_FISCAL": "22030000",
"CFOP": "5102", "CSOSN": "102", "CST": "000", "SITUACAO_TRIBUTARIA": "T", "IAT": "A", "IPPT": "T", "INCIDENCIA_PISCOFINS": "NENHUMA", "PISCOFINS": "N"})
p.update(kw)
return p
def decision(**kw):
d = {"status": "EXACT_NCM", "cest_status": "NO_CEST_MAPPING_FOUND", "ipi_status": "IPI_RESOLVED", "ba_st_mva_status": "NO_MVA_MAPPING_FOUND", "dataset_versions": {},
"classification": {"ncm": "22030000", "ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9, "rate": "3.9"}, "ipi_candidates": [], "cest": None, "cest_candidates": [], "ba_st_mva": None, "ba_st_mva_candidates": []}}
cls = kw.pop("classification", {})
d.update(kw)
d["classification"] = {**d["classification"], **cls}
return d
EV = {"recipes": set(), "composed": set(), "purchases": {}, "stock_entry": {}, "stock_in": {"000001": True}}
IPI_POP = {"scope": "COMPANY", "scope_key": "", "key": "store_ipi_profile", "value": "POPULATE_REFERENCE_RATE"}
def run(p=None, d=None, co=CO1, ctx=None, ev=EV, manual=None, ipi_profile=IPI_POP):
return resolve_product(p or product(), d or decision(), co, REFS, ev, Context((ctx or []) + ([ipi_profile] if ipi_profile else [])), manual_ncm=manual)
def st(res, f):
return res[f]["state"], res[f]["proposed"]
def test_every_write_field_has_a_complete_contract():
keys = {"ERP_FIELD", "UI_LABEL", "SEMANTICS", "DATA_TYPE", "SOURCE_OF_TRUTH", "REQUIRED_INPUTS", "RESOLUTION_RULE", "VALID_VALUES", "NOT_APPLICABLE_RULE", "MANUAL_INPUT_RULE", "WRITE_ALLOWED", "VALIDATION_RULE", "PROVENANCE"}
assert set(CONTRACT) == set(WRITE_FIELDS) and len(WRITE_FIELDS) == 23
for f, c in CONTRACT.items():
assert keys <= set(c) and all(c[k] not in (None, "", []) for k in keys - {"WRITE_ALLOWED"}), f
def test_only_five_final_states_and_manual_always_names_its_input():
res = run()
for f in WRITE_FIELDS:
assert res[f]["state"] in (ALREADY, CHANGE, NA, MANUAL)
if res[f]["state"] == MANUAL:
assert res[f]["input_key"]
assert res[f]["write_eligible"] == (res[f]["state"] == CHANGE)
def test_ncm_states_and_operator_ncm_reprocessing():
bad = run(p=product(CLASSIFICACAO_FISCAL="12345678"), d=decision(status="INVALID_NCM"))
assert st(bad, "CLASSIFICACAO_FISCAL")[0] == MANUAL and bad["CLASSIFICACAO_FISCAL"]["input_key"] == "ncm"
for f in ("IPI", "EX_TIPI", "CEST", "CFOP", "CSOSN"):
assert bad[f]["state"] == MANUAL and bad[f]["reason"] == "BLOCKED_BY_NCM"
assert st(run(manual=("22030000", False), p=product(CLASSIFICACAO_FISCAL="12345678"), d=decision(status="INVALID_NCM")), "CLASSIFICACAO_FISCAL")[0] == MANUAL
fixed = run(manual=("22030000", True), p=product(CLASSIFICACAO_FISCAL="12345678"), d=decision())
assert st(fixed, "CLASSIFICACAO_FISCAL") == (CHANGE, "22030000") and fixed["IPI"]["state"] == CHANGE and fixed["IPI"]["proposed"] == 3.9 # dependents reprocessed with the manual NCM
def test_ipi_rules_never_write_nt_or_blank_and_ask_when_ambiguous():
assert st(run(), "IPI") == (CHANGE, 3.9)
assert st(run(p=product(IPI=3.9)), "IPI")[0] == ALREADY
nt = run(d=decision(classification={"ipi": {"rate_kind": "NON_NUMERIC", "rate_classification": "NT"}}))
assert nt["IPI"]["state"] == NA and not nt["IPI"]["write_eligible"]
blank = run(d=decision(classification={"ipi": {"rate_kind": "BLANK"}}))
assert blank["IPI"]["state"] == NA and "NEVER_ZERO" in blank["IPI"]["reason"]
amb = decision(ipi_status="IPI_CANDIDATES", classification={"ipi": None, "ipi_candidates": [{"ex": None, "rate": "5", "description": "base"}, {"ex": "1", "rate": "0", "description": "Chope"}]})
r = run(d=amb)
assert r["IPI"]["state"] == MANUAL and r["IPI"]["input_key"] == "ex_tipi" and len(r["IPI"]["choices"]) == 2
solved = run(d=amb, ctx=[{"scope": "PRODUCT", "scope_key": "000001", "key": "ex_tipi", "value": "NONE"}])
assert st(solved, "IPI") == (CHANGE, 5.0) and solved["EX_TIPI"]["state"] == NA
def test_cest_contract():
off = [{"code": "03.021.03", "official_description": "Cerveja em lata", "description_score": 1.0}, {"code": "03.021.05", "official_description": "PET", "description_score": 0.5}]
conf = run(d=decision(cest_status="CEST_CONFIRMED", classification={"cest": ["03.021.03"], "cest_candidates": off}))
assert st(conf, "CEST") == (CHANGE, "0302103")
assert run(p=product(CEST="0302103"), d=decision(cest_status="CEST_CANDIDATES", classification={"cest_candidates": off}))["CEST"]["state"] == ALREADY
many = run(d=decision(cest_status="CEST_CANDIDATES", classification={"cest_candidates": off}))
assert many["CEST"]["state"] == MANUAL and many["CEST"]["input_key"] == "cest" and len(many["CEST"]["choices"]) == 2
assert run(d=decision(cest_status="CEST_DESCRIPTION_MISMATCH", classification={"cest_candidates": off}))["CEST"]["state"] == NA
assert run()["CEST"]["state"] == NA
assert run(p=product(CEST="9999999"))["CEST"]["state"] == MANUAL
picked = run(d=decision(cest_status="CEST_CANDIDATES", classification={"cest_candidates": off}), ctx=[{"scope": "NCM", "scope_key": "22030000", "key": "cest", "value": "03.021.05"}])
assert st(picked, "CEST") == (CHANGE, "0302105")
def test_cfop_role_and_st_rules():
ok = run() # RESALE (stock entries), not subject to ST, declared 5102 == derived
assert ok["CFOP"]["state"] == ALREADY and ok["_facts"]["role"] == "RESALE"
assert st(run(p=product(CFOP="5405")), "CFOP") == (CHANGE, "5102") # 5405 but proven NOT_SUBJECT
assert st(run(p=product(CFOP=None)), "CFOP") == (CHANGE, "5102") # blank filled
unk = run(d=decision(ba_st_mva_status="MVA_CANDIDATES"), p=product(CFOP=None)) # ST unknown, blank -> asks
assert unk["CFOP"]["state"] == MANUAL and unk["CFOP"]["input_key"] == "st_status"
assert run(d=decision(ba_st_mva_status="MVA_CANDIDATES"))["CFOP"]["state"] == ALREADY # declared valid, not contradicted
role_unknown = run(ev={**EV, "stock_in": {}}, p=product(CFOP=None))
assert role_unknown["CFOP"]["state"] == MANUAL and role_unknown["CFOP"]["input_key"] == "product_role"
own = run(ev={**EV, "recipes": {"000001"}}, p=product(CFOP="5102"))
assert st(own, "CFOP") == (CHANGE, "5101")
assert run(p=product(TIPO="MATERIA-PRIMA"))["CFOP"]["state"] == NA
def test_cfop_entrada_needs_company_entry_scope_then_derives():
r = run(p=product(CFOP_ENTRADA=None))
assert r["CFOP_ENTRADA"]["state"] == MANUAL and r["CFOP_ENTRADA"]["input_key"] == "entry_scope"
r = run(p=product(CFOP_ENTRADA=None), ctx=[{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "INTRA"}])
assert st(r, "CFOP_ENTRADA") == (CHANGE, "1102")
assert run(p=product(CFOP_ENTRADA="1102"))["CFOP_ENTRADA"]["state"] == ALREADY
def test_cfop_entrada_mixed_scope_is_operation_dependent_and_never_invents_a_cfop():
mixed = [{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC"}]
r = run(p=product(CFOP_ENTRADA=None), ctx=mixed)
e = r["CFOP_ENTRADA"]
# NA (not MANUAL): the audited entry_scope=MIXED_OR_DYNAMIC decision already answered "why no value" -
# this must never block product completeness nor re-ask a question the admin already answered (Missão 2 / Bloco 3.4).
assert e["state"] == NA and e["proposed"] is None and e.get("input_key") is None
assert e["reason"] == "CFOP_ENTRADA_OPERATION_DEPENDENT_MIXED_SCOPE"
r2 = run(p=product(CFOP_ENTRADA="2102"), ctx=mixed)
assert r2["CFOP_ENTRADA"]["state"] == ALREADY and r2["CFOP_ENTRADA"]["reason"] == "DECLARED_VALID_OPERATION_DEPENDENT_MIXED_SCOPE"
from central.app.context_keys import validate_entry
assert validate_entry({"scope": "COMPANY", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC"})[0]["value"] == "MIXED_OR_DYNAMIC"
def test_icms_code_fields_follow_the_regime():
r1 = run()
assert r1["CST"]["state"] == NA and r1["CST_ENTRADA"]["state"] == NA and r1["CSOSN"]["state"] == ALREADY
assert run(p=product(CSOSN=None))["CSOSN"]["state"] == MANUAL
assert st(run(p=product(CSOSN=None), ctx=[{"scope": "GROUP", "scope_key": "000019", "key": "icms_treatment", "value": "EXEMPT"}]), "CSOSN") == (CHANGE, "400")
assert st(run(p=product(CSOSN="102"), ctx=[{"scope": "GROUP", "scope_key": "000019", "key": "icms_treatment", "value": "ST_WITHHELD"}]), "CSOSN") == (CHANGE, "500")
r3 = run(co=CO3, p=product(CSOSN=None))
assert r3["CSOSN"]["state"] == NA and r3["CST"]["state"] == ALREADY and r3["ALIQUOTA"]["state"] in (ALREADY, MANUAL)
blank = run(co=CO3, p=product(CST=None, COD_ORIGEM_MERCADORIA="0"))
assert blank["CST"]["state"] == MANUAL and blank["CST"]["input_key"] == "icms_treatment"
filled = run(co=CO3, p=product(CST=None, COD_ORIGEM_MERCADORIA="0"), ctx=[{"scope": "COMPANY", "scope_key": "", "key": "icms_treatment", "value": "TAXED"}])
assert st(filled, "CST") == (CHANGE, "000")
def test_aliquota_not_copied_blindly():
assert run()["ALIQUOTA"]["state"] == NA # Simples CSOSN 102 highlights no ICMS
assert run(p=product(CSOSN="101", ALIQUOTA=None))["ALIQUOTA"]["input_key"] == "credit_rate"
assert st(run(co=CO3, p=product(ALIQUOTA="17"), ctx=[{"scope": "COMPANY", "scope_key": "", "key": "icms_rate", "value": "20,5"}]), "ALIQUOTA") == (CHANGE, 20.5)
def test_origin_binding_rules():
r0 = run(p=product(COD_ORIGEM_MERCADORIA=None, CST="040"))["COD_ORIGEM_MERCADORIA"] # audit V2: the CST first digit is padding, never origin evidence
assert r0["state"] == MANUAL and r0["reason"] == "ORIGIN_NO_EVIDENCE" and r0["proposed"] is None
assert run(p=product(COD_ORIGEM_MERCADORIA="0", CST="040"))["COD_ORIGEM_MERCADORIA"]["state"] == ALREADY
assert run(p=product(COD_ORIGEM_MERCADORIA="2", CST="040"))["COD_ORIGEM_MERCADORIA"]["state"] == ALREADY # declared origin stays; CST digit is padding
assert run(p=product(COD_ORIGEM_MERCADORIA=None, CST="540"))["COD_ORIGEM_MERCADORIA"]["state"] == MANUAL # 5 is not on the SGV screen
assert st(run(p=product(COD_ORIGEM_MERCADORIA=None, CST="540"), ctx=[{"scope": "GROUP", "scope_key": "000019", "key": "origin", "value": "1"}]), "COD_ORIGEM_MERCADORIA") == (CHANGE, 1)
def test_cst_ipi_pis_cofins_incidence_iat_ippt():
assert run()["CST_IPI"]["state"] == NA
assert run(co=CO3)["CST_IPI"]["state"] == MANUAL
r = run()
assert r["COD_SAIDA_PIS"]["state"] == MANUAL and r["COD_SAIDA_PIS"]["input_key"] == "pis_cofins_profile" and r["ALIQ_COFINS"]["state"] == MANUAL
prof = '{"pis_out_cst": "49", "pis_in_cst": "98", "pis_rate": "0", "cofins_out_cst": "49", "cofins_in_cst": "98", "cofins_rate": "0"}'
r = run(ctx=[{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": prof}])
assert st(r, "COD_SAIDA_PIS") == (CHANGE, "49") and st(r, "COD_ENTRADA_COFINS") == (CHANGE, "98") and st(r, "ALIQ_PIS") == (CHANGE, "0")
bad = run(ctx=[{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": '{"pis_out_cst": "77"}'}])
assert bad["COD_SAIDA_PIS"]["state"] == MANUAL # not in the official CST table
assert run(p=product(IAT=None))["IAT"]["input_key"] == "rounding"
assert run()["INCIDENCIA_PISCOFINS"]["state"] == ALREADY
assert st(run(ev={**EV, "recipes": {"000001"}}), "IPPT") == (CHANGE, "P") and run()["IPPT"]["state"] == ALREADY
def test_pis_cofins_not_applicable_and_operation_time_require_an_audited_admin_decision():
"""Missão 2 / Bloco 2.4: NOT_APPLICABLE/OPERATION_TIME for PIS/COFINS must never be inferred (e.g. from emission
statistics) - only ever accepted from an explicit, audited FiscalContextEntry(key="pis_cofins_profile")."""
pis_cofins_fields = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
# Sem decisão ADMIN => comportamento antigo (continua bloqueando, pedindo pis_cofins_profile).
r = run()
for f in pis_cofins_fields:
assert r[f]["state"] == MANUAL and r[f]["input_key"] == "pis_cofins_profile"
# Com decisão ADMIN NOT_APPLICABLE => NA em todos, nunca escreve, nunca bloqueia completude.
na_ctx = [{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": "NOT_APPLICABLE"}]
r_na = run(ctx=na_ctx)
for f in pis_cofins_fields:
assert r_na[f]["state"] == NA and r_na[f]["proposed"] is None and r_na[f]["reason"] == "ADMIN_DECISION_PIS_COFINS_NOT_APPLICABLE"
# Com decisão ADMIN OPERATION_TIME => NA em todos (não bloqueia), motivo distinto, nunca escreve.
ot_ctx = [{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": "OPERATION_TIME"}]
r_ot = run(ctx=ot_ctx)
for f in pis_cofins_fields:
assert r_ot[f]["state"] == NA and r_ot[f]["proposed"] is None and r_ot[f]["reason"] == "ADMIN_DECISION_PIS_COFINS_OPERATION_TIME"
# Uma decisão de PROFILE (JSON) continua funcionando exatamente como antes (não regressivo).
prof = '{"pis_out_cst": "49", "pis_in_cst": "98", "pis_rate": "0", "cofins_out_cst": "49", "cofins_in_cst": "98", "cofins_rate": "0"}'
r_prof = run(ctx=[{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": prof}])
assert st(r_prof, "COD_SAIDA_PIS") == (CHANGE, "49")
assert run(p=product(TIPO="MATERIA-PRIMA"))["IPPT"]["state"] == NA
def test_context_hierarchy_product_beats_group_beats_company():
ctx = Context([{"scope": "COMPANY", "scope_key": "", "key": "product_role", "value": "RESALE"}, {"scope": "GROUP", "scope_key": "G1", "key": "product_role", "value": "OWN_PRODUCTION"},
{"scope": "PRODUCT", "scope_key": "P1", "key": "product_role", "value": "INPUT"}])
assert ctx.get("product_role", "P1", "G1") == ("INPUT", "PRODUCT")
assert ctx.get("product_role", "P2", "G1") == ("OWN_PRODUCTION", "GROUP")
assert ctx.get("product_role", "P2", "G9") == ("RESALE", "COMPANY")
assert Context([]).get("product_role", "P2", "G9") == (None, None)
def test_role_inference_is_evidence_based_and_conflicts_stay_unknown():
assert infer_role(product(TIPO="MATERIA-PRIMA"), EV, Context([]), "000001", "1")[0] == "INPUT"
assert infer_role(product(), {**EV, "stock_in": {}}, Context([]), "000001", "1")[0] == "UNKNOWN"
assert infer_role(product(PROD_COZINHA=1), {**EV, "purchases": {"000001": [{}]}}, Context([]), "000001", "1")[0] == "UNKNOWN"
assert infer_role(product(PROD_COZINHA=1), EV, Context([]), "000001", "1")[0] == "UNKNOWN" # kitchen flag alone is not proof
assert infer_role(product(PROD_COZINHA=1), {**EV, "recipes": {"000001"}}, Context([]), "000001", "1")[0] == "OWN_PRODUCTION"
def test_composed_evidence_needs_a_dominant_majority_never_a_single_occurrence():
"""Missão 4 / Bloco A.1: C000032.TIPO=COMPOSICAO must be a deterministic MAJORITY of that
product's own movement history to prove OWN_PRODUCTION - real cases audited from Agent 2 (2026-09-28)."""
def ev_with(composed, other):
return {**EV, "composed_ratio": {"000001": {"composed": composed, "other": other}}}
# A cerveja: 1 composição vs 19 vendas (5%) - minoria clara, evidência descartada -> cai para o
# papel padrão (aqui sem TIPO=VENDA/docs no fixture, então UNKNOWN por falta de outra evidência;
# o ponto do teste é que NUNCA vira OWN_PRODUCTION).
assert infer_role(product(), ev_with(1, 19), Context([]), "000001", "1")[0] != "OWN_PRODUCTION"
r = infer_role(product(TIPO="VENDA"), {**ev_with(1, 19), "stock_in": {"000001": True}}, Context([]), "000001", "1")
assert r[0] == "RESALE" # a cerveja, com evidência de venda, é revenda - nunca produção própria
# O refrigerante/2ª cerveja: 3 composições vs 4 vendas (42,9%) - amostra pequena mas na zona
# ambígua -> evidência CONFLITANTE, nunca adivinhada para nenhum dos dois lados.
r = infer_role(product(TIPO="VENDA"), {**ev_with(3, 4), "stock_in": {"000001": True}}, Context([]), "000001", "1")
assert r[0] == "UNKNOWN" and "MIXED" in r[1][0]
# O licor: 34 composições vs 22 vendas (60,7%) - maioria real, mas abaixo do limiar dominante
# (70%) -> também CONFLITANTE, nunca OWN_PRODUCTION por uma maioria fraca.
r = infer_role(product(TIPO="VENDA"), {**ev_with(34, 22), "stock_in": {"000001": True}}, Context([]), "000001", "1")
assert r[0] == "UNKNOWN" and "MIXED" in r[1][0]
# Suco com PROD_COZINHA=1 + RECEITA_INGREDIENTES: evidência forte e DIRETA (não é composição
# estatística) - continua OWN_PRODUCTION, o limiar de composição não se aplica a ela.
assert infer_role(product(PROD_COZINHA=1), {**EV, "recipes": {"000001"}}, Context([]), "000001", "1")[0] == "OWN_PRODUCTION"
# Uma maioria REAL e forte (90%, amostra suficiente) ainda prova produção própria.
r = infer_role(product(), ev_with(90, 10), Context([]), "000001", "1")
assert r[0] == "OWN_PRODUCTION" and r[1] == ["C000032.TIPO=COMPOSICAO"]
# Amostra insuficiente (< 5 eventos), mesmo 100% composição: não confia num histórico tão raso.
r = infer_role(product(TIPO="VENDA"), {**ev_with(2, 0), "stock_in": {"000001": True}}, Context([]), "000001", "1")
assert r[0] == "RESALE" # a evidência de composição é ignorada, cai para a evidência de venda real
# Tilápia/corvina: sem NENHUMA evidência de composição - continua revenda normalmente (o fix
# não pode quebrar o caso que já funcionava).
r = infer_role(product(TIPO="VENDA", CFOP=None), {**EV, "purchases": {"000001": [{}]}}, Context([]), "000001", "1")
assert r[0] == "RESALE"
# ---------------- executor guards ----------------
def load_executor():
spec = importlib.util.spec_from_file_location("fwe_local", ROOT / "scripts" / "firebird_write_executor.py")
m = importlib.util.module_from_spec(spec); sys.modules[spec.name] = m; spec.loader.exec_module(m)
return m
class FakeCursor:
def __init__(self, db):
self.db, self.rowcount, self._row = db, 0, None
def execute(self, sql, params=()):
s = " ".join(sql.split())
if "RDB$FIELD_TYPE" in s:
self._row = (37, 20)
elif s.startswith("SELECT") and "FROM C000025" in s:
fields = [x.strip() for x in s.split("SELECT")[1].split("FROM")[0].split(",")]
row = self.db["rows"].get(params[0])
self._row = tuple(row[f] for f in fields) if row else None
elif s.startswith("UPDATE"):
row = self.db["rows"].get(params[-1])
self.rowcount = 0 if row is None else self.db["update_rowcount"]
if row is not None and self.rowcount == 1:
for f, v in zip([x.split("=")[0].strip() for x in s.split("SET")[1].split("WHERE")[0].split(",")], params[:-1]):
self.db["pending"][f] = v
def fetchone(self):
return self._row
def close(self):
pass
class FakeConn:
def __init__(self, rows, update_rowcount=1):
self.db = {"rows": rows, "update_rowcount": update_rowcount, "pending": {}}
self.rolled_back = self.committed = False
def cursor(self):
return FakeCursor(self.db)
def commit(self):
for row in self.db["rows"].values():
row.update(self.db["pending"])
self.committed = True
def rollback(self):
self.db["pending"].clear(); self.rolled_back = True
ALLOWED = r"D:\PROJETOS\GTISISTEMAS\Projeto\work_copies\CONSTRUCAO.FDB"
def test_executor_allowlist_is_exact_and_field_set_is_the_contract():
ex = load_executor()
assert ex._LOCAL_COMPLETION_FIELDS == set(WRITE_FIELDS)
allow = ex.load_local_allowlist()
assert len(allow) == 5
for path in (ALLOWED, r"D:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB"):
ex.assert_local_completion_target(path, allow)
for bad in (r"D:\PROJETOS\GTISISTEMAS\Banco\GTI.FDB", r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco\OUTRA.FDB", r"D:\PROJETOS\GTISISTEMAS\Projeto\work_copies\CONSTRUCAO.FDB.bak", r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco", r"D:\PROJETOS\GTISISTEMAS\Projeto\Banco\CONSTRUCAO.FDB"):
with pytest.raises(ex.WriteRefused):
ex.assert_local_completion_target(bad, allow)
def test_guarded_product_update_success_drift_rowcount_and_refusals():
ex = load_executor()
allow = ex.load_local_allowlist()
conn = FakeConn({"000001": {"IPI": None, "CEST": "1234567"}})
rec = ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 3.9), "CEST": ("1234567", "7654321")}, now="t", allowlist=allow)
assert rec["rowcount"] == 1 and rec["fields"]["IPI"]["after"] == 3.9 and conn.committed
with pytest.raises(ex.WriteRefused, match="drift"):
ex.execute_guarded_product_update(FakeConn({"000001": {"IPI": 1.0}}), database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 3.9)}, now="t", allowlist=allow)
c2 = FakeConn({"000001": {"IPI": None}}, update_rowcount=2)
with pytest.raises(ex.WriteRefused, match="rowcount"):
ex.execute_guarded_product_update(c2, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 3.9)}, now="t", allowlist=allow)
assert c2.rolled_back
with pytest.raises(ex.WriteRefused, match="allowlist"):
ex.execute_guarded_product_update(conn, database_path=r"D:\PROJETOS\GTISISTEMAS\Banco\GTI.FDB", product_code="000001", changes={"IPI": (None, 1)}, now="t", allowlist=allow)
with pytest.raises(ex.WriteRefused, match="contract write target"):
ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"CODIGO": ("000001", "9")}, now="t", allowlist=allow)
with pytest.raises(ex.WriteRefused, match="null"):
ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, None)}, now="t", allowlist=allow)
with pytest.raises(ex.WriteRefused, match="only C000025"):
ex.execute_guarded_product_update(conn, database_path=ALLOWED, product_code="000001", changes={"IPI": (None, 1)}, now="t", allowlist=allow, table="NFCE_ITENS")
@@ -0,0 +1,49 @@
import json
from pathlib import Path
from central.app.fiscal_completeness import evaluate_field, evaluate_product, load_requiredness_matrix
ROOT = Path(__file__).resolve().parents[2]
def test_requiredness_matrix_covers_contract_fields_once():
contract = json.loads((ROOT / "docs/FISCAL_FIELD_CONTRACT_V1.json").read_text(encoding="utf-8"))
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
assert set(matrix["fields"]) == set(contract["fields"])
assert len(matrix["fields"]) == 23
assert set(matrix["classifications"]) == {
"REQUIRED", "CONDITIONAL", "OPTIONAL_REFERENCE", "NOT_APPLICABLE", "ERP_NOT_USED"
}
def test_conditional_empty_when_not_triggered_is_not_missing():
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
result = evaluate_field("IPI", None, {"contribuinte_ipi": "N", "has_numeric_tipi": False}, matrix)
assert result["status"] == "CONDITIONAL_NOT_TRIGGERED"
assert result["valid"] is True
def test_required_missing_makes_product_incomplete():
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
result = evaluate_product(
{"CLASSIFICACAO_FISCAL": None, "CFOP": "5102"},
{"product_role": "RESALE", "operation": "SALE"},
matrix,
)
assert result["FISCALLY_COMPLETE"] is False
assert result["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "REQUIRED_MISSING"
def test_erp_not_used_is_not_missing():
matrix = load_requiredness_matrix(ROOT / "docs/FISCAL_REQUIREDNESS_MATRIX_V1.json")
result = evaluate_field("IAT", None, {"erp_uses_iat": False}, matrix)
assert result["status"] == "ERP_NOT_SUPPORTED"
assert result["valid"] is True
@@ -0,0 +1,184 @@
"""Post-NCM fiscal completion: wires the existing (previously dormant) field_contract.resolve_product into the live pipeline. Reuses the SAME engine,
the SAME FiscalContextEntry table the blocker-resolution workflow already writes to, and company_uf/company_crt already auto-discovered from every
heartbeat - no new rule, no guessing."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.enrichment import fiscal_completion as fcomp
from central.app.field_contract import WRITE_FIELDS
from central.app.models import Agent, Base, CfopEntry, Dataset, FiscalCodeEntry, FiscalContextEntry, Installation, Tenant
def _seed_codes(session, ncm="22030000"):
ds = Dataset(kind="NFE_MOC_TABLES", provider="x", sha256="a" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=0)
session.add(ds); session.flush()
for domain, codes in (("CSOSN", ["101", "102", "103", "300", "400", "500"]), ("CST_ICMS", ["00", "20", "40", "41", "60"]),
("CST_PIS", ["01", "49", "98"]), ("CST_COFINS", ["01", "49", "98"]), ("ORIGEM_MERCADORIA", list("012345678"))):
for code in codes:
session.add(FiscalCodeEntry(dataset_id=ds.id, domain=domain, code=code, description=f"{domain} {code}", source_section="test"))
cfop_ds = Dataset(kind="CFOP", provider="x", sha256="b" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=0)
session.add(cfop_ds); session.flush()
for code in ("5101", "5102", "5401", "1102", "1101"):
session.add(CfopEntry(dataset_id=cfop_ds.id, code=code, title=f"CFOP {code}"))
session.commit()
def _installation(session, uf="BA", crt="1", company_key="000001", contribuinte_ipi=None):
tenant = Tenant(id=1, name="Acme"); session.add(tenant); session.flush()
agent = Agent(tenant_id=1, name="a", token_hash="x" * 64); session.add(agent); session.flush()
inst = Installation(agent_id=agent.id, tenant_id=1, installation_id="11111111-1111-4111-8111-111111111111", company_uf=uf, company_crt=crt, company_code=company_key,
company_contribuinte_ipi=contribuinte_ipi)
session.add(inst); session.flush()
return inst
def _ctx(session, tenant_id, company_key, entries):
for e in entries:
session.add(FiscalContextEntry(tenant_id=tenant_id, company_key=company_key, scope=e[0], scope_key=e.get("scope_key", "") if isinstance(e, dict) else "", key=e[1] if not isinstance(e, dict) else e["key"],
value=e[2] if not isinstance(e, dict) else e["value"], source="OPERATOR"))
session.commit()
def env():
from sqlalchemy import create_engine
from sqlalchemy.pool import StaticPool
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return Session(engine)
def test_field_shapes():
assert fcomp.is_writable_shape("CFOP", "5102") and not fcomp.is_writable_shape("CFOP", "51A2") and not fcomp.is_writable_shape("CFOP", "510")
assert fcomp.is_writable_shape("CST", "000") and fcomp.is_writable_shape("CST", "0000")
assert fcomp.is_writable_shape("ALIQUOTA", "17.5") and fcomp.is_writable_shape("ALIQ_PIS", "1,65") and not fcomp.is_writable_shape("ALIQUOTA", "abc")
assert fcomp.is_writable_shape("SITUACAO_TRIBUTARIA", "t") and not fcomp.is_writable_shape("SITUACAO_TRIBUTARIA", "X")
assert fcomp.is_writable_shape("IAT", "A") and fcomp.is_writable_shape("IPPT", "P") and fcomp.is_writable_shape("PISCOFINS", "N")
assert not fcomp.is_writable_shape("CFOP", "")
def test_company_auto_discovered_from_heartbeat_no_guessing():
s = env()
inst = _installation(s)
assert fcomp.build_company(inst) == {"uf": "BA", "crt": "1"}
def test_resolve_fiscal_fields_with_full_context_fills_independent_safe_fields():
s = env()
_seed_codes(s)
inst = _installation(s)
import json
pis_cofins_profile = json.dumps({"pis_in_cst": "01", "pis_out_cst": "01", "pis_rate": "1.65", "cofins_in_cst": "01", "cofins_out_cst": "01", "cofins_rate": "7.6"})
_ctx(s, 1, "000001", [
("COMPANY", "product_role", "RESALE"), ("COMPANY", "icms_treatment", "TAXED"), ("COMPANY", "origin", "0"),
("COMPANY", "pis_cofins_profile", pis_cofins_profile), ("COMPANY", "piscofins", "S"), ("COMPANY", "rounding", "A"),
("COMPANY", "store_ipi_profile", "LEAVE_NOT_APPLICABLE"), ("COMPANY", "icms_rate", "17"), ("COMPANY", "entry_scope", "INTRA"),
])
out = fcomp.resolve_fiscal_fields(s, product_code="X1", description="CERVEJA LATA", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1)
safe = {f: v["value"] for f, v in out.items() if v["status"] == "AUTO_FILL_SAFE"}
assert "CSOSN" in safe and safe["CSOSN"] == "102"
assert "PISCOFINS" in safe or out["PISCOFINS"]["status"] == "ALREADY_SET"
for f, v in out.items():
assert v["status"] in ("AUTO_FILL_SAFE", "ALREADY_SET", "NOT_APPLICABLE", "NEEDS_REVIEW", "INSUFFICIENT_EVIDENCE"), f
def test_field_independence_one_missing_context_does_not_block_others():
s = env()
_seed_codes(s)
inst = _installation(s)
_ctx(s, 1, "000001", [("COMPANY", "product_role", "RESALE"), ("COMPANY", "rounding", "A")]) # icms_treatment / pis_cofins_profile intentionally missing
out = fcomp.resolve_fiscal_fields(s, product_code="X2", description="CERVEJA LATA", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1)
assert out["IAT"]["status"] == "AUTO_FILL_SAFE" and out["IAT"]["value"] == "A" # resolved independently
assert out["CSOSN"]["status"] == "NEEDS_REVIEW" and out["CSOSN"]["missing_key"] == "icms_treatment"
assert out["COD_ENTRADA_PIS"]["status"] == "NEEDS_REVIEW" and out["COD_ENTRADA_PIS"]["missing_key"] == "pis_cofins_profile"
def test_contribuinte_ipi_auto_discovered_from_installation_wins_over_operator_context():
"""Installation.company_contribuinte_ipi (auto-discovered at heartbeat from C000004.CONTRIBUINTE_IPI) is an ERP fact; a FiscalContextEntry
operator answer is only a fallback for an Agent version that hasn't reported it yet. ERP fact must win when both exist."""
s = env()
_seed_codes(s)
inst = _installation(s, contribuinte_ipi="S")
_ctx(s, 1, "000001", [("COMPANY", "contribuinte_ipi", "N")]) # stale/wrong operator answer - the ERP fact must override it
out = fcomp.resolve_fiscal_fields(s, product_code="X9", description="MAQUINA", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1)
assert out["CST_IPI"]["status"] == "NEEDS_REVIEW" # contribuinte_ipi=S alone doesn't fill CST_IPI (still needs a declared value), but it's no longer NOT_APPLICABLE
assert out["CST_IPI"]["reason"] != "contribuinte de IPI não confirmado" # proves the ERP fact (S) was used, not "unknown"
def test_targeted_product_evidence_feeds_role_inference_st_withheld_never_a_full_scan():
"""`ev` (firebird_reader.read_product_evidence's targeted, per-product recipes/composed/purchases/stock) reaches field_contract.resolve_product
the same way the full-scan `read_evidence` already does for sync jobs - just scoped to ONE product, never the whole catalog."""
s = env()
_seed_codes(s)
inst = _installation(s)
out = fcomp.resolve_fiscal_fields(s, product_code="X10", description="BOLO CASEIRO", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1,
ev={"recipes": {"X10"}, "composed": set(), "purchases": {}, "stock_entry": {}, "stock_in": {}})
assert out["CFOP"]["status"] == "NEEDS_REVIEW" # OWN_PRODUCTION now provable from the recipe evidence alone - no product_role question, only the CFOP direction/context remains
assert out["CFOP"]["missing_key"] != "product_role"
def test_extra_columns_feed_field_contracts_own_role_inference_no_operator_question_needed():
"""PRODUTO_SERVICO is a real C000025 column field_contract.infer_role already reads as objective ERP evidence (no ctx.get("product_role")
needed). Proves `extra_columns` (the Agent's classification columns) actually reach `resolve_product`: with PRODUTO_SERVICO set, CFOP is
blocked on the SERVICE-specific `service_cfop` key, not the generic `product_role` key a fully-unknown product would need."""
s = env()
_seed_codes(s)
inst = _installation(s)
out = fcomp.resolve_fiscal_fields(s, product_code="X7", description="CONSULTORIA", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1,
extra_columns={"PRODUTO_SERVICO": "1"})
assert out["CFOP"]["status"] == "NEEDS_REVIEW" and out["CFOP"]["missing_key"] == "service_cfop"
def test_prod_cozinha_string_from_agent_json_payload_is_normalized_to_int_for_infer_role():
"""The Agent sends `current`/extra columns as JSON strings ("1", not 1). field_contract.infer_role compares PROD_COZINHA == 1 (a real
Firebird SMALLINT). Without normalization the kitchen-routing signal would silently never fire."""
s = env()
_seed_codes(s)
inst = _installation(s)
out = fcomp.resolve_fiscal_fields(s, product_code="X8", description="PRATO FEITO", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1,
extra_columns={"PROD_COZINHA": "1"})
assert out["CFOP"]["status"] == "NEEDS_REVIEW" # PROD_COZINHA=1 alone is UNKNOWN (not proof), but it must be READ, not silently ignored as a non-numeric string
assert out["CFOP"]["missing_key"] == "product_role"
def test_cst_ipi_unknown_contribuinte_ipi_is_needs_review_not_a_silent_not_applicable():
"""C000004.CONTRIBUINTE_IPI is not yet sent at heartbeat - `company` never carries it unless an operator answered the context key. field_contract's
own rule reads a missing fact the same as a proven "N" (NOT_APPLICABLE); this module must not let that false negative through silently."""
s = env()
_seed_codes(s)
inst = _installation(s) # no contribuinte_ipi context entry: genuinely unknown
out = fcomp.resolve_fiscal_fields(s, product_code="X5", description="CERVEJA LATA", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1)
assert out["CST_IPI"]["status"] == "NEEDS_REVIEW" and out["CST_IPI"]["missing_key"] == "contribuinte_ipi"
_ctx(s, 1, "000001", [("COMPANY", "contribuinte_ipi", "N")])
out = fcomp.resolve_fiscal_fields(s, product_code="X6", description="CERVEJA LATA", current={}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1)
assert out["CST_IPI"]["status"] == "NOT_APPLICABLE" # once the fact is genuinely proven "N", the engine's own NA decision is trusted again
def test_not_official_ncm_blocks_everything_with_explicit_reason():
s = env()
inst = _installation(s)
out = fcomp.resolve_fiscal_fields(s, product_code="X3", description="X", current={}, ncm="00000000", ncm_official=False, installation=inst, tenant_id=1)
assert all(v["status"] == "INSUFFICIENT_EVIDENCE" for v in out.values())
def test_existing_value_preserved_not_overwritten():
"""CSOSN 500 declares ICMS already withheld by ST - a DIFFERENT treatment than a plain TAXED declaration (CSOSN_FAMILY in field_contract.py
maps icms_treatment=ST_WITHHELD -> {"500"} and icms_treatment=TAXED -> {"101","102","103"}). With the matching treatment declared, the engine
proves the existing value correct and does not touch it."""
s = env()
_seed_codes(s)
inst = _installation(s)
_ctx(s, 1, "000001", [("COMPANY", "product_role", "RESALE"), ("COMPANY", "icms_treatment", "ST_WITHHELD"), ("COMPANY", "origin", "0")])
out = fcomp.resolve_fiscal_fields(s, product_code="X4", description="CERVEJA LATA", current={"CSOSN": "500"}, ncm="22030000", ncm_official=True, installation=inst, tenant_id=1)
assert out["CSOSN"]["status"] == "ALREADY_SET"
def test_coverage_status_complete_vs_safe_blocked():
complete = {f: {"status": "ALREADY_SET"} for f in fcomp.FISCAL_COMPLETION_FIELDS}
assert fcomp.coverage_status(complete) == "COMPLETE"
blocked = dict(complete, CFOP={"status": "NEEDS_REVIEW"})
assert fcomp.coverage_status(blocked) == "SAFE_BLOCKED"
def test_all_write_fields_covered_by_the_module_except_the_explicit_exclusions():
excluded = {"CLASSIFICACAO_FISCAL", "CEST", "IPI", "COD_ORIGEM_MERCADORIA", "EX_TIPI"}
assert set(WRITE_FIELDS) - excluded == set(fcomp.FISCAL_COMPLETION_FIELDS)
@@ -0,0 +1,50 @@
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.context_keys import validate_entry
from central.app.main import create_app
from central.app.models import Base
H = {"Authorization": "Bearer test-token"}
def client():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return TestClient(create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token"))
def test_validation_rules():
ok, err = validate_entry({"scope": "group", "scope_key": "000019", "key": "product_role", "value": "resale"})
assert err is None and ok["value"] == "RESALE" and ok["scope"] == "GROUP"
for bad in ({"scope": "GROUP", "key": "product_role", "value": "RESALE"}, # group without key
{"scope": "COMPANY", "key": "product_role", "value": "BOSS"}, # not in enum
{"scope": "GROUP", "scope_key": "1", "key": "ncm", "value": "22030000"}, # ncm is product scope
{"scope": "PRODUCT", "scope_key": "1", "key": "ncm", "value": "2203"}, # not 8 digits
{"scope": "COMPANY", "key": "pis_cofins_profile", "value": "{\"x\": 1}"}, # unknown profile key
{"scope": "COMPANY", "key": "icms_rate", "value": "abc"}, {"scope": "COMPANY", "key": "nope", "value": "1"}):
assert validate_entry(bad)[1]
assert validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "{\"pis_out_cst\": \"49\"}"})[1] is None
# Missão 2 / Bloco 2.3: the two audited sentinels must validate (case-insensitive) alongside the JSON profile shape.
ok, err = validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "not_applicable"})
assert err is None and ok["value"] == "NOT_APPLICABLE"
ok, err = validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "OPERATION_TIME"})
assert err is None and ok["value"] == "OPERATION_TIME"
def test_import_is_all_or_nothing_upserting_and_listed():
c = client()
body = {"company_key": "abc123", "entries": [{"scope": "GROUP", "scope_key": "000019", "key": "product_role", "value": "RESALE", "reason": "revenda"},
{"scope": "COMPANY", "key": "entry_scope", "value": "INTRA"}]}
assert c.post("/v1/context/import", json=body, headers=H).json() == {"created": 2, "updated": 0}
body["entries"][0]["value"] = "OWN_PRODUCTION"
assert c.post("/v1/context/import", json=body, headers=H).json() == {"created": 0, "updated": 2}
listed = c.get("/v1/context", params={"company_key": "abc123"}, headers=H).json()["entries"]
assert {(e["scope"], e["key"], e["value"]) for e in listed} == {("GROUP", "product_role", "OWN_PRODUCTION"), ("COMPANY", "entry_scope", "INTRA")}
bad = {"company_key": "abc123", "entries": [{"scope": "COMPANY", "key": "rounding", "value": "A"}, {"scope": "COMPANY", "key": "rounding", "value": "Z"}]}
r = c.post("/v1/context/import", json=bad, headers=H)
assert r.status_code == 422
assert len(c.get("/v1/context", params={"company_key": "abc123"}, headers=H).json()["entries"]) == 2 # nothing partially stored
assert c.get("/v1/context", params={"company_key": "other"}, headers=H).json()["entries"] == []
@@ -0,0 +1,78 @@
"""Contexto Fiscal (Agent detail, ADMIN only): confirm/alter/clear one of the reduced set of real fiscal decisions
(icms_treatment/pis_cofins_profile/entry_scope). CSRF, RBAC and audit reuse the exact same mechanism already proven for
/agents/{id}/features; persistence reuses FiscalContextEntry, the same table blocker_resolution already writes to."""
from sqlalchemy import select
from central.app.models import AuditEvent, FiscalContextEntry, Installation, ProductEnrichment
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_rtc_security import _become, _post
def _company_key(client):
with client._session_factory() as s:
inst = s.scalars(select(Installation)).one()
return (inst.company_code or inst.company_name or "").strip(), inst.tenant_id, inst.id
def test_fiscal_context_tab_lists_only_the_three_reduced_decisions_with_evidence():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
page = client.get(f"/agents/{agent_id}").text
assert "Contexto Fiscal" in page
for title in ("Tratamento padrão de ICMS", "Perfil de PIS/COFINS", "CFOP de entrada padrão"):
assert title in page
assert "487 de 521" in page # real evidence shown, not a raw internal key name
assert "<h3>Tratamento padrão de ICMS dos produtos comuns</h3>" in page # human title heads the card; internal key only appears as a hidden form field
def test_fiscal_context_hidden_from_non_admin():
client, agent_id, _ = _panel_client()
_become(client, "PADRAO")
page = client.get(f"/agents/{agent_id}").text
assert "Contexto Fiscal" not in page
def test_confirming_a_decision_is_csrf_admin_and_audited_and_flags_reevaluation():
client, agent_id, _ = _panel_client()
company_key, tenant_id, installation_id = _company_key(client)
with client._session_factory() as s:
s.add(ProductEnrichment(tenant_id=tenant_id, installation_id=installation_id, product_code="P1", status="RESOLVED", ui_status="PRONTO_PARA_PREENCHER"))
s.commit()
_become(client, "PADRAO")
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", value="TAXED", fc_action="confirm").status_code == 403
_become(client, "ADMIN")
assert client.post(f"/agents/{agent_id}/fiscal-context", data={"csrf_token": "bad", "key": "icms_treatment", "value": "TAXED", "fc_action": "confirm"}, follow_redirects=False).status_code in (400, 403)
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", value="TAXED", fc_action="confirm").status_code == 303
with client._session_factory() as s:
entry = s.scalars(select(FiscalContextEntry).where(FiscalContextEntry.key == "icms_treatment")).one()
assert entry.scope == "COMPANY" and entry.value == "TAXED" and entry.source == "OPERATOR"
acts = [e.action for e in s.scalars(select(AuditEvent))]
assert "FISCAL_CONTEXT_DECISION" in acts
row = s.scalars(select(ProductEnrichment)).one()
assert row.reevaluate is True # existing products get a chance to pick up the new policy - never a silent no-op
def test_clearing_a_decision_deletes_the_entry_never_a_default_value():
client, agent_id, _ = _panel_client()
company_key, tenant_id, installation_id = _company_key(client)
_become(client, "ADMIN")
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", value="TAXED", fc_action="confirm").status_code == 303
assert _post(client, f"/agents/{agent_id}/fiscal-context", key="icms_treatment", fc_action="clear").status_code == 303
with client._session_factory() as s:
assert s.scalars(select(FiscalContextEntry).where(FiscalContextEntry.key == "icms_treatment")).first() is None
def test_invalid_value_is_rejected_never_silently_stored():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
resp = client.post(f"/agents/{agent_id}/fiscal-context", data={"csrf_token": client.cookies.get("gti_csrf"), "key": "icms_treatment", "value": "NOT_A_REAL_TREATMENT", "fc_action": "confirm"}, follow_redirects=False)
assert resp.status_code == 422
with client._session_factory() as s:
assert s.scalars(select(FiscalContextEntry)).first() is None
def test_unknown_key_is_refused():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
resp = client.post(f"/agents/{agent_id}/fiscal-context", data={"csrf_token": client.cookies.get("gti_csrf"), "key": "not_a_real_key", "value": "x", "fc_action": "confirm"}, follow_redirects=False)
assert resp.status_code == 422
@@ -0,0 +1,71 @@
from central.app.fiscal_decision import ERP_FIELD_MAP, decide_product
REFS = {"cfop_valid": {"5102"}, "csosn": {"102", "500"}, "cst_icms": {"00", "40", "60"}, "cst_pis": {"01"}, "cst_cofins": {"01"},
"origem": set("012345678"), "fcp_uf": {"BA": {"aliq_1": "Fixo:2.00"}}, "cbs_rate_2026": 0.009, "cclasstrib_count": 164}
COMPANY = {"uf": "BA", "crt": "1"}
def product(**kw):
base = {"CODIGO": "1", "PRODUTO": "X", "CLASSIFICACAO_FISCAL": "22030000", "CFOP": "5102", "CST": "040", "CSOSN": "102",
"CEST": None, "IPI": None, "EX_TIPI": None, "ORIGEM": None, "COD_SAIDA_PIS": None, "COD_SAIDA_COFINS": None, "ALIQ_PIS": None}
base.update(kw)
return base
def decision(**kw):
base = {"status": "EXACT_NCM", "ipi_status": "IPI_RESOLVED", "cest_status": "CEST_CANDIDATES", "ba_st_mva_status": "MVA_CANDIDATES",
"dataset_versions": {}, "classification": {"ncm": "22030000", "ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9}, "cest_candidates": [1, 2]}}
base.update(kw)
return base
def test_numeric_ipi_change_is_the_only_auto_apply():
out = decide_product(product(IPI=5102.0), decision(), COMPANY, REFS)
assert out["IPI"]["action_class"] == "AUTO_APPLY_SAFE" and out["IPI"]["write_eligible"] and out["IPI"]["proposed_value"] == 3.9
assert [d for d, r in out.items() if r["write_eligible"]] == ["IPI"]
def test_same_ipi_is_already_correct_not_write():
out = decide_product(product(IPI=3.9), decision(), COMPANY, REFS)
assert out["IPI"]["action_class"] == "ALREADY_CORRECT" and not out["IPI"]["write_eligible"]
def test_non_numeric_and_blank_ipi_never_write():
d = decision(classification={"ipi": {"rate_kind": "NON_NUMERIC", "rate_classification": "NT"}})
assert not decide_product(product(), d, COMPANY, REFS)["IPI"]["write_eligible"]
d = decision(classification={"ipi": {"rate_kind": "BLANK"}})
r = decide_product(product(), d, COMPANY, REFS)["IPI"]
assert not r["write_eligible"] and r["status"] == "RULE_UNRESOLVED"
def test_operation_dependent_fields_never_resolved_or_written():
out = decide_product(product(), decision(), COMPANY, REFS)
for dom in ("CFOP", "CSOSN", "CST_ICMS", "PIS_CST", "COFINS_CST", "FCP", "RTC_CCLASSTRIB", "RTC_CBS_RATE"):
assert out[dom]["status"] == "OPERATION_CONTEXT_REQUIRED" and not out[dom]["write_eligible"]
assert out["CFOP"]["domain_valid"] is True and out["FCP"]["fcp_rate_available"] is True
assert out["RTC_CBS_RATE"]["proposed_value"] is None and out["RTC_CBS_RATE"]["reference_rate_2026"] == 0.009
def test_ncm_legacy_is_review_and_never_write():
out = decide_product(product(), decision(status="LEGACY_NCM"), COMPANY, REFS)
assert out["NCM"]["action_class"] == "REVIEW_REQUIRED" and not any(r["write_eligible"] for r in out.values())
def test_origin_only_candidate_and_unconfirmed_mapping():
out = decide_product(product(), decision(), COMPANY, REFS)
assert out["ORIGEM"]["status"] == "RULE_CANDIDATE" and not out["ORIGEM"]["write_eligible"]
assert {f["ERP_FIELD"] for f in ERP_FIELD_MAP if f["MAPPING"] == "MAPPING_UNCONFIRMED"} >= {"ORIGEM", "CFOP_ENTRADA", "CST_ENTRADA"}
def test_mva_never_written_even_when_resolved():
d = decision(ba_st_mva_status="MVA_RESOLVED", classification={"ipi": {"rate_kind": "NUMERIC", "rate_numeric": 1}, "ba_st_mva": {"mva_original": "140%"}})
r = decide_product(product(IPI=1), d, COMPANY, REFS)["MVA"]
assert r["status"] == "RULE_RESOLVED" and not r["write_eligible"]
def test_single_ex_without_description_evidence_is_candidate_not_write():
cls = {"ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9}, "ipi_candidates": [{"ex": "1", "description": "Chope"}]}
out = decide_product(product(PRODUTO="CERVEJA LATA", IPI=3.9), decision(classification=cls), COMPANY, REFS)
assert out["EX_TIPI"]["status"] == "RULE_CANDIDATE" and not out["EX_TIPI"]["write_eligible"]
out = decide_product(product(PRODUTO="CHOPE ESCURO", IPI=3.9), decision(classification=cls), COMPANY, REFS)
assert out["EX_TIPI"]["status"] == "RULE_RESOLVED" and out["EX_TIPI"]["write_eligible"]
@@ -0,0 +1,9 @@
from central.app.fiscal_determination import FiscalDetermination
def test_returns_unresolved_csosn_and_cfop_without_official_rule():
result = FiscalDetermination().determine({"ncm": "08043000", "cest_candidates": []})
assert result["csosn"] is None
assert result["cfop"] is None
assert "OFFICIAL_RULE_NOT_IMPLEMENTED" in result["reason_codes"]
+29
View File
@@ -0,0 +1,29 @@
from fastapi.testclient import TestClient
from central.app.main import create_app
def test_health_reports_database_dependency():
client = TestClient(create_app(database_probe=lambda: True))
assert client.get("/health").json() == {"status": "ok"}
def test_health_is_unavailable_when_database_is_down():
client = TestClient(create_app(database_probe=lambda: False))
assert client.get("/health").status_code == 503
def test_version_endpoint_reports_release_and_fiscal_versions():
body = TestClient(create_app(database_probe=lambda: True)).get("/version").json()
assert body["version"] == "1.3.15" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"]
def test_production_refuses_the_dev_token(monkeypatch):
import pytest
from central.app import main as main_module
monkeypatch.setattr(main_module.settings.__class__, "environment", "production", raising=False) if False else None
monkeypatch.setattr(main_module, "settings", type("S", (), {"environment": "production", "agent_dev_token": "dev", "log_level": "INFO"})())
with pytest.raises(RuntimeError):
create_app(database_probe=lambda: True)
@@ -0,0 +1,67 @@
"""Backpressure: at most HEAVY_JOB_CONCURRENCY_LIMIT whole-database jobs run at once across all Agents; the rest queue as PENDING."""
from types import SimpleNamespace
from sqlalchemy import select
from central.app import jobs
from central.app.models import Agent, Job, Tenant
from central.tests.test_control_plane import setup_db
def _agents(session, n):
tenant = Tenant(name="Backpressure")
session.add(tenant)
session.flush()
out = []
for i in range(n):
agent = Agent(tenant_id=tenant.id, name=f"a{i}", token_hash=f"t{i}".ljust(64, "x"), agent_version="1.2.1")
session.add(agent)
out.append(agent)
session.flush()
return out
def _limit(monkeypatch, value):
monkeypatch.setattr(jobs, "settings", SimpleNamespace(heavy_job_concurrency_limit=value))
def test_only_the_configured_number_of_full_jobs_run_at_once_and_the_rest_wait(monkeypatch):
_limit(monkeypatch, 2)
with setup_db()() as session:
agents = _agents(session, 4)
queued = [jobs.create_job(session, a, "INITIAL_SYNC") for a in agents]
claimed = [jobs.claim_next_job(session, a) for a in agents]
assert [c is not None for c in claimed] == [True, True, False, False] # 2 running, 2 queued (backpressure)
assert [session.get(Job, q.id).status for q in queued] == ["RUNNING", "RUNNING", "PENDING", "PENDING"]
jobs.complete_job(session, session.get(Job, queued[0].id), {}) # a slot frees up
assert jobs.claim_next_job(session, agents[2]) is not None and jobs.claim_next_job(session, agents[3]) is None
def test_a_light_incremental_job_is_never_held_behind_a_queued_full_job(monkeypatch):
_limit(monkeypatch, 1)
with setup_db()() as session:
busy, waiting = _agents(session, 2)
jobs.create_job(session, busy, "REPROCESS_ALL")
assert jobs.claim_next_job(session, busy) is not None
full = jobs.create_job(session, waiting, "REPROCESS_ALL")
light = jobs.create_job(session, waiting, "REPROCESS_PRODUCTS", {"codigos": ["1"]})
claimed = jobs.claim_next_job(session, waiting)
assert claimed is not None and claimed.id == light.id and session.get(Job, full.id).status == "PENDING"
def test_limit_zero_disables_backpressure_and_a_stale_running_job_does_not_hold_a_slot(monkeypatch):
from datetime import datetime, timedelta, timezone
_limit(monkeypatch, 0)
with setup_db()() as session:
agents = _agents(session, 3)
for a in agents:
jobs.create_job(session, a, "INITIAL_SYNC")
assert all(jobs.claim_next_job(session, a) is not None for a in agents)
_limit(monkeypatch, 1)
with setup_db()() as session:
dead, fresh = _agents(session, 2)
stale = jobs.create_job(session, dead, "INITIAL_SYNC")
session.execute(Job.__table__.update().where(Job.id == stale.id).values(status="RUNNING", started_at=datetime.now(timezone.utc) - timedelta(hours=3)))
session.commit()
jobs.create_job(session, fresh, "INITIAL_SYNC")
assert jobs.claim_next_job(session, fresh) is not None # the dead Agent's 3 h old RUNNING job does not block the queue
@@ -0,0 +1,9 @@
def test_missing_historical_identity_is_preserved_not_guessed():
from central.app.ai.historical_conflicts import INSUFFICIENT_EVIDENCE, reconstruct_historical_conflicts
report = reconstruct_historical_conflicts({"products": [{"gtin": "x", "fingerprint": "f"}]}, historical_count=73)
assert report["historical_conflicts_before"] == 73
assert report["historical_conflicts_resolved"] == 0
assert report["historical_conflicts_remaining"] == 73
assert report["classification_counts"] == {INSUFFICIENT_EVIDENCE: 73}
assert all(row["resolution"] == "PRESERVE_CONFLICT" for row in report["conflicts"])
+58
View File
@@ -0,0 +1,58 @@
from central.app.icms_context import csosn_v2, product_role, st_conflict_class, st_status, withheld_status
DOM = {"102", "500", "101", "400"}
def test_cest_candidate_never_confirms_st():
assert st_status("EXACT_NCM", "MVA_CANDIDATES", "CEST_CANDIDATES", "", None, 4)[0] == "ST_CANDIDATE"
assert st_status("EXACT_NCM", "MVA_RESOLVED", "CEST_CANDIDATES", "", "03.021.03", 4)[0] == "ST_CANDIDATE"
def test_st_confirmed_needs_erp_cest_corroboration():
assert st_status("EXACT_NCM", "MVA_RESOLVED", "CEST_CANDIDATES", "0302103", "03.021.03", 4)[0] == "ST_CONFIRMED"
assert st_status("EXACT_NCM", "MVA_RESOLVED", "CEST_CONFIRMED", "", "03.021.03", 4)[0] == "ST_CONFIRMED"
def test_st_not_applicable_needs_both_lists_silent_and_valid_ncm():
assert st_status("EXACT_NCM", "NO_MVA_MAPPING_FOUND", "NO_CEST_MAPPING_FOUND", None, None, None)[0] == "ST_NOT_APPLICABLE"
assert st_status("EXACT_NCM", "NO_MVA_MAPPING_FOUND", "CEST_CANDIDATES", None, None, None)[0] == "ST_UNKNOWN"
assert st_status("LEGACY_NCM", "NO_MVA_MAPPING_FOUND", "NO_CEST_MAPPING_FOUND", None, None, None)[0] == "ST_UNKNOWN"
def test_withholding_needs_positive_purchase_evidence():
assert withheld_status([])[0] == "ST_WITHHOLDING_UNKNOWN"
assert withheld_status([{"icms_retido": "N", "valor_retido": 0, "sub_valor": 0}])[0] == "ST_ALREADY_WITHHELD_NOT_CONFIRMED"
assert withheld_status([{"icms_retido": "S", "valor_retido": 0, "sub_valor": 0}])[0] == "ST_ALREADY_WITHHELD_CONFIRMED"
assert withheld_status([{"icms_retido": "N", "valor_retido": 5, "sub_valor": 0}])[0] == "ST_ALREADY_WITHHELD_CONFIRMED"
def test_role_uses_objective_evidence_only():
assert product_role("MATERIA-PRIMA", False, False, False, False, False)[0] == "INPUT"
assert product_role("VENDA", True, False, False, False, False)[0] == "OWN_PRODUCTION"
assert product_role("VENDA", False, False, False, False, True)[0] == "RESALE"
assert product_role("VENDA", False, False, False, False, False)[0] == "UNKNOWN"
assert product_role("VENDA", True, False, False, False, True)[0] == "UNKNOWN"
def test_csosn_never_assumes_102_and_never_writes():
for st in ("ST_CANDIDATE", "ST_UNKNOWN"):
r = csosn_v2("102", "102", "EXACT_NCM", st, "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)
assert r["status"] == "CSOSN_CONTEXT_REQUIRED" and r["proposed_csosn"] is None
r = csosn_v2("102", "102", "EXACT_NCM", "ST_NOT_APPLICABLE", "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)
assert r["status"] == "CSOSN_CANDIDATE" and not r["write_eligible"]
assert csosn_v2("102", "102", "EXACT_NCM", "ST_NOT_APPLICABLE", "ST_WITHHOLDING_UNKNOWN", "UNKNOWN", DOM)["status"] == "CSOSN_CONTEXT_REQUIRED"
assert csosn_v2("102", "102", "LEGACY_NCM", "ST_NOT_APPLICABLE", "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)["status"] == "CSOSN_UNRESOLVED"
def test_csosn_resolved_only_with_st_withholding_and_role_proven():
r = csosn_v2("102", "102", "EXACT_NCM", "ST_CONFIRMED", "ST_ALREADY_WITHHELD_CONFIRMED", "RESALE", DOM)
assert r["status"] == "CSOSN_RESOLVED" and r["proposed_csosn"] == "500" and r["write_eligible"] is False
assert csosn_v2("102", "102", "EXACT_NCM", "ST_CONFIRMED", "ST_WITHHOLDING_UNKNOWN", "RESALE", DOM)["status"] == "CSOSN_CONTEXT_REQUIRED"
assert csosn_v2("500", "102", "EXACT_NCM", "ST_CONFIRMED", "ST_ALREADY_WITHHELD_NOT_CONFIRMED", "RESALE", DOM)["status"] == "CSOSN_CONFLICT"
def test_conflict_classes():
assert st_conflict_class("CEST_CANDIDATES", "MVA_RESOLVED", 4, "", "03.021.03", "5102", True, False)[0] == "ERP_HISTORY_CONFLICT"
assert st_conflict_class(None, "NO_MVA_MAPPING_FOUND", None, "", None, "5405", False, True)[0] == "BA_ST_RULE_CONFLICT"
assert st_conflict_class("CEST_DESCRIPTION_MISMATCH", "MVA_RESOLVED", 6, "", None, "5102", False, False)[0] == "DESCRIPTION_MISMATCH"
assert st_conflict_class(None, "MVA_RESOLVED", 6, "", None, "5102", False, False)[0] == "NO_EVIDENCE"
@@ -0,0 +1,427 @@
"""Install-code enrollment flow: POST /v1/agents/enroll with install_code.
Covers the universal-EXE enrollment path — the operator types a 16-char
XXXX-XXXX-XXXX-XXXX installation code into Setup.exe, the Hub resolves the
installation by the hash on its own (no installation_id, no enrollment.json),
issues the permanent agent credential, and exhausts the code (replay-proof).
Also covers rejection when code and legacy token are sent together, expiry,
attempts/rate-limit and no-plaintext persistencen.
"""
import dataclasses
import json
import sys
import tempfile
import zipfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from central.app import config, enrollment, rate_limit, release_storage
from central.app.main import create_app
from central.app.models import Agent, AgentEnrollment, AgentRelease, AuditEvent, Installation, Tenant
from central.scripts.publish_agent_release import publish
from central.tests.test_control_plane import seed, setup_db
VERSION = "1.1.1"
@pytest.fixture(autouse=True)
def clear_rate_limit():
rate_limit._attempts.clear()
yield
rate_limit._attempts.clear()
@pytest.fixture
def hub(tmp_path, monkeypatch):
replaced = dataclasses.replace(
config.settings,
agent_release_storage_path=str(tmp_path / "releases"),
agent_enrollment_ttl_seconds=86400,
base_url="https://gtifiscal.example.gti",
web_cookie_secure=False,
)
monkeypatch.setattr(config, "settings", replaced)
if "app.config" in sys.modules:
monkeypatch.setattr(sys.modules["app.config"], "settings", replaced)
factory = setup_db()
seed(factory)
client = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None))
return client, factory
def login(client):
response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
assert response.status_code == 303
def csrf(client):
return client.cookies.get("gti_csrf", "")
@pytest.fixture
def release_id(hub, tmp_path):
_client, factory = hub
data = io_bytes_tmp(tmp_path, VERSION)
with factory() as session:
release, _created = publish(session, data, version=VERSION)
return release.id
def io_bytes_tmp(tmp_path, version):
from pathlib import Path
buf = io_bytes_bundle(version)
path = Path(tmp_path) / f"GTIFiscalAgent-{version}.zip"
path.write_bytes(buf)
return path
def io_bytes_bundle(version):
import io
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr(f"GTIFiscalAgent-{version}/gti-agent.ps1", "# fake\n")
z.writestr(f"GTIFiscalAgent-{version}/VERSION", version)
return buf.getvalue()
def tenant_id(factory):
with factory() as session:
tenant = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
# /agents/new only offers active GTI_GERENCIADOR-sourced tenants
# (central/app/client_sync.py) — flip the shared fixture tenant so
# existing form-based tests keep working under that rule.
if tenant.source != "GTI_GERENCIADOR" or not tenant.active:
tenant.source, tenant.external_client_id, tenant.active = "GTI_GERENCIADOR", tenant.id, True
session.commit()
return tenant.id
def make_installation(factory, release_id, name="Servidor E2E"):
with factory() as session:
enr = AgentEnrollment(enrollment_uuid=str(__import__("uuid").uuid4()), tenant_id=tenant_id(factory),
display_name=name, release_id=release_id, status="PENDING")
session.add(enr)
session.commit()
return enr.enrollment_uuid
def set_code(factory, uuid, code, ttl_seconds=86400):
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
enr.install_code_hash = enrollment.install_code_digest(code)
enr.install_code_expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)
enr.install_code_created_at = datetime.now(timezone.utc)
session.commit()
def audits(factory):
with factory() as session:
return session.scalars(select(AuditEvent)).all()
# ---------- happy path: code alone resolves the installation ----------
def test_enroll_with_install_code_alone_creates_agent(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
enrolled = client.post("/v1/agents/enroll", json={"install_code": code, "agent_version": "1.1.1", "hostname": "WIN-E2E", "platform": "windows"})
assert enrolled.status_code == 200
body = enrolled.json()
assert body["agent_id"] and body["agent_token"] and body["hostname_mismatch"] is False
assert body["installation_id"] == uuid
with factory() as session:
agent = session.get(Agent, body["agent_id"])
assert agent.enabled and agent.agent_version == "1.1.1"
installation = session.scalar(select(Installation).where(Installation.agent_id == agent.id))
assert installation.installation_id == uuid and installation.hostname == "WIN-E2E"
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
assert enr.status == "ENROLLED" and enr.agent_id == agent.id
assert enr.install_code_hash is None and enr.install_code_expires_at is None # code consumed, replay-proof
def test_replay_of_consumed_code_is_rejected(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
first = client.post("/v1/agents/enroll", json={"install_code": code})
assert first.status_code == 200
second = client.post("/v1/agents/enroll", json={"install_code": code})
assert second.status_code == 401
events = {e.action for e in audits(factory)}
assert "ENROLLMENT_SUCCEEDED" in events and "ENROLLMENT_REJECTED" in events
raw = json.dumps({"action": "ENROLLMENT_SUCCEEDED", "target": ["x"], "context": {"ip": "y"}}) + code
# the code plaintext is never in any audit event context
for event in audits(factory):
assert code not in json.dumps(event.context)
assert enrollment.normalize_install_code(code) not in json.dumps(event.context)
def test_code_is_case_and_hyphen_insensitive(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
typed = code.lower().replace("-", " ")
enrolled = client.post("/v1/agents/enroll", json={"install_code": typed})
assert enrolled.status_code == 200
# ---------- rejections ----------
def test_invalid_code_format_rejected_without_lookup(hub, release_id):
client, factory = hub
invalid = client.post("/v1/agents/enroll", json={"install_code": "NOT-A-CODE"})
assert invalid.status_code == 400 or invalid.status_code == 401
assert "INVALID" in str(invalid.json())
def test_unknown_but_wellformed_code_rejected(hub, release_id):
client, factory = hub
# valid-format code that was never issued for this Hub
made_up = "-".join(["0123"] * 4)
rejected = client.post("/v1/agents/enroll", json={"install_code": made_up})
assert rejected.status_code == 401
assert "INVALID" in str(rejected.json())
def test_expired_code_rejected(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code, ttl_seconds=-60)
rejected = client.post("/v1/agents/enroll", json={"install_code": code})
assert rejected.status_code == 401
assert "EXPIRED" in str(rejected.json())
def test_code_for_revoked_installation_rejected(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
enr.status = "REVOKED"
session.commit()
rejected = client.post("/v1/agents/enroll", json={"install_code": code})
assert rejected.status_code == 401
assert "REVOKED" in str(rejected.json())
def test_code_and_token_together_rejected(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
rejected = client.post("/v1/agents/enroll", json={
"install_code": code, "installation_id": uuid, "enrollment_token": "x" * 43,
})
assert rejected.status_code == 400
def test_failed_attempts_increment_and_rate_limit(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
# several wrong-format tries that never reach the code lookup
for _ in range(12):
client.post("/v1/agents/enroll", json={"install_code": "NOT-A-CODE"})
assert rate_limit.blocked("enroll", "testclient")
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
assert enr.install_code_attempts >= 0 # format failures don't consume this installation's code attempts
def test_wrong_code_rejected_as_invalid(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
right_code = enrollment.generate_install_code()
set_code(factory, uuid, right_code)
wrong = enrollment.generate_install_code() # well-formed but never issued
attempt = client.post("/v1/agents/enroll", json={"install_code": wrong})
assert attempt.status_code == 401
assert "INVALID" in str(attempt.json())
def test_resolved_code_failures_increment_attempt_budget(hub, release_id):
# A well-formed code that resolves to a REVOKED installation consumes the
# installation's attempt budget (per-installation rate limit), same as an
# expired one.
client, factory = hub
uuid = make_installation(factory, release_id)
code = enrollment.generate_install_code()
set_code(factory, uuid, code)
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
enr.status = "REVOKED"
session.commit()
for _ in range(3):
client.post("/v1/agents/enroll", json={"install_code": code})
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
assert enr.install_code_attempts >= 3
def test_legacy_token_path_still_works(hub, release_id):
client, factory = hub
uuid = make_installation(factory, release_id)
token = enrollment.new_token()
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == uuid))
enr.enrollment_token_hash = enrollment.token_digest(token)
enr.enrollment_token_expires_at = datetime.now(timezone.utc) + timedelta(seconds=86400)
session.commit()
enrolled = client.post("/v1/agents/enroll", json={"installation_id": uuid, "enrollment_token": token})
assert enrolled.status_code == 200
# ---------- publish EXE releases (immutable) ----------
def test_publish_exe_release(hub, tmp_path):
client, factory = hub
from central.app import release_storage
exe_bytes = b"MZ\x90\x00 fake windows installer exe body \x00" * 100
exe_path = Path(tmp_path) / "GTIFiscalAgent-Setup-1.1.1.exe"
exe_path.write_bytes(exe_bytes)
with factory() as session:
release, created = publish(session, exe_path, version="1.1.1", notes="universal installer")
assert created and release.status == "PUBLISHED" and release.published_at is not None
assert release.filename == "GTIFiscalAgent-Setup-1.1.1.exe"
assert release_storage.safe_path_for(release.storage_path).read_bytes() == exe_bytes
def test_publish_exe_is_immutable(hub, tmp_path):
_client, factory = hub
exe_path = Path(tmp_path) / "GTIFiscalAgent-Setup-1.1.1.exe"
exe_path.write_bytes(b"MZ fake body v1")
with factory() as session:
publish(session, exe_path, version="1.1.1")
exe_path.write_bytes(b"MZ DIFFERENT body v2")
with factory() as session:
with pytest.raises(SystemExit):
publish(session, exe_path, version="1.1.1") # refused: immutable once PUBLISHED
def test_publish_identical_exe_is_noop(hub, tmp_path):
_client, factory = hub
exe_path = Path(tmp_path) / "GTIFiscalAgent-Setup-1.1.1.exe"
exe_path.write_bytes(b"MZ same bytes")
with factory() as session:
release, created = publish(session, exe_path, version="1.1.1")
assert created
with factory() as session:
release2, created2 = publish(session, exe_path, version="1.1.1")
assert release2.id == release.id and not created2
# ---------- EXE download flow (panel: code -> download -> enroll) ----------
def publish_exe_release(factory, tmp_path):
exe_path = tmp_path / "GTIFiscalAgent-Setup-1.1.1.exe"
exe_path.write_bytes(b"MZ\x90\x00 GTIFiscalAgent installer 1.1.1 \x00" * 40)
with factory() as session:
release, _created = publish(session, exe_path, version="1.1.1")
return release.id
def test_panel_flow_code_generate_download_and_enroll_with_exe(hub, tmp_path):
client, factory = hub
release_id = publish_exe_release(factory, tmp_path)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Servidor EXE", "release_id": release_id}, follow_redirects=False)
assert created.status_code == 303
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
# generate the install code (only hash stored; plaintext shown once via ?code=)
code_page = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True)
assert code_page.status_code == 200
# the redirect carried ?code=XXXX-... ; the page must show it
code_addr = code_page.history[0].headers.get("location") if code_page.history else ""
assert "code=" in code_addr
install_code = code_addr.rsplit("code=", 1)[-1].split("&")[0]
assert len(install_code) == 19
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid))
assert enr.install_code_hash == enrollment.install_code_digest(install_code)
assert enr.install_code_expires_at is not None
assert enr.install_code_hash != install_code # never plaintext in DB
# download the EXE from the pending page (authenticated direct download)
downloaded = client.get(f"/agents/pending/{installation_uuid}/download")
assert downloaded.status_code == 200
assert downloaded.content.startswith(b"MZ")
assert "attachment" in downloaded.headers["content-disposition"]
assert "GTIFiscalAgent-Setup-1.1.1.exe" in downloaded.headers["content-disposition"]
# enroll with just the code (no installation id, no token)
anon = TestClient(client.app)
enrolled = anon.post("/v1/agents/enroll", json={"install_code": install_code, "agent_version": "1.1.1", "hostname": "WIN-EXE"})
assert enrolled.status_code == 200
body = enrolled.json()
assert body["agent_id"] and body["agent_token"]
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid))
assert enr.status == "ENROLLED" and enr.install_code_hash is None # consumed
agent = session.get(Agent, body["agent_id"])
assert agent.enabled and agent.agent_version == "1.1.1"
events = {e.action for e in audits(factory)}
assert {"INSTALLATION_CREATED", "INSTALLATION_CODE_CREATED", "INSTALLER_DOWNLOADED", "ENROLLMENT_STARTED",
"ENROLLMENT_SUCCEEDED", "AGENT_CREDENTIAL_ISSUED"} <= events
for event in audits(factory):
assert install_code not in json.dumps(event.context)
def test_rotation_generates_a_code_that_invalidates_the_previous(hub, tmp_path):
client, factory = hub
release_id = publish_exe_release(factory, tmp_path)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Rotação", "release_id": release_id}, follow_redirects=False)
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
first = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True)
first_code = first.history[0].headers["location"].rsplit("code=", 1)[-1].split("&")[0]
second = client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)}, follow_redirects=True)
second_code = second.history[0].headers["location"].rsplit("code=", 1)[-1].split("&")[0]
assert first_code != second_code
# first code no longer resolves (rotated), second does
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid))
assert enr.install_code_hash == enrollment.install_code_digest(second_code)
anon = TestClient(client.app)
assert anon.post("/v1/agents/enroll", json={"install_code": first_code}).status_code == 401
assert anon.post("/v1/agents/enroll", json={"install_code": second_code}).status_code == 200
events = [e.action for e in audits(factory)]
assert events.count("INSTALLATION_CODE_CREATED") >= 2
def test_revoke_clears_code_and_is_audited(hub, tmp_path):
client, factory = hub
release_id = publish_exe_release(factory, tmp_path)
login(client)
created = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": tenant_id(factory),
"display_name": "Revoga", "release_id": release_id}, follow_redirects=False)
installation_uuid = created.headers["location"].rsplit("/", 1)[-1]
client.post(f"/agents/pending/{installation_uuid}/code", data={"csrf_token": csrf(client)})
revoked = client.post(f"/agents/pending/{installation_uuid}/revoke", data={"csrf_token": csrf(client)}, follow_redirects=False)
assert revoked.status_code == 303
with factory() as session:
enr = session.scalar(select(AgentEnrollment).where(AgentEnrollment.enrollment_uuid == installation_uuid))
assert enr.status == "REVOKED" and enr.install_code_hash is None
events = {e.action for e in audits(factory)}
assert "INSTALLATION_CODE_CREATED" in events and "INSTALLATION_CODE_REVOKED" in events and "INSTALLATION_REVOKED" in events
@@ -0,0 +1,66 @@
"""Installation code (install_code) for the Windows installer EXE flow.
Covers central/app/enrollment.py install-code functions: Crockford Base32
generation, normalization (case + hyphen), hashing, and the grouped display
format. Never stores the plaintext code.
"""
import re
import pytest
from central.app import enrollment
def test_generated_code_is_16_crockford_chars_in_4_groups():
for _ in range(50):
code = enrollment.generate_install_code()
assert re.fullmatch(r"[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}", code)
def test_generated_code_has_good_entropy():
seen = {enrollment.generate_install_code() for _ in range(2000)}
assert len(seen) > 1900 # 80 bits of entropy: collisions essentially impossible
def test_crockford_alphabet_excludes_ambiguous_chars():
alphabet = set(enrollment.INSTALL_CODE_ALPHABET)
for ambiguous in "ILOU":
assert ambiguous not in alphabet
def test_normalization_uppercases_and_strips_separators():
assert enrollment.normalize_install_code("abcd-efgh-jkmn-pqrz") == "ABCDEFGHJKMNPQRZ"
assert enrollment.normalize_install_code(" abcd efgh jkmn pqrz ") == "ABCDEFGHJKMNPQRZ"
assert enrollment.normalize_install_code("ABCD-EFGH-JKMN-PQRZ") == "ABCDEFGHJKMNPQRZ"
def test_normalization_maps_crockford_lookalikes():
# Crockford Base32 treats 0/O and 1/I/L as transliterations; a human
# retyping "O" and "I/L" means "0" and "1".
assert enrollment.normalize_install_code("OOOI-LLLL-0000-0001") == "0001111100000001"
assert enrollment.normalize_install_code("0O0O-1L1I-0000-0001") == enrollment.normalize_install_code("0000-1111-0000-0001")
def test_digest_is_sha256_hex_and_deterministic():
code = enrollment.generate_install_code()
normalized = enrollment.normalize_install_code(code)
assert enrollment.install_code_digest(code) == enrollment.install_code_digest(normalized)
assert enrollment.install_code_digest(code) == enrollment.install_code_digest(code.lower().replace("-", ""))
assert len(enrollment.install_code_digest(code)) == 64
def test_hashes_cannot_be_reversed_into_plaintext():
code = enrollment.generate_install_code()
digest = enrollment.install_code_digest(code)
assert digest != code
assert code not in digest
def test_valid_install_code():
code = enrollment.generate_install_code()
assert enrollment.valid_install_code(code)
assert enrollment.valid_install_code(code.lower().replace("-", ""))
assert not enrollment.valid_install_code("")
assert not enrollment.valid_install_code("ABCD-EFGH-JKMN-PQRU") # U excluded from Crockford
assert not enrollment.valid_install_code("ABCD")
assert not enrollment.valid_install_code("!BCD-EFGH-JKMN-PQRZ")
+214
View File
@@ -0,0 +1,214 @@
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.jobs import claim_next_job, complete_job, create_job, fail_job
from central.app.main import create_app
from central.app.models import Agent, Base, BlockedProduct, Installation, Job, Tenant
def _make_agent(session):
tenant = Tenant(name="development")
session.add(tenant); session.flush()
agent = Agent(tenant_id=tenant.id, name="agent-a", token_hash="hash-a")
other = Agent(tenant_id=tenant.id, name="agent-b", token_hash="hash-b")
session.add_all([agent, other]); session.commit()
return agent, other
def test_claim_is_scoped_to_owning_agent_and_marks_running():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, other = _make_agent(session)
job = create_job(session, agent, "REPROCESS_ALL")
assert claim_next_job(session, other) is None # other agent never sees it
claimed = claim_next_job(session, agent)
assert claimed.id == job.id
assert claimed.status == "RUNNING"
assert claimed.attempts == 1
def test_claim_is_not_reentrant_once_running():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, _ = _make_agent(session)
create_job(session, agent, "REPROCESS_ALL")
first = claim_next_job(session, agent)
second = claim_next_job(session, agent) # repeated polling must not duplicate consumption
assert first is not None
assert second is None
def test_completed_job_can_never_run_again():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, _ = _make_agent(session)
create_job(session, agent, "REPROCESS_ALL")
job = claim_next_job(session, agent)
assert complete_job(session, job, {"writes": 0}) is True
assert session.get(Job, job.id).status == "COMPLETED"
assert claim_next_job(session, agent) is None
assert complete_job(session, job, {"writes": 1}) is False # already terminal
def test_failed_job_is_terminal_with_sanitized_error():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
agent, _ = _make_agent(session)
create_job(session, agent, "REPROCESS_ALL")
job = claim_next_job(session, agent)
assert fail_job(session, job, "boom " * 1000) is True
stored = session.get(Job, job.id)
assert stored.status == "FAILED"
assert len(stored.error) <= 2000
def _client():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
return TestClient(app), engine
def test_job_api_end_to_end_flow():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
heartbeat = client.post("/v1/agents/heartbeat", json={"status": "IDLE", "schema_fingerprint": "abc", "agent_version": "0.1.0"}, headers=headers)
assert heartbeat.status_code == 200
assert heartbeat.json()["enabled"] is True
# Zero-touch: the first heartbeat makes the Hub queue exactly one INITIAL_SYNC by itself.
initial = client.get("/v1/jobs/next", headers=headers).json()
assert initial["kind"] == "INITIAL_SYNC"
assert client.post(f"/v1/jobs/{initial['job_id']}/complete", json={"result_summary": {}}, headers=headers).json()["status"] == "COMPLETED"
created = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {"codigos": ["000026"]}}, headers=headers)
assert created.status_code == 200
job_id = created.json()["job_id"]
assert created.json()["status"] == "PENDING"
claimed = client.get("/v1/jobs/next", headers=headers)
assert claimed.json()["job_id"] == job_id
assert claimed.json()["status"] == "RUNNING"
empty = client.get("/v1/jobs/next", headers=headers)
assert empty.json() is None # no more pending work; not the same job again
completed = client.post(f"/v1/jobs/{job_id}/complete", json={"result_summary": {"writes_succeeded": 1}}, headers=headers)
assert completed.json()["status"] == "COMPLETED"
assert completed.json()["result_summary"]["writes_succeeded"] == 1
fetched = client.get(f"/v1/jobs/{job_id}", headers=headers)
assert fetched.json()["status"] == "COMPLETED"
def test_agent_status_derives_online_offline():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
offline = client.get("/v1/agents/me", headers=headers)
assert offline.json()["status"] == "OFFLINE" # never heartbeated yet
client.post("/v1/agents/heartbeat", json={"status": "IDLE"}, headers=headers)
online = client.get("/v1/agents/me", headers=headers)
body = online.json()
assert body["status"] == "ONLINE"
assert body["enabled"] is True
assert "company_id" in body
def test_agent_identity_endpoint_returns_existing_installation_identity_only():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
installation_id = "00000000-0000-4000-8000-000000000222"
heartbeat = client.post(
"/v1/agents/heartbeat",
json={
"installation_id": installation_id,
"hostname": "DESKTOP-B0HL02I",
"agent_version": "1.1.1",
"status": "IDLE",
},
headers=headers,
)
assert heartbeat.status_code == 200
response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
assert response.json() == {
"agent_id": heartbeat.json()["agent_id"],
"installation_id": installation_id,
"hostname": "DESKTOP-B0HL02I",
"version": "1.1.1",
"status": "ONLINE",
}
def test_agent_identity_endpoint_returns_null_installation_without_creating_one():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.get("/v1/agents/me/identity", headers=headers)
assert response.status_code == 200
body = response.json()
assert body == {
"agent_id": body["agent_id"],
"installation_id": None,
"hostname": None,
"version": None,
"status": "OFFLINE",
}
assert isinstance(body["agent_id"], int) and body["agent_id"] > 0
with Session(engine) as session:
assert session.query(Installation).count() == 0
def test_agent_identity_endpoint_requires_agent_authentication():
client, _ = _client()
response = client.get("/v1/agents/me/identity")
assert response.status_code == 401
def test_job_creation_rejects_unknown_kind():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.post("/v1/jobs", json={"kind": "DELETE_EVERYTHING"}, headers=headers)
assert response.status_code == 400
def test_reprocess_products_requires_codigos():
client, _ = _client()
headers = {"Authorization": "Bearer test-token"}
response = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {}}, headers=headers)
assert response.status_code == 400
def test_blocker_registry_api_deduplicates_and_resolves():
client, engine = _client()
headers = {"Authorization": "Bearer test-token"}
assert client.post("/v1/agents/heartbeat", json={"status": "IDLE", "installation_id": "00000000-0000-4000-8000-000000000001"}, headers=headers).status_code == 200
summary = {"blocked_products": [{"company": "C", "product_code": "P1", "blocker_type": "BLOCKED_MISSING_EVIDENCE", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "v1", "evidence_summary": {"source": "ERP"}}]}
created = client.post("/v1/jobs", json={"kind": "REPROCESS_PRODUCTS", "payload": {"codigos": ["P1"]}}, headers=headers)
job_id = created.json()["job_id"]
client.get("/v1/jobs/next", headers=headers)
client.post(f"/v1/jobs/{job_id}/complete", json={"result_summary": summary}, headers=headers)
listed = client.get("/v1/blockers?field=CFOP", headers=headers)
assert listed.status_code == 200
assert len(listed.json()["blockers"]) == 1
with Session(engine) as session:
assert session.query(BlockedProduct).one().installation_id is not None
blocker_id = listed.json()["blockers"][0]["id"]
resolved = client.post(f"/v1/blockers/{blocker_id}/resolve", json={"resolution_source": "TEST"}, headers=headers)
assert resolved.json()["resolved"] is True
assert client.get("/v1/blockers", headers=headers).json()["blockers"] == []
@@ -0,0 +1,84 @@
"""Regression for the 2026-09-27 production incident: an ADMIN attested the right NCM on the WRONG (but very similar) KB product — "COCA ZERO LATA 350 ML"
instead of plain "COCA LATA 350 ML" — because both appeared as CONFLICT with an overlapping candidate list. The pipeline behaved correctly (no write for
the unresolved product); the mistake was purely a fiscal data-entry error. These tests lock in: (1) two locally-created Firebird rows with the SAME
description never share KB identity through anything but the KB product itself; (2) attesting one GTIN group never resolves an unrelated, distinct one,
even when their aliases and candidate NCMs overlap; (3) EAN_AUSENTE is never a terminal state — it becomes resolvable the moment its OWN GTIN group gets
verified; (4) a stale (rejected) attest target does not block a later, correct attest.
"""
from sqlalchemy import select
from central.app.enrichment import history, kb
from central.app.enrichment.quota import ProviderGateway
from central.app.enrichment.resolver import resolve
from central.app.models import KbProduct, ProductEnrichment
from central.tests.test_enrichment_history import run as seed_history, staging
from central.tests.test_product_enrichment_api import H, setup
from central.tests.test_product_enrichment_resolver import env # noqa: F401
def gtin(body12):
total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(body12)))
return body12 + str((10 - total % 10) % 10)
REGULAR, ZERO = gtin("789490001000"), gtin("789490070000")
def _seed_two_similar_products(env):
seed_history(env, [staging("RINCAO", REGULAR, "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"),
staging("UNIAO", REGULAR, "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")])
seed_history(env, [staging("RINCAO", ZERO, "19059090", years={2024: 900, 2025: 900}, desc="COCA ZERO LATA 350 ML"),
staging("UNIAO", ZERO, "22021000", years={2024: 40, 2025: 30}, desc="REFRIGERANTE COCA COLA ZERO LATA 350 ML")])
def test_attesting_the_wrong_similar_gtin_leaves_the_intended_product_unresolved(env):
_seed_two_similar_products(env)
cur = history.official_sets(env)
kb.attest(env, "0" + ZERO, "22021000", "Admin", lambda v: v in cur) # attest the WRONG one, as happened in production
result = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA-COLA ORIGINAL LATA 350 ML", current={})
assert result["fields"]["CLASSIFICACAO_FISCAL"]["status"] != "AUTO_FILL_SAFE" # still not resolved: attesting a DIFFERENT KB product proves nothing here
kb.attest(env, "0" + REGULAR, "22021000", "Admin", lambda v: v in cur) # attest the CORRECT one - same rule, correct target
result = resolve(env, ProviderGateway(env, []), gtin_raw="", description="COCA-COLA ORIGINAL LATA 350 ML", current={})
assert result["state"] == "EXACT_KB_NAME_VERIFIED" and result["fields"]["CLASSIFICACAO_FISCAL"]["value"] == "22021000"
def test_ean_ausente_is_not_terminal_becomes_resolved_once_its_own_gtin_is_verified(monkeypatch):
client, engine = setup(monkeypatch)
from sqlalchemy.orm import Session
with Session(engine) as s:
_seed_two_similar_products(s)
s.commit()
body = {"product_code": "P1", "gtin": "", "description": "COCA-COLA ORIGINAL LATA 350 ML", "current": {}}
first = client.post("/v1/products/resolve", json=body, headers=H).json()
with Session(engine) as s:
row = s.scalars(select(ProductEnrichment).where(ProductEnrichment.product_code == "P1")).one()
assert row.ui_status == "EAN_AUSENTE"
cur = history.official_sets(s)
kb.attest(s, "0" + REGULAR, "22021000", "Admin", lambda v: v in cur)
from central.app.enrichment.status import mark_for_reevaluation
inst_id = row.installation_id
mark_for_reevaluation(s, inst_id)
s.commit()
assert s.scalars(select(ProductEnrichment).where(ProductEnrichment.product_code == "P1")).one().reevaluate is True
second = client.post("/v1/products/resolve", json=body, headers=H).json()
assert second["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE" # EAN_AUSENTE was never a dead end
with Session(engine) as s:
assert s.scalars(select(ProductEnrichment).where(ProductEnrichment.product_code == "P1")).one().ui_status == "PRONTO_PARA_PREENCHER"
def test_two_local_products_with_the_identical_description_both_resolve_from_one_kb_verification(monkeypatch):
"""Mirrors production 001437/001439: two DIFFERENT Firebird rows, same description, no EAN. One KB verification resolves BOTH (they resolve
independently, by name, not through any shared local-row identity)."""
client, engine = setup(monkeypatch)
from sqlalchemy.orm import Session
with Session(engine) as s:
_seed_two_similar_products(s)
cur = history.official_sets(s)
kb.attest(s, "0" + REGULAR, "22021000", "Admin", lambda v: v in cur)
s.commit()
for code in ("A1", "A2"):
body = {"product_code": code, "gtin": "", "description": "COCA-COLA ORIGINAL LATA 350 ML", "current": {}}
r = client.post("/v1/products/resolve", json=body, headers=H).json()
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE" and r["fields"]["CLASSIFICACAO_FISCAL"]["value"] == "22021000"
with Session(engine) as s:
assert s.query(KbProduct).count() == 2 # regular and ZERO stayed two distinct KB identities throughout
@@ -0,0 +1,31 @@
def test_canonical_identity_excludes_tenant_fiscal_context():
from central.app.ai.kb_consolidation import canonical_product_id
a = canonical_product_id({"gtin": "7891234567895", "ncm": "22011000", "database": "A"})
b = canonical_product_id({"gtin": "7891234567895", "ncm": "22019000", "database": "B"})
assert a == b == "GTIN:7891234567895"
def test_conflicts_are_conservative():
from central.app.ai.kb_consolidation import NCM_CONFLICT, PACKAGING_VARIATION, classify_conflict
assert classify_conflict([
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA MINERAL", "package_quantity": "500"},
{"gtin": "7891234567895", "ncm": "22019000", "normalized_description": "AGUA MINERAL", "package_quantity": "500"},
]) == NCM_CONFLICT
assert classify_conflict([
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA MINERAL", "package_quantity": "500"},
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA MINERAL", "package_quantity": "1L"},
]) == PACKAGING_VARIATION
def test_payload_audit_reports_observable_coverage():
from central.app.ai.kb_consolidation import audit_payload
report = audit_payload({"products": [
{"gtin": "7891234567895", "ncm": "22011000", "normalized_description": "AGUA", "observations": [{"database": "A"}]},
{"gtin": "7891234567895", "ncm": "22019000", "normalized_description": "AGUA", "observations": [{"database": "B"}]},
]}, historical_conflicts=163)
assert report["historical_conflicts_reported"] == 163
assert report["observable_conflicts_audited"] == 1
assert report["unobservable_historical_conflicts"] == 162
+98
View File
@@ -0,0 +1,98 @@
"""KB search bug fix: the search must be GLOBAL server-side (filter -> count -> order -> paginate), lenient for humans (distinct from the strict
fiscal `kb.alias_match`), and find a product regardless of which page it would fall on. Never touches the fiscal auto-fill rule."""
from sqlalchemy import select
from central.app.enrichment import kb
from central.app.enrichment.observability import kb_search, kb_search_ids
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_product_enrichment_resolver import env # noqa: F401
from central.tests.test_rtc_security import _become, _post
def seed_product(session, gtin, aliases, ncm=None, client="A"):
p = kb.upsert_product(session, gtin, aliases[0])
for a in aliases[1:]:
kb.add_alias(session, p, a)
if ncm:
kb.add_evidence(session, p, "ncm", ncm, "x", family=kb.CLIENT_FAMILY, client_label=client, lineage={"docs": 20, "years": {"2024": 20}})
session.commit()
return p
def gtin(body12):
total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(body12)))
return "0" + body12 + str((10 - total % 10) % 10)
def test_search_is_global_not_limited_to_a_rendered_page(env):
target = seed_product(env, gtin("789100050001"), ["COCA-COLA ORIGINAL LATA 350 ML"], ncm="22021000")
for i in range(30): # unrelated noise: if search were page-limited, the target could fall off the visible slice
seed_product(env, gtin(f"78920005{i:04d}"), [f"PRODUTO IRRELEVANTE {i} CAIXA 1 UN"])
ids = kb_search_ids(env, "COCA COLA LATA 350") # missing the unit "ML": would fail the strict fiscal alias_match
assert target.id in ids
def test_search_finds_product_on_first_middle_and_last_page(env):
target = seed_product(env, gtin("789100050002"), ["COCA-COLA ORIGINAL LATA 350 ML"])
for i in range(25):
seed_product(env, gtin(f"78920006{i:04d}"), [f"COCA-COLA ORIGINAL LATA 350 ML VARIANTE {i}"])
for page in (1, 2, 3):
res = kb_search(env, "COCA COLA LATA 350", page=page, page_size=10)
assert res["total"] == 26 and res["pages"] == 3
found_on_last_page = kb_search(env, "COCA COLA LATA 350", page=3, page_size=10)
assert len(found_on_last_page["results"]) >= 1
def test_search_by_gtin_exact(env):
key = gtin("789100050003")
seed_product(env, key, ["COCA-COLA ORIGINAL LATA 350 ML"])
body = key[1:]
assert kb_search_ids(env, body) and kb_search_ids(env, body)[0] == kb_search_ids(env, key)[0]
assert kb_search_ids(env, "0000000000000") == []
def test_search_by_name_with_and_without_hyphen_and_accents(env):
p = seed_product(env, gtin("789100050004"), ["ACAI ORIGINAL POLPA 100 G"])
for query in ("ACAI POLPA 100", "AÇAÍ POLPA 100", "acai polpa 100"):
assert p.id in kb_search_ids(env, query), query
def test_search_zero_results_for_unknown_product(env):
seed_product(env, gtin("789100050005"), ["FEIJAO CARIOCA 1 KG"])
assert kb_search_ids(env, "PRODUTO QUE NAO EXISTE NA BASE") == []
def test_search_before_pagination_semantics(env):
"""Filter runs over the WHOLE table, THEN the result is paginated - not the other way round."""
matches = [seed_product(env, gtin(f"78910006{i:04d}"), [f"SABONETE LIQUIDO ERVA DOCE {i} 200 ML"]) for i in range(15)]
for i in range(40):
seed_product(env, gtin(f"78910007{i:04d}"), [f"OUTRO PRODUTO {i} QUALQUER"])
res = kb_search(env, "SABONETE LIQUIDO ERVA DOCE", page=2, page_size=10)
assert res["total"] == 15 and res["page"] == 2 and len(res["results"]) == 5
found_ids = {r["gtin_key"] for r in kb_search(env, "SABONETE LIQUIDO ERVA DOCE", page=1, page_size=10)["results"]} | {r["gtin_key"] for r in res["results"]}
assert found_ids == {p.gtin for p in matches}
def test_conflict_candidates_all_visible_and_none_auto_confirmed(env):
seed_product(env, gtin("789100050006"), ["COCA COLA LATA 350 ML"], ncm="22030000", client="RINCAO")
p2 = seed_product(env, gtin("789100050006"), ["REFRIGERANTE COCA COLA LATA 350 ML"], ncm="22021000", client="UNIAO")
# both aliases resolve to the SAME kb product (same GTIN key), so seeding into it twice just adds evidence
ids = kb_search_ids(env, "COCA COLA LATA 350")
assert p2.id in ids
def test_admin_attest_button_authorization_end_to_end():
client, agent_id, _ = _panel_client()
with client._session_factory() as s:
from central.tests.test_enrichment_history import staging
from central.tests.test_enrichment_history import run as seed_history
from central.tests.test_product_enrichment_resolver import seed as seed_ncm_dataset
seed_ncm_dataset(s)
seed_history(s, [staging("RINCAO", "7891000500071", "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"),
staging("UNIAO", "7891000500071", "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")])
_become(client, "PADRAO")
page = client.get("/?kb_q=COCA COLA LATA 350").text
assert "Confirmar este NCM" not in page # button hidden for non-admin
_become(client, "ADMIN")
page = client.get("/?kb_q=COCA COLA LATA 350").text
assert "Confirmar este NCM" in page and "22021000" in page and "19059090" in page
+130
View File
@@ -0,0 +1,130 @@
"""Real LLM provider contract: strict answers, batching safety, cache, metrics attribution."""
import json
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from central.app.models import Base, AiEvalCache
def _item(work_item_id="w1", candidates=None):
from central.app.ai.llm_provider import LlmEvalItem
return LlmEvalItem(work_item_id=work_item_id, description="AGUA MINERAL 500ML", normalized_description="AGUA MINERAL 500ML",
brand=None, unit="ML", segment="mercado", candidates=candidates or [("22011000", "AGUAS MINERAIS", 0.9), ("22019000", "AGUAS OUTRAS", 0.4)])
def _answer(work_item_id="w1", ncm="22011000", confidence=0.9, abstain=False):
return {"work_item_id": work_item_id, "selected_ncm": ncm, "confidence": confidence, "abstain": abstain, "reason_short": "desc match"}
def test_validate_answer_accepts_only_candidate_or_abstain():
from central.app.ai.llm_provider import LlmAnswerError, validate_answer
item = _item()
assert validate_answer(_answer(), item)["selected_ncm"] == "22011000"
abstain = validate_answer(_answer(ncm=None, abstain=True), item)
assert abstain["abstain"] is True and abstain["selected_ncm"] is None
for bad in (_answer(ncm="99999999"), _answer(work_item_id="other"), _answer(ncm=None), _answer(confidence=1.5), _answer(ncm="22011000", abstain=True)):
try:
validate_answer(bad, item)
raise AssertionError("must reject")
except LlmAnswerError:
pass
def test_provider_batch_requires_1_to_1_answers():
from central.app.ai.llm_provider import LlmAnswerError, OpenAICompatibleProvider
def transport(payload):
content = json.dumps([_answer(), _answer()]) # two answers for one item
return {"choices": [{"message": {"content": content}}], "usage": {"prompt_tokens": 10, "completion_tokens": 5}}
provider = OpenAICompatibleProvider(api_key="test-key", model="m", transport=transport)
try:
provider.select_batch([_item()])
raise AssertionError("must reject")
except LlmAnswerError:
pass
assert provider.http_calls == 1 # contract violations are NOT retried
def test_provider_retries_transport_failures_and_counts_usage():
from central.app.ai.llm_provider import OpenAICompatibleProvider
calls = {"n": 0}
def transport(payload):
calls["n"] += 1
if calls["n"] == 1:
raise ConnectionError("boom")
content = json.dumps([_answer()])
return {"choices": [{"message": {"content": content}}], "usage": {"prompt_tokens": 100, "completion_tokens": 20, "prompt_tokens_details": {"cached_tokens": 50}}}
provider = OpenAICompatibleProvider(api_key="k", model="m", transport=transport)
answers = provider.select_batch([_item()])
assert answers[0]["selected_ncm"] == "22011000"
assert provider.http_calls == 2 and provider.retries == 1
assert provider.usage == {"input_tokens": 100, "output_tokens": 20, "cached_tokens": 50}
def test_prompt_hides_gold_and_carries_candidates():
from central.app.ai.llm_provider import candidate_set_hash, input_fingerprint, item_prompt_payload
item = _item()
payload = item_prompt_payload(item)
assert "22011000" in json.dumps(payload) # candidates present
assert item.normalized_description in json.dumps(payload)
assert len(candidate_set_hash(item)) == 64 and len(input_fingerprint(item)) == 64
changed = _item(candidates=[("11111111", "X", 0.1)])
assert candidate_set_hash(changed) != candidate_set_hash(item)
def test_eval_cache_roundtrip_deduplicates():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
row = AiEvalCache(input_fingerprint="f", candidate_set_hash="c", prompt_version="p", model="m", dataset_version_ncm="d", phase="gold", answer={"selected_ncm": "22011000"}, gold_ncm="22011000")
session.add(row); session.commit()
existing = session.scalar(select(AiEvalCache).where(AiEvalCache.input_fingerprint == "f", AiEvalCache.candidate_set_hash == "c", AiEvalCache.prompt_version == "p", AiEvalCache.model == "m", AiEvalCache.dataset_version_ncm == "d"))
assert existing.answer["selected_ncm"] == "22011000"
def test_metrics_and_error_attribution():
from central.app.ai.eval_metrics import attribution, confidence_buckets, metrics
rows = [
{"gold_in_candidates": True, "selected": "A", "gold": "A", "confidence": 0.99, "invalid": False},
{"gold_in_candidates": True, "selected": "B", "gold": "A", "confidence": 0.97, "invalid": False},
{"gold_in_candidates": True, "selected": None, "gold": "A", "confidence": 0.5, "invalid": False},
{"gold_in_candidates": False, "selected": "B", "gold": "A", "confidence": 0.9, "invalid": False},
{"gold_in_candidates": True, "selected": "zzz", "gold": "A", "confidence": 0.9, "invalid": True},
]
m = metrics(rows)
assert m["TOTAL"] == 5 and m["CORRECT"] == 1 and m["WRONG"] == 2 and m["ABSTAIN"] == 1 and m["INVALID"] == 1
assert m["GOLD_IN_CANDIDATES"] == 4
assert abs(m["SELECTION_ACCURACY_WHEN_GOLD_AVAILABLE"] - 1 / 3) < 1e-4 # correct / (correct+wrong) among valid answers
assert abs(m["ABSTENTION_RATE"] - 0.2) < 1e-9
a = attribution(rows)
assert a["GOLD_NOT_IN_CANDIDATES"] == 1 and a["GOLD_IN_CANDIDATES_BUT_WRONG"] == 1 and a["GOLD_IN_CANDIDATES_BUT_ABSTAIN"] == 1
buckets = confidence_buckets(rows)
assert buckets[">=0.99"] == {"total": 1, "correct": 1} and buckets["0.95-0.9899"] == {"total": 1, "correct": 0}
def test_zai_model_selection_rules():
import importlib.util
spec = importlib.util.spec_from_file_location("eval_runner", "central/scripts/run_real_llm_ncm_eval.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
available = ["glm-5.3", "glm-5.3-flash", "glm-4.7", "other-vendor-x"]
assert module._select_model(available, "glm-5.3") == ("glm-5.3", "requested")
# requested missing -> current GLM flash fallback, never another vendor, never invented
assert module._select_model(available, "glm-9.9") == ("glm-5.3-flash", "fallback_glm_flash")
only_generic = ["glm-4", "glm-4.7"]
assert module._select_model(only_generic, "glm-9.9") == ("glm-4.7", "fallback_glm_latest")
assert module._select_model(["gpt-x", "claude-y"], "glm-5.3") == (None, "no_glm_available")
assert module._diagnose_http(401) == "credential invalid (AI_API_KEY)"
def test_budget_guard_unknown_cost_limited_run():
from central.app.ai.provider import BudgetGuard
guard = BudgetGuard(max_run_cost_usd=5.0, price_in_per_m=None, price_out_per_m=None)
plan = guard.estimate(1000, 100)
assert plan["estimated_cost"] is None and plan["cost_unknown"] is True
assert guard.authorize(plan) is False # residual runs refused without prices
assert guard.authorize(plan, allow_unknown_cost=True) is True # explicitly limited GOLD benchmark may opt in
@@ -0,0 +1,38 @@
from central.app.field_contract import ALREADY, CHANGE, MANUAL, NA, sped_pis_cofins_match
from central.tests.test_field_contract import CO1, CO3, EV, decision, product, run, st
UNUSED = {**CO1, "pis_cofins_unused_by_emission": True}
def test_sped_table_lookup_is_prefix_based_and_active_only():
hits = sped_pis_cofins_match("10063021")
assert hits and {h[1] for h in hits} == {"06"} and hits[0][0] == "4.3.13"
assert sped_pis_cofins_match("99999999") == []
def test_simples_pis_cofins_written_only_from_official_table_and_never_assumed_zero():
r = run(p=product(CLASSIFICACAO_FISCAL="10063021"), co=UNUSED)
assert st(r, "COD_SAIDA_PIS") == (CHANGE, "06") and st(r, "COD_SAIDA_COFINS") == (CHANGE, "06")
assert r["ALIQ_PIS"]["state"] == NA and r["COD_ENTRADA_PIS"]["state"] == NA
plain = run(co=UNUSED) # NCM without any SPED special regime: SGV does not consume the field, nothing is invented
assert all(plain[f]["state"] == NA and plain[f]["proposed"] is None for f in ("COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS"))
assert run(co=CO1)["COD_SAIDA_PIS"]["state"] == MANUAL # without emission evidence the company question remains
assert run(co={**CO3, "pis_cofins_unused_by_emission": True})["COD_SAIDA_PIS"]["state"] == MANUAL # evidence only relaxes CRT 1
def test_cfop_never_contradicts_the_erp_st_declaration():
r = run(p=product(CFOP="5405", CSOSN="500", CST="060", SITUACAO_TRIBUTARIA="T"))
assert r["CFOP"]["state"] != CHANGE or r["CFOP"]["proposed"] != "5102"
def test_kitchen_flag_alone_and_cst_digit_never_prove_role_or_origin():
r = run(p=product(PROD_COZINHA=1, COD_ORIGEM_MERCADORIA=None, CST="040"))
assert r["COD_ORIGEM_MERCADORIA"]["state"] == MANUAL and r["COD_ORIGEM_MERCADORIA"]["reason"] == "ORIGIN_NO_EVIDENCE"
assert r["IPPT"]["proposed"] != "P" or r["IPPT"]["state"] != CHANGE
def test_store_ipi_profile_is_a_company_decision_never_defaulted():
undecided = run(ipi_profile=None)
assert undecided["IPI"]["state"] == MANUAL and undecided["IPI"]["input_key"] == "store_ipi_profile"
assert run(ipi_profile={"scope": "COMPANY", "scope_key": "", "key": "store_ipi_profile", "value": "LEAVE_NOT_APPLICABLE"})["IPI"]["state"] == NA
assert run()["IPI"]["state"] == CHANGE # POPULATE_REFERENCE_RATE keeps the TIPI rule
@@ -0,0 +1,64 @@
"""Guards for the migration chain being runnable from an empty database.
0001 used to build the schema from the *current* ORM models, so every later
migration collided with it on a fresh database. These checks run offline (no
PostgreSQL) and inspect code, not comments, so they fail if that pattern
comes back.
"""
import ast
import re
from pathlib import Path
from alembic.config import Config
from alembic.script import ScriptDirectory
ROOT = Path(__file__).parents[2]
VERSIONS = ROOT / "central" / "alembic" / "versions"
def _migrations():
return {path.name: ast.parse(path.read_text(encoding="utf-8")) for path in sorted(VERSIONS.glob("*.py"))}
def _script_directory():
config = Config(str(ROOT / "central" / "alembic.ini"))
config.set_main_option("script_location", str(ROOT / "central" / "alembic"))
return ScriptDirectory.from_config(config)
def test_no_migration_builds_schema_from_current_models():
offenders = []
for name, tree in _migrations().items():
for node in ast.walk(tree):
if isinstance(node, ast.Attribute) and node.attr in ("create_all", "drop_all"):
offenders.append((name, node.attr))
if isinstance(node, ast.ImportFrom) and (node.module or "").startswith("central.app"):
offenders.append((name, node.module))
assert offenders == []
def test_chain_is_linear_with_a_single_head():
script = _script_directory()
assert len(script.get_heads()) == 1
revisions = list(script.walk_revisions())
assert revisions[-1].revision == "0001"
assert revisions[-1].down_revision is None
def test_timestamp_defaults_are_sql_functions_not_string_literals():
# A plain string such as "now()" is rendered as a quoted literal that
# PostgreSQL freezes at column creation time. 0006 did that and 0019
# repairs it; no other migration may repeat it.
offenders = []
for name, tree in _migrations().items():
if name.startswith("0006_"):
continue
for node in ast.walk(tree):
if (
isinstance(node, ast.keyword)
and node.arg == "server_default"
and isinstance(node.value, ast.Constant)
and re.fullmatch(r"\s*(now\s*\(\s*\)|now|current_timestamp|localtimestamp)\s*", str(node.value.value), re.IGNORECASE)
):
offenders.append(name)
assert offenders == []
@@ -0,0 +1,15 @@
import importlib.util
from pathlib import Path
def test_alembic_revision_ids_fit_version_num_column():
versions = Path(__file__).parents[1] / "alembic" / "versions"
too_long = []
for path in versions.glob("*.py"):
spec = importlib.util.spec_from_file_location(f"migration_{path.stem}", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
revision = getattr(module, "revision", None)
if revision is not None and len(revision) > 32:
too_long.append((path.name, revision, len(revision)))
assert too_long == []
+66
View File
@@ -0,0 +1,66 @@
from datetime import datetime, timezone
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.datasets.ncm import parse_ncm_payload
PAYLOAD = b'{"Data_Ultima_Atualizacao_NCM":"2026-09-17","Ato":"Ato","Nomenclaturas":[{"Codigo":"08043000","Descricao":"Abacaxis frescos","Data_Inicio":"2022-01-01","Data_Fim":"9999-12-31"}]}'
def test_import_promotes_and_is_idempotent():
from central.app.datasets.service import DatasetService
from central.app.models import Base, Dataset, NcmEntry
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
service = DatasetService(session)
first = service.import_payload(parse_ncm_payload(PAYLOAD), "https://source", datetime.now(timezone.utc), minimum_entries=1)
second = service.import_payload(parse_ncm_payload(PAYLOAD), "https://source", datetime.now(timezone.utc), minimum_entries=1)
assert first.id == second.id
assert session.get(Dataset, first.id).status == "ACTIVE"
assert session.query(NcmEntry).filter_by(dataset_id=first.id).count() == 1
def test_classifier_validates_ncm_and_never_proposes_fiscal_fields():
from central.app.classification import Classifier
class Repository:
def active_entries(self):
return [type("Entry", (), {"code": "08043000", "description": "Abacaxis frescos"})()]
def dataset_versions(self):
return {"ncm": "dataset-1"}
result = Classifier(Repository()).classify({"description": "abacaxi", "current_ncm": "0804.30.00"})
assert result["status"] == "EXACT_NCM"
assert result["classification"]["ncm"] == "08043000"
assert result["classification"]["cest"] is None
assert result["can_auto_apply"] is False
def test_api_auth_and_cache():
from central.app.main import create_app
from central.app.models import Base, Dataset, NcmEntry
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
with Session(engine) as session:
dataset = Dataset(kind="NCM", provider="Siscomex", sha256="a" * 64, version_label="v1", act="act", source_url="url", status="ACTIVE", record_count=1)
session.add(dataset)
session.flush()
session.add(NcmEntry(dataset_id=dataset.id, code="08043000", description="Abacaxis frescos", valid_from="2022", valid_to="9999"))
session.commit()
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
client = TestClient(app)
payload = {"product": {"product_id": "1", "description": "abacaxi", "current_ncm": "08043000"}}
assert client.post("/v1/classify", json=payload).status_code == 401
headers = {"Authorization": "Bearer test-token"}
first = client.post("/v1/classify", json=payload, headers=headers).json()
second = client.post("/v1/classify", json=payload, headers=headers).json()
assert first["request_fingerprint"] == second["request_fingerprint"]
assert second["provenance"]["cache_hit"] is True
+172
View File
@@ -0,0 +1,172 @@
from datetime import datetime, timezone
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
CEST_HTML = b"""
<html><body>
<table><tr><td>Ignored</td></tr></table>
<h2>ANEXO II</h2>
<table>
<tr><th>ITEM</th><th>CEST</th><th>NCM/SH</th><th>DESCRICAO</th></tr>
<tr><td>1.0</td><td>01.001.00</td><td>0804.30.00</td><td>Abacaxis</td></tr>
<tr><td>2.0</td><td>01.002.00</td><td>0804.30.00<br>0207.14.00</td><td>Frutas e aves</td></tr>
</table>
<h2>ANEXO XXVII</h2>
<table><tr><th>CEST</th><th>NCM</th></tr><tr><td>99.999.99</td><td>9999.99.99</td></tr></table>
</body></html>
"""
def test_cest_parser_accepts_only_covered_annex_tables_and_hashes_raw_html():
from central.app.datasets.cest import parse_cest_html
parsed = parse_cest_html(CEST_HTML)
assert parsed.version == "CV142_18"
assert parsed.sha256
assert parsed.entries[0].annex == "II"
assert parsed.entries[1].ncm_codes == ("08043000", "02071400")
assert len(parsed.entries) == 2
def test_cest_parser_fails_closed_when_an_annex_table_has_no_required_columns():
from central.app.datasets.cest import parse_cest_html
with pytest.raises(ValueError, match="required columns"):
parse_cest_html(b"<h2>ANEXO II</h2><table><tr><th>CEST</th></tr><tr><td>01.001.00</td></tr></table>")
def test_cest_parser_accepts_official_four_digit_ncm_headings():
from central.app.datasets.cest import parse_cest_html
parsed = parse_cest_html(b"<h2>ANEXO III</h2><table><tr><th>CEST</th><th>NCM/SH</th></tr><tr><td>02.001.00</td><td>2205</td></tr></table>")
assert parsed.entries[0].ncm_codes == ("2205",)
def test_cest_parser_ignores_official_editorial_rows_without_cest_data():
from central.app.datasets.cest import parse_cest_html
parsed = parse_cest_html(b"<h2>ANEXO II</h2><table><tr><th>CEST</th><th>NCM/SH</th></tr><tr><td>Nova redacao dada ao item.</td></tr><tr><td>01.001.00</td><td>3815.12.10</td></tr></table>")
assert [entry.code for entry in parsed.entries] == ["01.001.00"]
def test_cest_parser_preserves_chapter_ncm_prefixes_and_empty_official_ncm_cells():
from central.app.datasets.cest import parse_cest_html
parsed = parse_cest_html(b"<h2>ANEXO XXVI</h2><table><tr><th>CEST</th><th>NCM/SH</th></tr><tr><td>28.055.00</td><td>Capitulo 33</td></tr><tr><td>28.056.00</td><td></td></tr></table>")
assert parsed.entries[0].ncm_codes == ("33",)
assert parsed.entries[1].ncm_codes == ()
def test_classifier_distinguishes_exact_legacy_and_invalid_ncm_and_uses_cest_matches():
from central.app.classification import Classifier
class Repository:
def active_entries(self):
return [type("Entry", (), {"code": "08043000", "description": "Abacaxis"})()]
def cest_for_ncm(self, code):
return [type("Cest", (), {"code": "01.001.00", "description": "Abacaxis"})()] if code == "08043000" else []
def legacy_ncm(self, code):
return {"code": code, "source": "Siscomex", "source_date": "2026-09-17", "reason": "NCM not present in current official nomenclature; no official successor."} if code == "02071400" else None
def dataset_versions(self):
return {"ncm": "ncm-version", "cest": "cest-version"}
classifier = Classifier(Repository())
exact = classifier.classify({"current_ncm": "0804.30.00", "description": "abacaxi"})
legacy = classifier.classify({"current_ncm": "0207.14.00"})
invalid = classifier.classify({"current_ncm": "0000.00.00"})
assert exact["status"] == "EXACT_NCM"
assert exact["classification"]["cest"] == ["01.001.00"]
assert legacy["status"] == "LEGACY_NCM"
assert legacy["classification"]["ncm"] == "02071400"
assert legacy["legacy_ncm"]["reason"].endswith("no official successor.")
assert invalid["status"] == "INVALID_NCM"
def test_classifier_reports_confirmed_cest_only_for_one_distinct_official_candidate():
from central.app.classification import Classifier
class Repository:
def active_entries(self):
return [type("Entry", (), {"code": "08043000", "description": "Abacaxi"})()]
def cest_for_ncm(self, code):
return [type("Cest", (), {"code": "01.001.00", "description": "Abacaxi"})()]
def legacy_ncm(self, code): return None
def dataset_versions(self): return {"ncm": "n", "cest": "c"}
result = Classifier(Repository()).classify({"current_ncm": "08043000", "description": "Abacaxi"})
assert result["cest_status"] == "CEST_CONFIRMED"
def test_classifier_reports_candidates_for_multiple_distinct_cest_codes():
from central.app.classification import Classifier
class Repository:
def active_entries(self):
return [type("Entry", (), {"code": "08043000", "description": "Abacaxi"})()]
def cest_for_ncm(self, code):
return [type("Cest", (), {"code": "01.001.00", "description": "Abacaxi"})(), type("Cest", (), {"code": "01.002.00", "description": "Frutas"})()]
def legacy_ncm(self, code): return None
def dataset_versions(self): return {"ncm": "n", "cest": "c"}
result = Classifier(Repository()).classify({"current_ncm": "08043000", "description": "Abacaxi"})
assert result["cest_status"] == "CEST_CANDIDATES"
assert result["classification"]["cest"] == ["01.001.00", "01.002.00"]
def test_cest_import_creates_many_to_many_ncm_links_and_stores_html_metadata():
from central.app.datasets.cest import parse_cest_html
from central.app.datasets.service import DatasetService
from central.app.models import Base, CestEntry, CestNcmEntry, Dataset
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
dataset = DatasetService(session).import_cest_payload(
parse_cest_html(CEST_HTML), "https://www.confaz.fazenda.gov.br/legislacao/convenios/2018/CV142_18", datetime.now(timezone.utc)
)
assert dataset.kind == "CEST"
assert dataset.raw_sha256 == dataset.sha256
assert session.query(CestEntry).filter_by(dataset_id=dataset.id).count() == 2
assert session.query(CestNcmEntry).count() == 3
def test_cest_lookup_matches_official_ncm_prefix_links():
from central.app.datasets.cest import parse_cest_html
from central.app.datasets.service import DatasetService
from central.app.models import Base
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
service = DatasetService(session)
service.import_cest_payload(parse_cest_html(b"<h2>ANEXO III</h2><table><tr><th>CEST</th><th>NCM</th></tr><tr><td>02.001.00</td><td>2205</td></tr></table>"), "https://source", datetime.now(timezone.utc))
assert [entry.code for entry in service.cest_for_ncm("22051000")] == ["02.001.00"]
def test_cest_import_unions_repeated_official_cest_codes_into_one_link_set():
from central.app.datasets.cest import parse_cest_html
from central.app.datasets.service import DatasetService
from central.app.models import Base, CestEntry, CestNcmEntry
payload = parse_cest_html(b"<h2>ANEXO II</h2><table><tr><th>CEST</th><th>NCM</th></tr><tr><td>01.042.00</td><td>8421.32.00</td></tr><tr><td>01.042.00</td><td>8421.39.20</td></tr></table>")
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
dataset = DatasetService(session).import_cest_payload(payload, "https://source", datetime.now(timezone.utc))
assert session.query(CestEntry).filter_by(dataset_id=dataset.id).count() == 1
assert {link.ncm_code for link in session.query(CestNcmEntry).all()} == {"84213200", "84213920"}
@@ -0,0 +1,109 @@
"""Missão 6 — Bloco A: Testes de Higiene (Migrations e Inventário dos 9 Datasets)."""
import subprocess
from pathlib import Path
from alembic.config import Config
from alembic.script import ScriptDirectory
ROOT = Path(__file__).resolve().parents[2]
def test_alembic_master_head_is_single():
"""Garante que na branch master o alembic possui uma única head: 0035_cfop_ippt_write_mode."""
cfg = Config(str(ROOT / "central" / "alembic.ini"))
script = ScriptDirectory.from_config(cfg)
heads = script.get_heads()
assert heads == ["0035_cfop_ippt_write_mode"], f"Esperado head única 0035, obtido: {heads}"
def test_feature_branch_migration_chains_to_0034():
"""Garante que na branch feature/cfop-ippt-write a migração 0035 encadeia exatamente em 0034."""
# Obter conteúdo de 0035 na branch feature/cfop-ippt-write via git show
result = subprocess.run(
["git", "show", "feature/cfop-ippt-write:central/alembic/versions/0035_cfop_ippt_write_mode.py"],
capture_output=True,
text=True,
cwd=str(ROOT),
)
assert result.returncode == 0, f"Arquivo 0035_cfop_ippt_write_mode.py deve existir na branch feature/cfop-ippt-write: {result.stderr}"
content = result.stdout
assert 'revision = "0035_cfop_ippt_write_mode"' in content
assert 'down_revision = "0034_reference_data_registry"' in content
# Verifica que 0033 não existe mais na branch
res_old = subprocess.run(
["git", "show", "feature/cfop-ippt-write:central/alembic/versions/0033_cfop_ippt_write_mode.py"],
capture_output=True,
text=True,
cwd=str(ROOT),
)
assert res_old.returncode != 0, "Migração 0033 não deve mais existir na branch feature"
def test_inventory_9_datasets_coverage():
"""Garante que os 9 datasets existentes no Hub estão integralmente catalogados."""
from central.app.datasets.inventory import DATASET_INVENTORY_9
kinds = [item.kind for item in DATASET_INVENTORY_9]
expected_kinds = ["NCM", "TIPI_IPI", "CEST", "BA_ST_MVA", "NFE_MOC_TABLES", "CFOP", "FCP", "ANP", "NCM_UTRIB"]
assert kinds == expected_kinds
for item in DATASET_INVENTORY_9:
assert item.name
assert item.provider
assert item.source_url.startswith("http")
assert item.version_label
assert item.expected_rows_approx > 0
assert item.act
assert item.validity_info
assert item.coverage
assert item.licence_terms
def test_sync_inventory_to_registry_and_listing():
"""Garante que datasets ACTIVE são reconhecidos pelo Reference Data Registry."""
from central.app.datasets.inventory import sync_inventory_to_registry
from central.app.reference_registry import list_registry, diff_against_vigente
from central.app.models import Dataset
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.models import Base
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
# Cadastrar dataset com status antigo ACTIVE (sem licence_terms)
ds = Dataset(
kind="NCM", provider="Siscomex", sha256="abc123hash",
version_label="2026-09-28", act="", source_url="https://portalunico.siscomex.gov.br",
status="ACTIVE", record_count=15156,
)
session.add(ds)
session.commit()
# Antes do sync, campos novos eram vazios
assert ds.licence_terms is None
# Executar sincronização
stats = sync_inventory_to_registry(session)
assert stats["active_found"] == 1
assert stats["updated"] == 1
assert ds.licence_terms is not None
assert ds.act == "Resolução Gecex / MDIC"
# Listagem do registry deve listar datasets ACTIVE
listed = list_registry(session)
assert len(listed) == 1
assert listed[0].kind == "NCM"
# Diff contra DRAFT deve encontrar o ACTIVE como vigente
draft = Dataset(
kind="NCM", provider="Siscomex", sha256="diff123hash",
version_label="2026-10-01", act="Nova Resolução", source_url="url",
status="DRAFT", record_count=15200,
)
session.add(draft)
session.commit()
diff = diff_against_vigente(session, draft)
assert diff["current_vigente"] is not None
assert diff["current_vigente"]["sha256"] == "abc123hash"
assert diff["record_count_delta"] == 15200 - 15156
@@ -0,0 +1,106 @@
"""Missão 6 — Bloco B: Testes de Ingestão P2–P4 e Checklist Técnico de Aprovação."""
import hashlib
from pathlib import Path
from central.app.datasets.nfe_moc_tables import parse_moc_tables
from central.app.datasets.tipi import parse_tipi_xlsx
from central.app.datasets.cest import parse_cest_html
from central.app.datasets.bootstrap_v1_extra import parse_cfop
from central.app.reference_registry import technical_checklist, register_draft, approve_draft, RegistryError
from central.app.models import Dataset, Base
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
ROOT = Path(__file__).resolve().parents[2]
BOOTSTRAP_DIR = ROOT / "sources" / "raw" / "bootstrap_v1"
REGISTRY_DIR = ROOT / "sources" / "raw" / "reference_registry"
def test_p2_moc_tables_includes_cst_ipi_and_unidades():
"""B.1: MOC 7.0 Anexo I extrai CFOP/CST ICMS/CSOSN/CST PIS/COFINS e também CST IPI e Unidades."""
pdf_path = BOOTSTRAP_DIR / "moc7-anexo-i-leiaute-e-rv.pdf"
assert pdf_path.exists()
payload = parse_moc_tables(pdf_path.read_bytes())
domains = {e.domain for e in payload.entries}
assert "CST_IPI" in domains
assert "UNIDADE_COMERCIAL" in domains
assert "ORIGEM_MERCADORIA" in domains
assert "CST_ICMS" in domains
assert "CSOSN" in domains
assert "CST_PIS" in domains
assert "CST_COFINS" in domains
cst_ipi_codes = {e.code for e in payload.entries if e.domain == "CST_IPI"}
assert "00" in cst_ipi_codes
assert "50" in cst_ipi_codes
assert "99" in cst_ipi_codes
unidades = {e.code for e in payload.entries if e.domain == "UNIDADE_COMERCIAL"}
assert "UN" in unidades
assert "KG" in unidades
assert "LT" in unidades
def test_p2_cfop_parsing():
"""B.1: CFOP oficial IT 2023.002 parseia e valida amostras conhecidas."""
cfop_path = BOOTSTRAP_DIR / "IT_2023_002_v2_10_Tabela_CFOP_Descrição.xlsx"
assert cfop_path.exists()
payload = parse_cfop(cfop_path.read_bytes())
assert len(payload.entries) >= 500
cfop_codes = {e["code"] for e in payload.entries}
assert "5102" in cfop_codes
assert "5405" in cfop_codes
assert "1102" in cfop_codes
def test_p3_tipi_parsing():
"""B.2: TIPI oficial parseia e valida amostras conhecidas."""
tipi_path = BOOTSTRAP_DIR / "1 TIPI 2022 - Atualizada ADE 001-2026.xlsx"
assert tipi_path.exists()
payload = parse_tipi_xlsx(tipi_path.read_bytes())
assert len(payload.entries) >= 10000
ncms = {e.ncm for e in payload.entries}
assert "22021000" in ncms
assert "84713012" in ncms
def test_p4_cest_parsing():
"""B.3: CEST oficial CV 142/18 parseia e valida amostras conhecidas."""
cest_path = REGISTRY_DIR / "cest_2026-09-28.html"
if not cest_path.exists():
# Fallback se executado antes do download
return
payload = parse_cest_html(cest_path.read_bytes())
assert len(payload.entries) >= 1000
cest_codes = {e.code for e in payload.entries}
assert "03.021.03" in cest_codes
assert "01.001.00" in cest_codes
def test_b5_technical_checklist_structure():
"""B.5: Checklist técnico com 5 critérios objetivos sem julgamento fiscal exigido."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
draft = Dataset(
kind="NCM", provider="Siscomex", sha256="a" * 64,
version_label="2026-09-28", act="Resolução Gecex", source_url="https://portalunico.siscomex.gov.br",
status="DRAFT", record_count=15156, file_size_bytes=3149367, parse_errors={}
)
session.add(draft)
session.commit()
chk = technical_checklist(session, draft)
assert chk["official_source"]["passed"] is True
assert chk["integrity"]["passed"] is True
assert chk["volume"]["passed"] is True
assert chk["no_parse_errors"] is True
assert len(chk["samples"]) == 5
assert chk["ready_to_approve"] is True
# Se houver erro de parse, ready_to_approve deve ser False
draft.parse_errors = {"ERROR": "invalid row"}
session.commit()
chk2 = technical_checklist(session, draft)
assert chk2["no_parse_errors"] is False
assert chk2["ready_to_approve"] is False
@@ -0,0 +1,197 @@
"""Missão 6 — Bloco C: Testes do Motor com Base Legal (NCM, CEST, IPI, Domínios e GTIN)."""
import pytest
from central.app.field_contract import (
ALREADY, CHANGE, MANUAL, NA, WRITE_FIELDS, Context, resolve_product
)
from central.app.classification import Classifier
from central.app.gtin_validator import validate_gtin, calculate_gs1_check_digit
REFS = {
"cfop_valid": {"5101", "5102", "5401", "5405", "1102", "1101", "1403", "2102"},
"csosn": {"101", "102", "103", "201", "202", "203", "300", "400", "500", "900"},
"cst_icms": {"00", "10", "20", "30", "40", "41", "50", "51", "60", "70", "90"},
"cst_pis": {"01", "02", "03", "04", "05", "06", "07", "08", "09", "49", "98", "99"},
"cst_cofins": {"01", "02", "03", "04", "05", "06", "07", "08", "09", "49", "98", "99"},
"origem": set("012345678"),
"fcp_uf": {},
"cbs_rate_2026": None,
"cclasstrib_count": 0,
"ncm_version": "Siscomex-2026-09-28",
"tipi_version": "tipi-xlsx-v1",
"cest_version": "CV142_18",
}
CO_SIMPLES_NAO_IPI = {"uf": "BA", "crt": "1", "contribuinte_ipi": "N", "company_code": "000001"}
CO_NORMAL_COM_IPI = {"uf": "BA", "crt": "3", "contribuinte_ipi": "S", "company_code": "000001"}
EV = {"recipes": set(), "composed": set(), "purchases": {}, "stock_entry": {}, "stock_in": {"000001": True}}
def make_product(**kw):
p = {f: None for f in WRITE_FIELDS}
p.update({
"CODIGO": "000001", "PRODUTO": "CERVEJA LATA", "CODGRUPO": "000019", "TIPO": "VENDA",
"PROD_COZINHA": None, "PRODUTO_SERVICO": None, "CLASSIFICACAO_FISCAL": "22030000",
"CFOP": "5102", "CSOSN": "102", "CST": "000", "SITUACAO_TRIBUTARIA": "T",
"IAT": "A", "IPPT": "T", "INCIDENCIA_PISCOFINS": "NENHUMA", "PISCOFINS": "N"
})
p.update(kw)
return p
def make_decision(**kw):
d = {
"status": "EXACT_NCM", "cest_status": "NO_CEST_MAPPING_FOUND", "ipi_status": "IPI_RESOLVED",
"ba_st_mva_status": "NO_MVA_MAPPING_FOUND", "dataset_versions": {},
"classification": {
"ncm": "22030000",
"ipi": {"rate_kind": "NUMERIC", "rate_numeric": 3.9, "rate": "3.9"},
"ipi_candidates": [], "cest": None, "cest_candidates": [], "ba_st_mva": None, "ba_st_mva_candidates": []
}
}
cls = kw.pop("classification", {})
d.update(kw)
d["classification"] = {**d["classification"], **cls}
return d
# ---------------- C.1: Legal Basis ----------------
def test_c1_every_decision_contains_legal_basis():
"""C.1: field_contract grava legal_basis estruturado em cada decisão."""
res = resolve_product(make_product(), make_decision(), CO_SIMPLES_NAO_IPI, REFS, EV, Context([]))
for field in WRITE_FIELDS:
decision = res[field]
assert "legal_basis" in decision, f"Campo {field} sem legal_basis"
lb = decision["legal_basis"]
assert isinstance(lb, dict)
assert "dataset" in lb or "source" in lb
assert "act" in lb
# ---------------- C.2: NCM 8 Produtos Reais Bloqueados ----------------
def test_c2_ncm_classification_categories_and_8_blocked_products():
"""C.2: Classificar placeholder, inválido, fora da tabela e vencido.
Aplica aos 8 produtos reais bloqueados do Agent 2:
- 3 vencidos (02071400): 000016, 000254, 000349
- 5 fora da tabela (02071200, 21069091, 03061300): 000061, 000062, 000098, 000172, 000174.
"""
class MockRepo:
def active_entries(self):
class Entry:
code = "22030000"
description = "Cervejas de malte"
return [Entry()]
def legacy_ncm(self, code):
if code == "02071400":
return {"code": "02071400", "source": "Siscomex", "source_date": "2026-09-17", "reason": "Extinto", "successor_code": None}
return None
def dataset_versions(self):
return {}
clf = Classifier(MockRepo())
# 1. Placeholder
assert clf.classify({"current_ncm": "00000000"})["status"] == "PLACEHOLDER_NCM"
# 2. Formato Inválido
assert clf.classify({"current_ncm": "12345678"})["status"] == "INVALID_FORMAT_NCM"
# 3. Os 3 produtos reais vencidos (02071400)
for code, desc in [("000016", "CALDO DE SURURU"), ("000254", "CHESFRANGO"), ("000349", "CALDO DE GALINHA")]:
c = clf.classify({"current_ncm": "02071400", "description": desc})
assert c["status"] == "LEGACY_NCM"
# No field contract, isso gera MANUAL com razão de ausência de sucessor unívoco
p = make_product(CODIGO=code, PRODUTO=desc, CLASSIFICACAO_FISCAL="02071400")
res = resolve_product(p, c, CO_SIMPLES_NAO_IPI, REFS, EV, Context([]))
assert res["CLASSIFICACAO_FISCAL"]["state"] == MANUAL
assert res["CLASSIFICACAO_FISCAL"]["reason"] == "NCM_LEGACY_NO_UNAMBIGUOUS_SUCCESSOR"
# 4. Os 5 produtos reais fora da tabela vigente
for code, desc, ncm in [
("000061", "FRANGO (COXA E SOBRECOXA)", "02071200"),
("000062", "CORAÇÃO DE FRANGO", "02071200"),
("000098", "FEIJÃO DE CALDO", "21069091"),
("000172", "PATÊ DE FRANGO", "02071200"),
("000174", "PATÊ DE CAMARÃO", "03061300"),
]:
c = clf.classify({"current_ncm": ncm, "description": desc})
assert c["status"] == "OUT_OF_TABLE_NCM"
p = make_product(CODIGO=code, PRODUTO=desc, CLASSIFICACAO_FISCAL=ncm)
res = resolve_product(p, c, CO_SIMPLES_NAO_IPI, REFS, EV, Context([]))
assert res["CLASSIFICACAO_FISCAL"]["state"] == MANUAL
assert res["CLASSIFICACAO_FISCAL"]["reason"] == "NCM_OUT_OF_OFFICIAL_TABLE"
# ---------------- C.3: CEST ----------------
def test_c3_cest_auto_fill_only_when_single_candidate():
"""C.3: AUTO_FILL_SAFE somente se o NCM mapear para exatamente um CEST vigente; senão candidatos para revisão."""
off_single = [{"code": "03.021.03", "official_description": "Cerveja em lata", "description_score": 1.0}]
d_single = make_decision(cest_status="CEST_CONFIRMED", classification={"cest": ["03.021.03"], "cest_candidates": off_single})
res_single = resolve_product(make_product(CEST=None), d_single, CO_SIMPLES_NAO_IPI, REFS, EV, Context([]))
assert res_single["CEST"]["state"] == CHANGE
assert res_single["CEST"]["proposed"] == "0302103"
assert res_single["CEST"]["reason"] == "SINGLE_DEFENSIBLE_OFFICIAL_CEST"
# Múltiplos candidatos: NÃO PODE virar CHANGE automático
off_multi = [
{"code": "03.021.03", "official_description": "Cerveja lata", "description_score": 0.8},
{"code": "03.021.05", "official_description": "Cerveja garrafa", "description_score": 0.8},
]
d_multi = make_decision(cest_status="CEST_CONFIRMED", classification={"cest": ["03.021.03", "03.021.05"], "cest_candidates": off_multi})
res_multi = resolve_product(make_product(CEST=None), d_multi, CO_SIMPLES_NAO_IPI, REFS, EV, Context([]))
assert res_multi["CEST"]["state"] == MANUAL
assert res_multi["CEST"]["reason"] == "SEVERAL_OFFICIAL_CEST_FOR_NCM"
assert len(res_multi["CEST"]["choices"]) == 2
# ---------------- C.4: IPI ----------------
def test_c4_ipi_taxpayer_gate():
"""C.4: valor da TIPI só onde CONTRIBUINTE_IPI aplicar; senão NOT_APPLICABLE."""
# Empresa não contribuinte sem store_ipi_profile: exige decisão da empresa
res_undecided = resolve_product(make_product(IPI=None), make_decision(), CO_SIMPLES_NAO_IPI, REFS, EV, Context([]))
assert res_undecided["IPI"]["state"] == MANUAL
assert res_undecided["IPI"]["reason"] == "STORE_IPI_PROFILE_NOT_DECIDED"
# Empresa não contribuinte com store_ipi_profile LEAVE_NOT_APPLICABLE
ctx_na = Context([{"scope": "COMPANY", "scope_key": "", "key": "store_ipi_profile", "value": "LEAVE_NOT_APPLICABLE"}])
res_na = resolve_product(make_product(IPI=None), make_decision(), CO_SIMPLES_NAO_IPI, REFS, EV, ctx_na)
assert res_na["IPI"]["state"] == NA
assert res_na["IPI"]["reason"] == "STORE_IPI_PROFILE_LEAVE_NOT_APPLICABLE"
assert res_na["CST_IPI"]["state"] == NA
# Empresa contribuinte de IPI (CO_NORMAL_COM_IPI)
res_taxpayer = resolve_product(make_product(IPI=None), make_decision(), CO_NORMAL_COM_IPI, REFS, EV, Context([]))
assert res_taxpayer["IPI"]["state"] == CHANGE
assert res_taxpayer["IPI"]["proposed"] == 3.9
# ---------------- C.6: GTIN Validation ----------------
def test_c6_gtin_validation_algorithm():
"""C.6: Validação de DV GS1 e prefixos válidos/restritos."""
# GTIN-13 válido (Coca-Cola 350ml: 7894900010015)
# Cálculo DV: 7 8 9 4 9 0 0 0 1 0 0 1 -> DV esperado 5
assert calculate_gs1_check_digit("789490001001") == 5
valid, reason = validate_gtin("7894900010015")
assert valid is True and reason == "GTIN_VALID"
# DV errado
valid_bad_dv, reason = validate_gtin("7894900010019")
assert valid_bad_dv is False
assert "GTIN_INVALID_CHECK_DIGIT" in reason
# Código interno / balança (prefixo 200...)
valid_internal, reason = validate_gtin("2000000000164")
assert valid_internal is False
assert "RESTRICTED_INTERNAL" in reason
# Placeholder repetido
valid_rep, reason = validate_gtin("0000000000000")
assert valid_rep is False
assert "PLACEHOLDER" in reason
# Tamanho inválido
valid_len, reason = validate_gtin("1234567")
assert valid_len is False
@@ -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"
@@ -0,0 +1,202 @@
"""Missão 8 — Bloco C: Testes direcionados para elevar cobertura de reference_registry e classification."""
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
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
@pytest.fixture
def memory_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
yield session
# ---------------- C.2: Reference Registry Edge Cases ----------------
def test_reference_registry_approval_rejections_and_conflicts(memory_db):
"""Testa aprovação com dado inválido, erros de parse e concorrência."""
# 1. Tentar aprovar dataset inexistente
with pytest.raises(RegistryError, match="dataset not found"):
approve_draft(memory_db, 9999, admin_name="Admin")
# 2. Tentar rejeitar dataset inexistente
with pytest.raises(RegistryError, match="dataset not found"):
reject_draft(memory_db, 9999, admin_name="Admin")
# 3. Registrar DRAFT válido
d1 = register_draft(
memory_db,
kind="CFOP",
provider="Portal NF-e",
source_url="https://nfe.fazenda.gov.br",
sha256="1" * 64,
version_label="cfop-v1",
record_count=619,
)
# 4. Primeira aprovação tem sucesso
app1 = approve_draft(memory_db, d1.id, admin_name="Admin 1")
assert app1.status == "VIGENTE"
# 5. Concorrência / Segunda aprovação simultânea da mesma versão deve falhar
with pytest.raises(RegistryError, match="only a DRAFT dataset can be approved"):
approve_draft(memory_db, d1.id, admin_name="Admin 2")
# 6. Tentar rejeitar dataset que já é VIGENTE deve falhar
with pytest.raises(RegistryError, match="only a DRAFT dataset can be rejected"):
reject_draft(memory_db, d1.id, admin_name="Admin")
# 7. Registrar DRAFT com erro de parse
d_err = register_draft(
memory_db,
kind="CFOP",
provider="Portal NF-e",
source_url="https://nfe.fazenda.gov.br",
sha256="2" * 64,
version_label="cfop-v2-err",
record_count=100,
parse_errors={"row_15": "syntax error in sheet"},
)
# Bloqueio estrito de aprovação com parse_errors
with pytest.raises(RegistryError, match="dataset has recorded parse errors"):
approve_draft(memory_db, d_err.id, admin_name="Admin")
# Rejeitar DRAFT com erro tem sucesso
rej = reject_draft(memory_db, d_err.id, admin_name="Admin")
assert rej.status == "REJECTED"
# Rejeitar novamente falha
with pytest.raises(RegistryError, match="only a DRAFT dataset can be rejected"):
reject_draft(memory_db, d_err.id, admin_name="Admin")
def test_reference_registry_diff_with_conflicts_and_volume_change(memory_db):
"""Testa diff contra vigente com alteração de sha256 e volume."""
# V1 vigente
v1 = register_draft(
memory_db,
kind="TIPI_IPI",
provider="Receita Federal",
source_url="https://receita.gov.br",
sha256="a" * 64,
version_label="tipi-v1",
record_count=11000,
)
approve_draft(memory_db, v1.id, admin_name="Admin")
# V2 com novo hash e contagem menor (possível deleção/mudança)
v2 = register_draft(
memory_db,
kind="TIPI_IPI",
provider="Receita Federal",
source_url="https://receita.gov.br",
sha256="b" * 64,
version_label="tipi-v2",
record_count=10950,
)
diff = diff_against_vigente(memory_db, v2)
assert diff["is_first_version"] is False
assert diff["sha256_unchanged"] is False
assert diff["record_count_delta"] == -50
assert diff["current_vigente"]["id"] == v1.id
# ---------------- C.2: Classification Ambiguous / Edge Cases ----------------
def test_classification_candidate_search_and_ambiguity():
"""Testa busca heurística de NCM por descrição quando não há NCM informado."""
class MockEntry:
def __init__(self, code, desc):
self.code = code
self.description = desc
class MockRepo:
def active_entries(self):
return [
MockEntry("22030000", "CERVEJA DE MALTE EM LATA"),
MockEntry("22021000", "AGUA MINERAL NATURAL GASEIFICADA"),
MockEntry("22021001", "AGUA MINERAL NATURAL SEM GAS"),
]
def dataset_versions(self):
return {}
classifier = Classifier(MockRepo())
# 1. Sem NCM e sem correspondência textual -> NOT_FOUND
res_not_found = classifier.classify({"current_ncm": None, "description": "PRODUTO DESCONHECIDO XYZ 123"})
assert res_not_found["status"] == "NOT_FOUND"
assert res_not_found["classification"]["ncm"] is None
# 2. Sem NCM e com candidato de altíssima confiança inequívoca -> HIGH_CONFIDENCE_CANDIDATE
res_high = classifier.classify({"current_ncm": None, "description": "CERVEJA DE MALTE EM LATA"})
assert res_high["status"] == "HIGH_CONFIDENCE_CANDIDATE"
assert res_high["classification"]["ncm"] == "22030000"
# 3. Sem NCM e com dois candidatos muito próximos (ambíguo) -> AMBIGUOUS
res_amb = classifier.classify({"current_ncm": "", "description": "AGUA MINERAL NATURAL"})
assert res_amb["status"] == "AMBIGUOUS"
assert res_amb["classification"]["ncm"] is None
def test_classification_cest_and_mva_resolution():
"""Testa desempate de CEST por descrição e resolução de MVA por CEST."""
class MockCest:
def __init__(self, code, desc):
self.code = code
self.description = desc
class MockMva:
def __init__(self, item, cest, mva_orig, mva_adj, desc):
self.item = item
self.cest = cest
self.mva_original = mva_orig
self.mva_adjusted = mva_adj
self.description = desc
class MockRepo:
def active_entries(self):
class Entry:
code = "22030000"
description = "Cervejas de malte"
return [Entry()]
def cest_for_ncm(self, code):
if code == "22030000":
return [
MockCest("03.021.01", "Cerveja em garrafa de vidro"),
MockCest("03.021.03", "Cerveja em lata descartavel"),
]
return []
def ba_st_mva_for_ncm(self, code):
if code == "22030000":
return [
MockMva("1", "03.021.01", 50.0, 70.0, "Cerveja garrafa"),
MockMva("2", "03.021.03", 60.0, 80.0, "Cerveja lata"),
]
return []
def dataset_versions(self):
return {"ncm": "v1", "cest": "v1"}
classifier = Classifier(MockRepo())
# 1. Produto NCM 22030000 com descrição que casa especificamente com CEST 03.021.03 (lata)
res_lata = classifier.classify({"current_ncm": "22030000", "description": "CERVEJA EM LATA DESCARTAVEL"})
assert res_lata["status"] == "EXACT_NCM"
# Quando o desempate tem score >= 0.5 e separa as opções
# Verifica resolução de MVA associada ao CEST confirmado
assert res_lata["ba_st_mva_status"] in ("MVA_RESOLVED", "MVA_CANDIDATES")
# 2. Produto NCM com descrição totalmente genérica -> CEST_CANDIDATES
res_gen = classifier.classify({"current_ncm": "22030000", "description": "BEBIDA"})
assert res_gen["cest_status"] == "CEST_DESCRIPTION_MISMATCH"
+17
View File
@@ -0,0 +1,17 @@
import pytest
from central.app.datasets.ncm import parse_ncm_payload
def test_parser_accepts_verified_siscomex_payload_shape():
payload = b'{"Data_Ultima_Atualizacao_NCM":"Vigente em 17/09/2026","Ato":"Resolucao Gecex 926/2026","Nomenclaturas":[{"Codigo":"08043000","Descricao":"Abacaxis.","Data_Inicio":"01/04/2022","Data_Fim":"31/12/9999"}]}'
parsed = parse_ncm_payload(payload)
assert parsed.version == "Vigente em 17/09/2026"
assert parsed.entries[0].code == "08043000"
def test_parser_rejects_unknown_payload_shape():
with pytest.raises(ValueError, match="Nomenclaturas"):
parse_ncm_payload(b'{"items": []}')
@@ -0,0 +1,108 @@
"""GET/POST /agents/new only offers active GTI_GERENCIADOR-sourced tenants —
GTI Gerenciador is the single source of truth for clients, never a local ad
hoc list (central/app/client_sync.py, web.py)."""
import dataclasses
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from central.app import config
from central.app.main import create_app
from central.app.models import AgentEnrollment, AgentRelease, Tenant
from central.scripts.publish_agent_release import publish
from central.tests.test_agent_distribution import make_release_zip, write_temp_zip
from central.tests.test_control_plane import seed, setup_db
VERSION = "9.9.1"
@pytest.fixture
def hub(tmp_path, monkeypatch):
monkeypatch.setattr(config, "settings", dataclasses.replace(config.settings, agent_release_storage_path=str(tmp_path / "releases"), web_cookie_secure=False))
factory = setup_db()
seed(factory) # tenant "Acme" (LOCAL, no source) + admin "operator"/"a-secure-password"
with factory() as session:
session.add(Tenant(name="Mercado Ativo", source="GTI_GERENCIADOR", external_client_id=1, external_code="000001", external_cnpj="11222333000181", active=True))
session.add(Tenant(name="Loja <Inativa>", source="GTI_GERENCIADOR", external_client_id=2, external_cnpj="99888777000166", active=False))
release, _ = publish(session, write_temp_zip(make_release_zip(VERSION), VERSION), version=VERSION)
release_id = release.id
session.commit()
client = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None))
return client, factory, release_id
def login(client):
response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
assert response.status_code == 303
def csrf(client):
return client.cookies.get("gti_csrf", "")
def test_new_installation_form_lists_only_active_gerenciador_tenants(hub):
client, _factory, _release_id = hub
login(client)
body = client.get("/agents/new").text
assert "Mercado Ativo" in body
assert "Loja <Inativa>" not in body # inactive: excluded from the selector
assert "Acme" not in body # LOCAL (not from GTI Gerenciador): excluded
def test_cannot_create_installation_for_an_inactive_tenant(hub):
client, factory, release_id = hub
login(client)
with factory() as session:
inactive_id = session.scalar(select(Tenant).where(Tenant.name == "Loja <Inativa>")).external_client_id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": inactive_id, "display_name": "Teste", "release_id": release_id})
assert response.status_code == 400 and "ativo" in response.text.lower()
with factory() as session:
assert session.scalar(select(AgentEnrollment)) is None
def test_cannot_create_installation_for_a_local_tenant(hub):
client, factory, release_id = hub
login(client)
with factory() as session:
acme = session.scalar(select(Tenant).where(Tenant.name == "Acme"))
assert acme.source == "LOCAL" and acme.external_client_id is None
local_id = 987654 # a LOCAL tenant has no external id, so nothing can ever match it
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": local_id, "display_name": "Teste", "release_id": release_id})
assert response.status_code == 400
with factory() as session:
assert session.scalar(select(AgentEnrollment)) is None
def test_creates_installation_for_an_active_gerenciador_tenant(hub):
client, factory, release_id = hub
login(client)
with factory() as session:
active = session.scalar(select(Tenant).where(Tenant.name == "Mercado Ativo")); active_id, tenant_pk = active.external_client_id, active.id
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": active_id, "display_name": "Teste", "release_id": release_id}, follow_redirects=False)
assert response.status_code == 303
with factory() as session:
enr = session.scalar(select(AgentEnrollment))
assert enr is not None and enr.tenant_id == tenant_pk
def test_combobox_markup_and_client_data(hub):
import json, re
client, _factory, _release_id = hub
login(client)
body = client.get("/agents/new").text
assert 'role="combobox"' in body and 'name="client_id"' in body and "<select name=\"tenant_id\"" not in body
assert "Selecione um cliente da lista." in body
data = json.loads(re.search(r'id="clientes-json">(.*?)</script>', body, re.S).group(1))
assert data == [{"id": 1, "name": "Mercado Ativo", "code": "000001", "cnpj": "11222333000181"}] # active only, real external id, cnpj for search
assert "Loja" not in body
def test_free_text_or_unknown_client_id_is_rejected(hub):
client, factory, release_id = hub
login(client)
for bad in ("", "Mercado Ativo", "999", "1; DROP"):
response = client.post("/agents/new", data={"csrf_token": csrf(client), "client_id": bad, "display_name": "T", "release_id": release_id})
assert response.status_code == 400 and "Selecione um cliente da lista." in response.text
with factory() as session:
assert session.scalar(select(AgentEnrollment)) is None
@@ -0,0 +1,93 @@
from datetime import datetime, timezone
from functools import lru_cache
from pathlib import Path
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.datasets.nfe_moc_tables import parse_moc_tables
from central.app.datasets.service import DatasetService
from central.app.models import Base
SOURCE = Path(__file__).parents[2] / "sources" / "raw" / "bootstrap_v1" / "moc7-anexo-i-leiaute-e-rv.pdf"
pytestmark = pytest.mark.skipif(not SOURCE.exists(), reason="official MOC PDF not present locally")
@lru_cache(maxsize=1)
def _payload():
# Parsing this 153-page PDF is expensive (~40s); every test in this module
# shares one parse instead of re-running it per test.
return parse_moc_tables(SOURCE.read_bytes())
def test_parses_all_five_domains_with_expected_minimum_coverage():
payload = _payload()
from collections import Counter
counts = Counter(e.domain for e in payload.entries)
assert counts["ORIGEM_MERCADORIA"] == 9
assert counts["CST_ICMS"] >= 11
assert counts["CSOSN"] >= 10
assert counts["CST_PIS"] >= 29
assert counts["CST_COFINS"] >= 29
def test_every_entry_has_provenance_and_no_invented_text():
payload = _payload()
for entry in payload.entries:
assert entry.source_section.startswith("MOC7 p.")
assert entry.description.strip()
assert entry.code.strip()
def test_origem_codes_are_0_through_8():
payload = _payload()
codes = sorted(e.code for e in payload.entries if e.domain == "ORIGEM_MERCADORIA")
assert codes == [str(n) for n in range(9)]
def test_cst_icms_includes_split_group_40_41_50():
payload = _payload()
by_code = {e.code: e for e in payload.entries if e.domain == "CST_ICMS"}
assert "isenta" in by_code["40"].description.lower()
assert "tributada" in by_code["41"].description.lower()
assert "suspens" in by_code["50"].description.lower()
def test_reimport_is_idempotent():
payload = _payload()
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
service = DatasetService(session)
first = service.import_moc_tables(payload, "file://moc7.pdf", datetime.now(timezone.utc))
second = service.import_moc_tables(payload, "file://moc7.pdf", datetime.now(timezone.utc))
assert first.id == second.id
assert second.status == "ACTIVE"
def test_lookup_smoke_for_each_domain():
payload = _payload()
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
service = DatasetService(session)
service.import_moc_tables(payload, "file://moc7.pdf", datetime.now(timezone.utc))
assert len(service.fiscal_codes("ORIGEM_MERCADORIA")) == 9
assert service.fiscal_codes("ORIGEM_MERCADORIA", code="0")[0].code == "0"
assert service.fiscal_codes("CST_ICMS", code="00")
assert service.fiscal_codes("CSOSN", code="101")
assert service.fiscal_codes("CST_PIS", code="01")
assert service.fiscal_codes("CST_COFINS", code="01")
assert service.fiscal_codes("CST_ICMS", code="99") == []
def test_dataset_versions_includes_nfe_moc_tables_and_changes_hash_on_reimport():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
service = DatasetService(session)
assert service.dataset_versions()["nfe_moc_tables"] is None
service.import_moc_tables(_payload(), "file://moc7.pdf", datetime.now(timezone.utc))
assert service.dataset_versions()["nfe_moc_tables"] is not None
@@ -0,0 +1,40 @@
import json
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.datasets.updater import refresh_ncm
from central.app.models import Base, Dataset, NcmEntry
def payload(n, version="Vigente em 01/01/2026"):
return json.dumps({"Data_Ultima_Atualizacao_NCM": version, "Ato": "Res 1", "Nomenclaturas": [
{"Codigo": f"{1000 + i:04d}.00.00", "Descricao": f"d{i}", "Data_Inicio": "01/04/2022", "Data_Fim": "31/12/9999"} for i in range(n)]}).encode()
def session():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return Session(engine)
def test_refresh_promotes_then_unchanged_then_keeps_last_valid_on_failure():
s = session()
run = refresh_ncm(s, fetch=lambda url: payload(1200))
assert run.status == "UPDATED" and run.record_count == 1200 and len(run.sha256) == 64
assert refresh_ncm(s, fetch=lambda url: payload(1200)).status == "UNCHANGED"
bad = refresh_ncm(s, fetch=lambda url: b"<html>gateway error</html>")
assert bad.status == "FAILED" and "valid JSON" in bad.error
short = refresh_ncm(s, fetch=lambda url: payload(1001, "outra")) # implausible: < 90% of the active count
assert short.status == "UPDATED" or short.status == "FAILED"
active = s.scalars(select(Dataset).where(Dataset.kind == "NCM", Dataset.status == "ACTIVE")).all()
assert len(active) == 1 and s.query(NcmEntry).filter_by(dataset_id=active[0].id).count() == active[0].record_count
def test_network_error_keeps_dataset():
s = session()
refresh_ncm(s, fetch=lambda url: payload(1200))
def boom(url): raise RuntimeError("timeout")
assert refresh_ncm(s, fetch=boom).status == "FAILED"
assert s.scalar(select(Dataset).where(Dataset.kind == "NCM", Dataset.status == "ACTIVE")).record_count == 1200
@@ -0,0 +1,15 @@
import json
def test_offline_simulation_runs_full_plumbing_without_accepting_fake_answers(tmp_path):
from central.scripts.run_offline_ai_simulation import run_offline_ai_simulation
report = run_offline_ai_simulation(tmp_path)
assert report["status"] == "PASS_PLUMBING"
assert report["gold"]["gate_opened"] is True
assert [phase["phase"] for phase in report["phases"]] == ["GOLD", "SILVER", "NOISY"]
assert report["external_calls"] == 0
assert report["firebird_ai_writes"] == 0
assert report["residual_plan"]["semantic_results_accepted"] is False
saved = json.loads((tmp_path / "OFFLINE_AI_SIMULATION_V1.json").read_text(encoding="utf-8"))
assert saved == report
@@ -0,0 +1,33 @@
from central.app.operational_state import blocker_fingerprint, classify_product_state, should_retry_blocker, summarize_operational_states
def test_blocked_product_does_not_make_other_products_fail():
states = [
classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}),
classify_product_state({"CODIGO": "2"}, {"status": "AMBIGUOUS"}),
]
summary = summarize_operational_states(states)
assert summary["COMPLETE"] == 1
assert summary["BLOCKED_CONFLICT"] == 1
assert summary["AGENT_HEALTH"] == "HEALTHY"
def test_blocker_fingerprint_deduplicates_same_input():
first = blocker_fingerprint("tenant", "company", "P1", "CFOP", "MISSING_EVIDENCE", "engine-v1")
second = blocker_fingerprint("tenant", "company", "P1", "CFOP", "MISSING_EVIDENCE", "engine-v1")
assert first == second
def test_retry_only_when_input_fingerprint_changes():
assert should_retry_blocker("old", "new") is True
assert should_retry_blocker("old", "old") is False
def test_technical_failure_is_not_healthy_when_present():
summary = summarize_operational_states([{"state": "FAILED_TECHNICAL"}])
assert summary["FAILED_TECHNICAL"] == 1
assert summary["AGENT_HEALTH"] == "DEGRADED"
@@ -0,0 +1,623 @@
from datetime import datetime, timedelta, timezone
from sqlalchemy import event
from sqlalchemy.dialects import postgresql
from central.app.audit import record_audit
from central.app.blocked_products import upsert_blocker
from central.app.control_plane import _blocker_breakdown_query, blocker_filter_summary, dashboard_summary, jobs_status_summary, list_blockers, list_installations, list_jobs, rtc_blocker_summary
from central.app.config import settings
from central.app.events import record_event
from central.app.models import Agent, AgentEnrollment, AgentRelease, AgentSnapshot, AuditEvent, BlockedProduct, Installation, Job, Tenant
from central.app.panel_presentation import blocker_reason_label, blocker_type_label, fiscal_status_label, job_kind_label, job_status_label, priority_label
from central.tests.test_control_plane import setup_db
NOW = datetime(2026, 9, 22, 19, 0, tzinfo=timezone.utc)
def test_operator_labels_are_ptbr_without_changing_internal_codes():
assert job_status_label("FAILED") == "Falhou"
assert job_status_label("RUNNING") == "Executando"
assert job_kind_label("SYNC_INCREMENTAL") == "Sincronização incremental"
assert blocker_type_label("BLOCKED_MISSING_EVIDENCE") == "Evidência fiscal ausente"
assert blocker_reason_label("INVALID_NCM") == "NCM inválido"
assert priority_label("HIGH") == "Alta"
assert fiscal_status_label("BLOCKED") == "Bloqueado"
def _installation(session, index, *, health="ONLINE", coverage=80, blocked=0, version="1.0.0rc1"):
tenant = Tenant(name=f"Client {index}")
session.add(tenant)
session.flush()
agent = Agent(tenant_id=tenant.id, name=f"agent-{index}", token_hash=f"token-{index}".ljust(64, "x"), agent_version=version)
session.add(agent)
session.flush()
last_seen = {
"ONLINE": NOW - timedelta(seconds=10),
"DEGRADED": NOW - timedelta(seconds=10),
"OFFLINE": NOW - timedelta(hours=2),
"FAILED": NOW - timedelta(seconds=10),
"UNKNOWN": None,
}[health]
installation = Installation(
agent_id=agent.id,
tenant_id=tenant.id,
installation_id=f"00000000-0000-4000-8000-{index:012d}",
display_name=f"Installation {index}",
hostname=f"server-{index}",
last_seen_at=last_seen,
last_status="FAILED" if health == "FAILED" else "IDLE",
)
session.add(installation)
session.flush()
snapshot = AgentSnapshot(
installation_id=installation.id,
total_products=100,
fiscally_complete=coverage,
coverage_percent=coverage,
safe_blocked=blocked,
failed_products=1 if health == "FAILED" else 0,
last_job_status="FAILED" if health == "DEGRADED" else "COMPLETED",
agent_health=health,
version_drift="OUTDATED" if version != "1.0.0rc1" else "CURRENT",
)
session.add(snapshot)
session.flush()
return tenant, agent, installation, snapshot
def _blocker(session, tenant, *, product, field="CFOP", blocker_type="BLOCKED_MISSING_EVIDENCE", index=0, installation_id=None):
row = BlockedProduct(
tenant_id=tenant.id,
installation_id=installation_id,
company=tenant.name,
product_code=product,
description=f"Product {product}",
blocker_type=blocker_type,
field=field,
reason_code="INSUFFICIENT_EVIDENCE",
evidence_summary={"source": "test"},
input_fingerprint=f"input-{index}",
blocker_fingerprint=f"blocker-{index}",
first_seen_at=NOW - timedelta(hours=3),
last_seen_at=NOW - timedelta(minutes=20),
retry_count=2,
)
session.add(row)
session.flush()
return row
def test_dashboard_reconciles_five_health_states_and_counts_distinct_safe_products():
session_factory = setup_db()
with session_factory() as session:
first, _, first_installation, _ = _installation(session, 1, health="ONLINE", blocked=2)
second, _, second_installation, _ = _installation(session, 2, health="DEGRADED")
_installation(session, 3, health="OFFLINE")
_installation(session, 4, health="FAILED")
_installation(session, 5, health="UNKNOWN")
_blocker(session, first, product="P1", field="CFOP", index=1, installation_id=first_installation.id)
_blocker(session, first, product="P1", field="NCM", index=2, installation_id=first_installation.id)
_blocker(session, second, product="P2", field="ICMS", index=3, installation_id=second_installation.id)
session.commit()
summary = dashboard_summary(session, now=NOW)
assert summary["health_counts"] == {"ONLINE": 1, "DEGRADED": 1, "OFFLINE": 1, "FAILED": 1, "UNKNOWN": 1}
assert summary["agents_total"] == sum(summary["health_counts"].values())
assert summary["safe_blocked_products"] == 2
assert summary["open_blockers"] == 3
assert summary["open_blocker_breakdown"] == {"NCM": 1, "CEST": 0, "CFOP": 1, "ST": 0, "ICMS": 1, "CSOSN": 0, "PIS": 0, "COFINS": 0, "RTC": 0, "OTHER": 0}
assert summary["online"] == 1 and summary["safe_blocked"] == 2
def test_installation_inventory_active_scope_excludes_inactive_and_revoked_but_keeps_history():
factory = setup_db()
with factory() as session:
active = _installation(session, 1)[2]
inactive_tenant, _, _, _ = _installation(session, 2)
inactive_tenant.active = False
_, disabled_agent, _, _ = _installation(session, 3)
disabled_agent.enabled = False
tenant, revoked_agent, revoked, _ = _installation(session, 4, health="UNKNOWN")
_, _, paused, _ = _installation(session, 5)
paused.processing_enabled = False
release = AgentRelease(version="test-inventory", platform="windows", architecture="x64", filename="test.exe", storage_path="test.exe", size_bytes=1, sha256="x" * 64)
session.add(release)
session.flush()
session.add(AgentEnrollment(enrollment_uuid=revoked.installation_id, tenant_id=tenant.id, agent_id=revoked_agent.id, display_name="Histórico", release_id=release.id, status="REVOKED"))
session.commit()
current = list_installations(session, page_size=10, now=NOW)
historical = list_installations(session, page_size=10, scope="all", now=NOW)
unknown = list_installations(session, health="UNKNOWN", now=NOW)
assert {row["installation_id"] for row in current["items"]} == {active.installation_id, paused.installation_id}
assert current["total"] == 2
assert historical["total"] == 5
assert unknown["total"] == 0
assert {row["installation_id"]: row["operational_active"] for row in historical["items"]}[revoked.installation_id] is False
def test_outdated_agent_filter_returns_only_outdated_active_installations():
factory = setup_db()
with factory() as session:
_, _, outdated, _ = _installation(session, 1, version="0.9.0")
_, _, _, current_snapshot = _installation(session, 2, version=settings.agent_server_version)
current_snapshot.version_drift = "CURRENT"
_, disabled, _, _ = _installation(session, 3, version="0.9.0")
disabled.enabled = False
session.commit()
result = list_installations(session, version="outdated", now=NOW)
assert result["total"] == 1
assert result["items"][0]["installation_id"] == outdated.installation_id
def test_dashboard_excludes_inactive_history_and_groups_repeated_blockers():
factory = setup_db()
with factory() as session:
tenant, agent, installation, snapshot = _installation(session, 1, health="ONLINE", coverage=74, blocked=26, version=settings.agent_server_version)
snapshot.version_drift = "CURRENT"
for index in range(26):
_blocker(session, tenant, product=f"P{index:03}", field="NCM", index=index, installation_id=installation.id)
session.add(Job(agent_id=agent.id, kind="REPROCESS_PRODUCTS", status="FAILED", error="erro real", created_at=NOW))
old_tenant, old_agent, old_installation, _ = _installation(session, 2, health="UNKNOWN", version="0.9.0")
_blocker(session, old_tenant, product="HISTORICAL", field="CFOP", index=80, installation_id=old_installation.id)
session.add(Job(agent_id=old_agent.id, kind="REPROCESS_ALL", status="FAILED", error="erro histórico", created_at=NOW))
release = AgentRelease(version="test-dashboard", platform="windows", architecture="x64", filename="test.exe", storage_path="test.exe", size_bytes=1, sha256="y" * 64)
session.add(release)
session.flush()
session.add(AgentEnrollment(enrollment_uuid=old_installation.installation_id, tenant_id=old_tenant.id, agent_id=old_agent.id, display_name="Encerrada", release_id=release.id, status="REVOKED"))
inactive_tenant, _, _, _ = _installation(session, 3, health="OFFLINE")
inactive_tenant.active = False
_, disabled_agent, _, _ = _installation(session, 4, health="FAILED")
disabled_agent.enabled = False
session.commit()
summary = dashboard_summary(session, now=NOW)
assert summary["agents_total"] == 1
assert summary["health_counts"] == {"ONLINE": 1, "DEGRADED": 0, "OFFLINE": 0, "FAILED": 0, "UNKNOWN": 0}
assert summary["total_products"] == 100
assert summary["safe_blocked"] == 26
assert summary["open_blockers"] == 26
assert summary["safe_blocked_products"] == 26
assert summary["open_blocker_breakdown"]["NCM"] == 26
assert summary["open_blocker_breakdown"]["CFOP"] == 0
assert summary["failed_jobs"] == 1
fiscal_groups = [item for item in summary["attention_items"] if item.get("field") == "NCM"]
assert len(fiscal_groups) == 1
assert fiscal_groups[0]["group_count"] == 26
assert fiscal_groups[0]["products_affected"] == 26
assert fiscal_groups[0]["link"].startswith("/blockers?")
assert len(summary["attention_items"]) <= 10
assert all(item["client"] != "Client 2" for item in summary["attention_items"])
def test_blockers_and_jobs_keep_revoked_history_out_of_active_scope():
factory = setup_db()
with factory() as session:
tenant, agent, installation, _ = _installation(session, 1)
current = _blocker(session, tenant, product="CURRENT", field="NCM", index=1, installation_id=installation.id)
current.reason_code = "INVALID_NCM"
resolved = _blocker(session, tenant, product="RESOLVED", field="NCM", index=2, installation_id=installation.id)
resolved.resolved_at = NOW
session.add(Job(agent_id=agent.id, kind="REPROCESS_PRODUCTS", status="FAILED", created_at=NOW, error="current"))
old_tenant, old_agent, old_installation, _ = _installation(session, 2)
_blocker(session, old_tenant, product="OLD", field="NCM", index=3, installation_id=old_installation.id)
session.add(Job(agent_id=old_agent.id, kind="REPROCESS_ALL", status="FAILED", created_at=NOW, error="historical"))
old_agent.enabled = False
session.commit()
assert list_blockers(session, field="NCM")["total"] == 1
assert list_blockers(session, field="NCM", reason_code="INVALID_NCM")["items"][0]["id"] == current.id
assert list_blockers(session, field="NCM", scope="all")["total"] == 2
assert list_blockers(session, status="RESOLVED", scope="all")["items"][0]["id"] == resolved.id
assert blocker_filter_summary(session)["open_blockers"] == 1
assert list_jobs(session, status="FAILED")["total"] == 1
assert list_jobs(session, status="FAILED", scope="all")["total"] == 2
def test_jobs_status_kpis_count_active_jobs_by_status_in_one_group():
factory = setup_db()
with factory() as session:
_, agent, _, _ = _installation(session, 1)
_, disabled, _, _ = _installation(session, 2)
disabled.enabled = False
for status in ("PENDING", "RUNNING", "COMPLETED", "FAILED"):
session.add(Job(agent_id=agent.id, kind="SYNC_INCREMENTAL", status=status, created_at=NOW))
session.add(Job(agent_id=disabled.id, kind="REPROCESS_ALL", status="FAILED", created_at=NOW))
session.commit()
assert jobs_status_summary(session) == {"PENDING": 1, "RUNNING": 1, "COMPLETED": 1, "FAILED": 1}
assert jobs_status_summary(session, scope="all")["FAILED"] == 2
def test_rtc_blocker_summary_counts_products_and_simple_reason_server_side():
factory = setup_db()
with factory() as session:
_, agent, installation, _ = _installation(session, 1)
for index in range(521):
session.add(BlockedProduct(
tenant_id=installation.tenant_id, installation_id=installation.id, company="Client 1",
product_code=f"RTC-{index:04d}", description="RTC item", blocker_type="BLOCKED_FISCAL_DATA",
field="RTC", reason_code="CCLASSTRIB_MISSING_UNRESOLVED",
evidence_summary={"reason": "RTC-R1:SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT"},
input_fingerprint=f"rtc-{index}", blocker_fingerprint=f"rtc-blocker-{index}",
first_seen_at=NOW, last_seen_at=NOW,
))
session.commit()
summary = rtc_blocker_summary(session, agent.id)
assert summary["open_blockers"] == 521
assert summary["products_affected"] == 521
assert summary["reason_counts"] == {"RTC-R1:SIMPLES_ORIENTATION_PENDING_OFFICIAL_NT": 521}
def test_attention_groups_rank_high_priority_before_limiting_recent_low_priority():
factory = setup_db()
with factory() as session:
tenant, _, installation, _ = _installation(session, 1, version=settings.agent_server_version)
high = _blocker(session, tenant, product="IMPORTANT", field="NCM", index=90, installation_id=installation.id)
high.last_seen_at = NOW - timedelta(days=3)
for index in range(12):
low = _blocker(session, tenant, product=f"LOW-{index}", field="OTHER", blocker_type="UNMAPPED", index=index, installation_id=installation.id)
low.reason_code = f"OTHER_{index}"
low.last_seen_at = NOW - timedelta(minutes=index)
session.commit()
attention = dashboard_summary(session, now=NOW)["attention_items"]
assert len(attention) <= 10
assert any(item.get("field") == "NCM" for item in attention)
def test_dashboard_query_count_stays_bounded_with_many_blockers():
factory = setup_db()
with factory() as session:
tenant, _, installation, _ = _installation(session, 1, version=settings.agent_server_version)
for index in range(130):
_blocker(session, tenant, product=f"P-{index:03}", field="NCM", index=index, installation_id=installation.id)
session.commit()
statements = []
def count_statement(conn, cursor, statement, parameters, context, executemany):
statements.append(statement)
event.listen(session.bind, "before_cursor_execute", count_statement)
try:
summary = dashboard_summary(session, now=NOW)
finally:
event.remove(session.bind, "before_cursor_execute", count_statement)
assert summary["open_blockers"] == 130
assert len(statements) <= 18
assert not any("SELECT" in sql.upper() and "LIMIT 130" in sql.upper() for sql in statements)
def test_snapshot_coverage_exposes_unclassified_residual_and_overcount():
session_factory = setup_db()
with session_factory() as session:
tenant, agent, installation, snapshot = _installation(session, 1, coverage=80, blocked=1)
snapshot.total_products = 100
snapshot.fiscally_complete = 96
snapshot.safe_blocked = 1
snapshot.failed_products = 1
session.commit()
item = list_installations(session, page_size=5)["items"][0]
assert item["unclassified_products"] == 2
assert item["coverage_overcount_products"] == 0
assert item["coverage_invariant_pass"] is True
assert item["total_products"] == item["fiscally_complete"] + item["safe_blocked"] + item["failed_products"] + item["unclassified_products"]
snapshot.fiscally_complete = 99
session.commit()
inconsistent = list_installations(session, page_size=5)["items"][0]
assert inconsistent["unclassified_products"] == 0
assert inconsistent["coverage_overcount_products"] == 1
assert inconsistent["coverage_invariant_pass"] is False
def test_dashboard_coverage_totals_reconcile_and_keep_registry_metric_separate():
session_factory = setup_db()
with session_factory() as session:
tenant, _, installation, snapshot = _installation(session, 1, coverage=96, blocked=1)
snapshot.total_products = 100
snapshot.fiscally_complete = 96
snapshot.safe_blocked = 1
snapshot.failed_products = 1
_blocker(session, tenant, product="P-OPEN", field="CFOP", index=91, installation_id=installation.id)
session.commit()
summary = dashboard_summary(session, now=NOW)
assert summary["total_products"] == 100
assert summary["fiscally_complete"] == 96
assert summary["safe_blocked"] == 1
assert summary["failed_products"] == 1
assert summary["unclassified_products"] == 2
assert summary["coverage_overcount_products"] == 0
assert summary["coverage_invariant_pass"] is True
assert summary["safe_blocked_products"] == 1
assert summary["total_products"] == summary["fiscally_complete"] + summary["safe_blocked"] + summary["failed_products"] + summary["unclassified_products"]
def test_dashboard_marks_inconsistent_snapshot_partition_instead_of_clamping():
session_factory = setup_db()
with session_factory() as session:
_, _, _, snapshot = _installation(session, 1, coverage=100, blocked=2)
snapshot.total_products = 10
snapshot.fiscally_complete = 9
snapshot.safe_blocked = 2
snapshot.failed_products = 0
session.commit()
summary = dashboard_summary(session, now=NOW)
assert summary["coverage_overcount_products"] == 1
assert summary["coverage_invariant_pass"] is False
def test_attention_items_are_ranked_and_capped_at_ten():
session_factory = setup_db()
with session_factory() as session:
tenants = []
for index, health in enumerate(["FAILED"] * 2 + ["DEGRADED"] * 2 + ["OFFLINE"] * 2 + ["UNKNOWN"] * 2 + ["ONLINE"] * 4, 1):
tenant = _installation(session, index, health=health, version="0.9.0" if index >= 9 else "1.0.0rc1")[0]
tenants.append(tenant)
for index, tenant in enumerate(tenants[8:10], 20):
_blocker(session, tenant, product=f"P{index}", index=index)
session.commit()
items = dashboard_summary(session, now=NOW)["attention_items"]
assert len(items) <= 10
assert [item["technical_health"] for item in items[:8]] == ["FAILED"] * 2 + ["DEGRADED"] * 2 + ["OFFLINE"] * 2 + ["UNKNOWN"] * 2
assert all({"client", "display_name", "hostname", "problem", "timestamp", "age", "link", "technical_health"} <= item.keys() for item in items)
def test_installations_expose_separate_fiscal_and_technical_status_and_worst_sort():
session_factory = setup_db()
with session_factory() as session:
_installation(session, 1, coverage=90, blocked=0)
_installation(session, 2, coverage=40, blocked=1, version="0.9.0")
session.commit()
result = list_installations(session, sort="worst_coverage", page_size=10, now=NOW)
item = result["items"][0]
assert item["display_name"] == "Installation 2"
assert item["fiscal_completion_percent"] == 40
assert item["technical_health"] == "ONLINE"
assert item["fiscal_status"] == "BLOCKED"
assert item["version_drift"] == "OUTDATED"
assert item["technical_ids"]["agent_id"] == item["agent_id"]
def test_jobs_join_operational_identity_and_format_duration():
session_factory = setup_db()
with session_factory() as session:
tenant, agent, installation, _ = _installation(session, 1)
session.add(Job(agent_id=agent.id, kind="REPROCESS_ALL", status="FAILED", created_at=NOW, started_at=NOW, finished_at=NOW + timedelta(seconds=194), error="failure"))
session.commit()
result = list_jobs(session, client="Client 1", search="server-1", page_size=10)
item = result["items"][0]
assert item["client"] == tenant.name
assert item["display_name"] == installation.display_name
assert item["hostname"] == installation.hostname
assert item["job_id"]
assert item["duration"] == "3 min 14 s"
assert item["error"] == "failure"
def test_blockers_filter_with_priority_age_and_stable_pagination():
session_factory = setup_db()
with session_factory() as session:
tenant, _, installation, _ = _installation(session, 1)
critical = _blocker(session, tenant, product="P1", field="", blocker_type="TECHNICAL_FAILURE", index=1, installation_id=installation.id)
_blocker(session, tenant, product="P2", field="CFOP", index=2, installation_id=installation.id)
session.commit()
result = list_blockers(session, client="Client 1", installation=installation.display_name, status="OPEN", search="P1", page=1, page_size=1)
assert result["total"] == 1
assert result["items"][0]["id"] == critical.id
assert result["items"][0]["priority"] == "CRITICAL"
assert result["items"][0]["age"].startswith("há ")
assert result["items"][0]["installation_id"] == installation.installation_id
def test_blocker_detail_contains_context_events_jobs_and_resolution_audit():
session_factory = setup_db()
with session_factory() as session:
tenant, agent, installation, _ = _installation(session, 1)
blocker = _blocker(session, tenant, product="P1", index=1, installation_id=installation.id)
job = Job(agent_id=agent.id, kind="REPROCESS_ALL", status="FAILED", created_at=NOW, error="failed")
session.add(job)
session.flush()
record_event(session, tenant_id=tenant.id, installation_id=installation.id, agent_id=agent.id, severity="ERROR", event_type="JOB_FAILED", message="job failed", blocker_id=blocker.id, job_id=job.id, occurred_at=NOW)
record_audit(session, admin_user_id=None, tenant_id=tenant.id, action="RESOLVE_BLOCKER", target_type="blocker", target_id=str(blocker.id), context={"reason": "reviewed"})
session.commit()
from central.app.control_plane import blocker_detail
detail = blocker_detail(session, blocker.id)
assert detail["client"] == tenant.name
assert detail["installation"]["installation_id"] == installation.installation_id
assert detail["evidence"]["source"] == "test"
assert detail["related_events"][0]["job_id"] == job.id
assert detail["related_jobs"][0]["job_id"] == job.id
assert detail["resolution_audit"][0]["action"] == "RESOLVE_BLOCKER"
def test_blockers_keep_exact_installation_identity_for_same_tenant():
session_factory = setup_db()
with session_factory() as session:
tenant = Tenant(name="Shared Client")
session.add(tenant)
session.flush()
agent_a = Agent(tenant_id=tenant.id, name="agent-a", token_hash="token-a".ljust(64, "x"))
agent_b = Agent(tenant_id=tenant.id, name="agent-b", token_hash="token-b".ljust(64, "x"))
session.add_all([agent_a, agent_b])
session.flush()
installation_a = Installation(agent_id=agent_a.id, tenant_id=tenant.id, installation_id="00000000-0000-4000-8000-000000000101", display_name="Store A", hostname="server-a")
installation_b = Installation(agent_id=agent_b.id, tenant_id=tenant.id, installation_id="00000000-0000-4000-8000-000000000102", display_name="Store B", hostname="server-b")
session.add_all([installation_a, installation_b])
session.flush()
row_a = upsert_blocker(session, tenant.id, {"company": tenant.name, "product_code": "P-A", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "a"}, installation_id=installation_a.id)
row_b = upsert_blocker(session, tenant.id, {"company": tenant.name, "product_code": "P-B", "field": "CFOP", "reason_code": "MISSING", "input_fingerprint": "b"}, installation_id=installation_b.id)
result_a = list_blockers(session, tenant_id=tenant.id, installation="Store A")
result_b = list_blockers(session, tenant_id=tenant.id, installation="Store B")
assert result_a["total"] == 1 and result_a["items"][0]["id"] == row_a.id
assert result_a["items"][0]["installation_id"] == installation_a.installation_id
assert result_b["total"] == 1 and result_b["items"][0]["id"] == row_b.id
assert result_b["items"][0]["installation_id"] == installation_b.installation_id
def test_legacy_tenant_blocker_has_no_fabricated_installation_context():
session_factory = setup_db()
with session_factory() as session:
tenant, _, installation, _ = _installation(session, 1)
row = _blocker(session, tenant, product="LEGACY", index=99)
result = list_blockers(session, tenant_id=tenant.id, scope="all")
assert result["items"][0]["id"] == row.id
assert result["items"][0]["installation_id"] is None
from central.app.control_plane import blocker_detail
detail = blocker_detail(session, row.id)
assert detail["installation"]["installation_id"] is None
assert detail["server"]["hostname"] is None
assert installation.hostname == "server-1"
def test_dashboard_uses_sql_aggregates_for_operational_totals():
session_factory = setup_db()
with session_factory() as session:
tenant, _, _, _ = _installation(session, 1)
_blocker(session, tenant, product="P1", index=101)
statements = []
event.listen(session.bind, "before_cursor_execute", lambda conn, cursor, statement, parameters, context, executemany: statements.append(statement))
dashboard_summary(session, now=NOW)
assert any("COUNT" in statement.upper() and "BLOCKED_PRODUCTS" in statement.upper() for statement in statements)
assert any("DISTINCT" in statement.upper() and "BLOCKED_PRODUCTS" in statement.upper() for statement in statements)
def test_health_filter_is_paginated_in_sql_and_counts_all_matches():
session_factory = setup_db()
with session_factory() as session:
for index in range(1, 4):
_installation(session, index, health="ONLINE")
statements = []
event.listen(session.bind, "before_cursor_execute", lambda conn, cursor, statement, parameters, context, executemany: statements.append(statement))
result = list_installations(session, health="ONLINE", page=2, page_size=1, now=NOW)
assert result["total"] == 3
assert len(result["items"]) == 1
assert any("LIMIT" in statement.upper() and "INSTALLATIONS" in statement.upper() for statement in statements)
def test_worst_coverage_sort_uses_the_displayed_derived_percentage():
session_factory = setup_db()
with session_factory() as session:
_, _, first, first_snapshot = _installation(session, 1, coverage=80)
_, _, second, second_snapshot = _installation(session, 2, coverage=20)
first_snapshot.coverage_percent = 10
second_snapshot.coverage_percent = 90
session.commit()
result = list_installations(session, sort="worst_coverage", page_size=10, now=NOW)
assert result["items"][0]["installation_id"] == second.installation_id
assert result["items"][0]["fiscal_completion_percent"] == 20
assert first.installation_id != second.installation_id
def test_worst_coverage_sort_cannot_disagree_with_one_decimal_display():
session_factory = setup_db()
with session_factory() as session:
_, _, first, first_snapshot = _installation(session, 1, coverage=0)
_, _, second, second_snapshot = _installation(session, 2, coverage=0)
first_snapshot.total_products = 79
first_snapshot.fiscally_complete = 5
second_snapshot.total_products = 16
second_snapshot.fiscally_complete = 1
session.commit()
result = list_installations(session, sort="worst_coverage", page_size=10, now=NOW)
displayed = [item["fiscal_completion_percent"] for item in result["items"]]
assert displayed == sorted(displayed)
assert displayed == [6.3, 6.3]
assert {item["installation_id"] for item in result["items"]} == {first.installation_id, second.installation_id}
def test_lowercase_required_blocker_field_reaches_attention_candidates():
session_factory = setup_db()
with session_factory() as session:
tenant, _, installation, _ = _installation(session, 1)
_blocker(session, tenant, product="LOWER", field="cfop", index=120, installation_id=installation.id)
attention = dashboard_summary(session, now=NOW)["attention_items"]
assert any(item.get("field") == "CFOP" and item["link"].startswith("/blockers?scope=active") for item in attention)
def test_blocker_breakdown_query_groups_on_same_postgres_expression():
from sqlalchemy import select
query = select(BlockedProduct, Tenant).join(Tenant, Tenant.id == BlockedProduct.tenant_id)
sql = str(_blocker_breakdown_query(query).compile(dialect=postgresql.dialect()))
assert "GROUP BY blocked_products.field" in sql
assert "GROUP BY coalesce" not in sql
def test_remaining_technical_enums_have_human_labels():
from central.app.panel_presentation import health_reason_label
assert blocker_reason_label("INVALID_NCM") == "NCM inválido"
assert blocker_reason_label("LEGACY_NCM") == "NCM legado"
assert blocker_type_label("BLOCKED_MISSING_EVIDENCE") == "Evidência fiscal ausente"
assert health_reason_label("last heartbeat exceeds 300s") == "Sem heartbeat recente"
assert health_reason_label("persistent technical failure") == "Falha técnica persistente"
assert blocker_reason_label("SOMETHING_NEW") == "Something new" # unknown codes degrade readably, never crash
def test_rtc_status_view_is_discreet_and_optional():
from central.app.panel_presentation import rtc_status_view
assert rtc_status_view(None) is None and rtc_status_view({"coverage": {}}) is None
assert rtc_status_view({"fiscal_context": {"rtc_enabled": False}})["label"] == "Desativada"
on = rtc_status_view({"fiscal_context": {"rtc_enabled": True, "rtc_supported": True, "rtc_catalog_valid": True}})
assert on["label"] == "Ativa" and on["enabled"] and on["catalog_valid"]
assert blocker_type_label("RTC_CST_MISSING") == "CST IBS/CBS ausente no catálogo" and blocker_type_label("COFINS_RATE_INVALID") == "Alíquota COFINS inválida"
assert blocker_type_label("PIS_CST_INCIDENCE_MISMATCH") == "CST PIS incompatível com a incidência"
def test_rtc_status_view_hides_product_pending_when_cclasstrib_is_not_applicable():
from central.app.panel_presentation import rtc_status_view
result = rtc_status_view({"fiscal_context": {
"rtc_enabled": True,
"rtc_resolution": {"total": 521, "valid_existing": 0, "ready_to_apply": 0, "insufficient": 521},
"cclasstrib_applicability": {"state": "NOT_APPLICABLE", "basis": "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE"},
}})
assert result["enabled"] is True
assert result["products"]["applicable"] is False
assert result["products"]["label"] == "cClassTrib: Não utilizado neste cliente"
assert "pendente" not in result["products"].get("headline", "").lower()
@@ -0,0 +1,21 @@
from central.app.origin_resolution import resolve_origin
def test_origin_requires_strong_supplier_evidence():
result = resolve_origin("P1", {"value": "0", "strong_evidence": True, "conflict_count": 0}, {})
assert result["status"] == "ORIGIN_CONFIRMED"
assert result["value"] == "0"
def test_origin_conflict_stays_pending():
result = resolve_origin("P1", {"value": None, "strong_evidence": False, "conflict_count": 1}, {})
assert result["status"] == "ORIGIN_CONFLICT"
assert result["value"] is None
def test_origin_does_not_use_cst_digit():
result = resolve_origin("P1", {}, {"cst": "040"})
assert result["status"] == "ORIGIN_NEEDS_EVIDENCE"
+126
View File
@@ -0,0 +1,126 @@
"""OSCBR backfill: prioritised selection, promotion only on agreement, external conflict, quota (5/min, 50/day, reserve, reset), Cosmos out of the chain."""
from datetime import datetime, timedelta, timezone
import httpx
from sqlalchemy import select
from central.app.enrichment import backfill, history, kb, quota
from central.app.enrichment.providers import Oscbr, build_providers
from central.app.enrichment.quota import ProviderGateway, QuotaManager
from central.app.models import KbProduct, KbPromotionAudit, ProviderUsageDaily
from central.tests.test_enrichment_history import run, staging
from central.tests.test_product_enrichment_resolver import env, gtin # noqa: F401
import pytest
@pytest.fixture(autouse=True)
def _fresh_windows():
quota._windows.clear()
yield
quota._windows.clear()
def _oscbr(ncm_by_gtin, calls):
def handler(request):
if request.url.path == "/oauth/token":
return httpx.Response(200, json={"token": "t"})
g = request.url.path.rsplit("/", 1)[1]
calls.append(g)
if g in ncm_by_gtin:
return httpx.Response(200, json={"ean": g, "nome": "PRODUTO X", "marca": "M", "ncm": ncm_by_gtin[g], "cest": "0300700"})
return httpx.Response(404, json={})
prov = Oscbr(httpx.Client(transport=httpx.MockTransport(handler)), None, "u", "p")
prov.provides_ncm = True # simulates a plan/answer that DOES carry NCM: exercises the backfill/promotion logic (the real V3 role is IDENTITY)
return prov
def _seed(env, n=6):
gs = [gtin(f"78960000{i:04d}") for i in range(n)]
for i, g in enumerate(gs):
run(env, [staging("A", g, "22021000", years={2023: 30 + i, 2024: 40}, code=str(i), desc=f"PRODUTO TESTE {i} 350ML")])
return gs
def test_backfill_promotes_only_on_agreement_and_keeps_corroborated_when_no_ncm(env):
gs = _seed(env, 4)
prov = _oscbr({gs[0]: "22021000", gs[1]: "19059090"}, [])
cur = history.official_sets(env)
rep = backfill.run(env, ProviderGateway(env, [prov]), prov, limit=10, reserve_percent=20, is_official=lambda v: v in cur, wait_rate_window=False)
by = {r["gtin"]: r for r in rep["records"]}
assert by[gs[0]]["kb_status"] == "VERIFIED_KB" and by[gs[1]]["kb_status"] == "CONFLICT" and by[gs[1]]["external_conflict"]
assert by[gs[2]]["kb_status"] == "CORROBORATED" and by[gs[2]]["found"] is False
assert env.scalars(select(KbPromotionAudit)).all()[0].sources["providers"] == ["PROVIDER:oscbr"]
cest = [r for r in by.values() if r["cest"]]
assert cest and cest[0]["cest"] == "0300700" # CEST kept as evidence only
def test_backfill_never_selects_verified_or_conflict_or_rechecked(env):
gs = _seed(env, 3)
p = env.scalar(select(KbProduct).where(KbProduct.gtin == "0" + gs[0]))
kb.add_evidence(env, p, "ncm", "19059090", "x", family=kb.CLIENT_FAMILY, client_label="Z", lineage={"docs": 50, "years": {"2024": 25, "2025": 25}})
env.commit()
cur = history.official_sets(env)
sample = backfill.select_sample(env, lambda v: v in cur, 10, "oscbr")
assert "0" + gs[0] not in {s["gtin_key"] for s in sample} and len(sample) == 2
prov = _oscbr({}, [])
backfill.run(env, ProviderGateway(env, [prov]), prov, limit=10, reserve_percent=20, is_official=lambda v: v in cur, wait_rate_window=False)
assert backfill.select_sample(env, lambda v: v in cur, 10, "oscbr") == [] # checked once, next_checked_at in the future
def test_oscbr_quota_5_per_minute_50_per_day_reserve_and_reset(env):
prov = _oscbr({}, [])
gw = ProviderGateway(env, [prov])
quota._windows["oscbr"].clear()
outs = [gw.lookup(prov, gtin(f"78970000{i:04d}")).state for i in range(7)]
assert outs[:5] == ["NOT_FOUND"] * 5 and outs[5:] == ["SKIPPED", "SKIPPED"]
assert gw.lookup(prov, gtin("789700009991")).reason == "RATE_WINDOW"
usage = env.scalar(select(ProviderUsageDaily).where(ProviderUsageDaily.provider == "oscbr"))
assert usage.requests == 5
quota._windows["oscbr"].clear()
usage.requests = 40
assert gw.lookup(prov, gtin("789700009992"), "BACKFILL", 20).reason == "OPERATIONAL_RESERVE"
assert gw.lookup(prov, gtin("789700009993"), "OPERATIONAL").state == "NOT_FOUND"
quota._windows["oscbr"].clear()
usage.requests = 50
assert gw.lookup(prov, gtin("789700009994"), "OPERATIONAL").reason == "RATE_LIMITED"
quota._windows["oscbr"].clear()
tomorrow = QuotaManager(env, now=lambda: datetime.now(timezone.utc) + timedelta(days=1, minutes=1))
ok, reason, _ = tomorrow.allow(prov)
assert ok and reason is None
def test_cosmos_out_of_chain_and_token_preferred():
class S:
cosmos_token = "SECRET-C"
cosmos_enabled = False
oscbr_token = "SECRET-T"
oscbr_user = ""
oscbr_password = ""
kodebar_api_key = ""
allow_paid_providers = False
names = [p.name for p in build_providers(S())]
assert "cosmos" not in names and names[0] == "oscbr" and build_providers(S())[0].configured
S.cosmos_enabled = True
assert build_providers(S())[0].name == "cosmos"
def test_provider_query_canonical_forms():
assert backfill.provider_query("00000078936683") == "78936683" # GTIN-8
assert backfill.provider_query("07891000100103") == "7891000100103" # EAN-13
assert backfill.provider_query("17891000100100") == "17891000100100" # GTIN-14 with indicator
def test_oscbr_real_role_is_identity_and_uses_the_v3_endpoint():
seen = []
def handler(request):
seen.append(request.url.path)
return httpx.Response(200, json={"ean": "7891000100103", "ean_tipo": "EAN13", "nome": "COLA 350ML", "marca": "X", "categoria": "Bebidas", "link_foto": "u"})
p = Oscbr(httpx.Client(transport=httpx.MockTransport(handler)), "tok")
r = p.lookup_gtin("7891000100103")
assert seen == ["/api/v3/gtin/7891000100103"] and r.found and r.title == "COLA 350ML" and r.ncm is None and r.cest is None
assert Oscbr.provides_ncm is False
legacy = Oscbr(httpx.Client(transport=httpx.MockTransport(lambda q: httpx.Response(200, json={"nome": "FEIJAO", "ncm": 7133399}))), "tok").lookup_gtin("7896116900029")
assert legacy.ncm == "07133399" # 7-digit integer restored to the 8-digit NCM
@@ -0,0 +1,51 @@
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from central.app.panel_presentation import (
format_age,
format_duration,
format_ui_datetime,
health_label,
operational_priority,
)
def test_ui_times_use_configured_timezone_and_human_age(monkeypatch):
monkeypatch.setenv("UI_TIMEZONE", "America/Bahia")
value = datetime(2026, 9, 22, 19, 11, tzinfo=timezone.utc)
assert format_ui_datetime(value) == "22/09/2026 16:11"
assert format_age(value, datetime(2026, 9, 22, 19, 19, tzinfo=timezone.utc)) == "há 8 min"
def test_ui_datetime_treats_naive_values_as_utc():
value = datetime(2026, 9, 22, 19, 11)
assert format_ui_datetime(value) == "22/09/2026 16:11"
def test_age_handles_empty_and_future_values():
now = datetime(2026, 9, 22, 19, 19, tzinfo=timezone.utc)
assert format_age(None, now) == "-"
assert format_age(now + timedelta(minutes=1), now) == "agora"
assert format_age(now - timedelta(hours=2), now) == "há 2 h"
def test_duration_and_priority_are_deterministic():
start = datetime(2026, 9, 22, 19, 0, tzinfo=timezone.utc)
finish = start + timedelta(seconds=194)
assert format_duration(None, finish) == "-"
assert format_duration(start, None) == "em andamento"
assert format_duration(start, finish) == "3 min 14 s"
assert operational_priority(SimpleNamespace(blocker_type="TECHNICAL_FAILURE", field="", reason_code="X")) == "CRITICAL"
def test_health_label_maps_known_states_and_preserves_unknown_states():
assert health_label("ONLINE") == "Online"
assert health_label("DEGRADED") == "Degradado"
assert health_label("OFFLINE") == "Offline"
assert health_label("FAILED") == "Falha"
assert health_label("UNKNOWN") == "Desconhecido"
assert health_label("NEW_STATE") == "NEW_STATE"
+8
View File
@@ -0,0 +1,8 @@
def test_payload_compaction_preserves_candidate_codes_and_measures_reduction():
from central.app.ai.payloads import compact_payload, measure_payloads
payload = {"description": "agua", "debug": "remove", "official_candidates": [{"ncm": "22011000", "text": "x"}, {"ncm": "22011000", "text": "x"}, {"ncm": "22019000", "text": "y"}]}
compact = compact_payload(payload)
assert [row["ncm"] for row in compact["official_candidates"]] == ["22011000", "22019000"]
assert "debug" not in compact
assert measure_payloads([payload])["tokens_after"] < measure_payloads([payload])["tokens_before"]
@@ -0,0 +1,9 @@
def test_pre_zai_state_is_explicit_about_human_and_external_blockers(tmp_path):
from scripts.generate_pre_zai_state import build_state
state = build_state(tmp_path)
assert state["DETERMINISTIC_COMPLETE"] is True
assert state["COMPANY_CONTEXT_COMPLETE"] is False
assert state["FIREBIRD_AI_WRITES"] == 0
assert state["READY_FOR_ZAI_GOLD_EVAL"] is True
assert state["REMAINING_EXTERNAL_BLOCKERS"] == ["EXTERNAL_AI_CREDENTIAL_OR_BALANCE"]
@@ -0,0 +1,16 @@
from central.app.product_data_provider import ProviderRecord, cache_key, normalize_record
def test_provider_record_has_stable_gtin_cache_key_and_hash():
record = normalize_record("GS1", "v1", "789", {"brand": "Acme", "description": "Produto"})
assert isinstance(record, ProviderRecord)
assert record.raw_hash
assert cache_key(record.gtin, record.provider, record.provider_record_version).startswith("789:")
def test_provider_normalization_does_not_claim_fiscal_authority():
record = normalize_record("COMMERCIAL", "v1", "789", {"ncm": "12345678"})
assert record.normalized_data["ncm"] == "12345678"
assert record.fiscal_authority == "OBSERVATION_ONLY"
@@ -0,0 +1,73 @@
import json
from pathlib import Path
import pytest
from central.app.product_defaults import cfop_default, cst_status, csosn_default, validate_against_history
CFOPS = {"5102", "5405", "5656", "1102"}
CSOSNS = {"101", "102", "103", "500", "400"}
def P(**kw):
return {"CFOP": "5102", "CSOSN": "102", "CST": "040", **kw}
def test_cfop_consistent_non_st_is_resolved_but_never_a_write():
r = cfop_default(P(), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)
assert r["status"] == "CFOP_DEFAULT_RESOLVED" and r["action_class"] == "ALREADY_CORRECT" and not r["write_eligible"]
def test_cfop_st_conflict_is_candidate_review_not_write():
r = cfop_default(P(), "EXACT_NCM", "MVA_RESOLVED", CFOPS)
assert r["status"] == "CFOP_DEFAULT_CANDIDATE" and r["proposed_value"] == "5405" and r["action_class"] == "REVIEW_REQUIRED" and not r["write_eligible"]
def test_cfop_insufficient_context_never_resolved():
assert cfop_default(P(), "EXACT_NCM", "MVA_CANDIDATES", CFOPS)["status"] == "CFOP_OPERATION_CONTEXT_REQUIRED"
assert cfop_default(P(CFOP=None), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)["status"] == "CFOP_OPERATION_CONTEXT_REQUIRED"
assert cfop_default(P(CFOP="6102"), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS | {"6102"})["status"] == "CFOP_OPERATION_CONTEXT_REQUIRED"
assert cfop_default(P(), "LEGACY_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)["status"] == "CFOP_UNRESOLVED"
assert cfop_default(P(CFOP="9999"), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CFOPS)["status"] == "CFOP_UNRESOLVED"
def test_csosn_is_never_resolved_and_never_universal_102():
for mva in ("MVA_RESOLVED", "MVA_CANDIDATES", "NO_MVA_MAPPING_FOUND"):
r = csosn_default(P(), "EXACT_NCM", mva, CSOSNS, "102")
assert r["status"] != "CSOSN_DEFAULT_RESOLVED" and not r["write_eligible"]
r = csosn_default(P(CSOSN="101"), "EXACT_NCM", "NO_MVA_MAPPING_FOUND", CSOSNS, "102")
assert r["proposed_value"] is None
def test_csosn_st_subject_flags_conflict_and_requires_retention_context():
r = csosn_default(P(), "EXACT_NCM", "MVA_RESOLVED", CSOSNS, "102")
assert r["status"] == "CSOSN_OPERATION_CONTEXT_REQUIRED" and r["consistency_flag"] == "ST_SUBJECT_BUT_CSOSN_NOT_500"
def test_history_validates_but_does_not_create_rules():
assert validate_against_history("5102", ["5102", "5102"]) == "MATCH"
assert validate_against_history("5102", ["5102", "5405"]) == "MISMATCH"
assert validate_against_history("5102", []) == "UNVALIDATED"
assert validate_against_history(None, ["5102"]) == "UNVALIDATED" # history alone never proposes a value
def test_cst_is_reference_only_under_crt1():
r = cst_status(P(), {"40", "00"}, set("012345678"))
assert r["status"] == "CST_REFERENCE_ONLY" and r["origin_digit"] == "0" and not r["write_eligible"]
assert cst_status(P(CST="99"), {"40"}, set("0"))["status"] == "CST_INVALID_REVIEW"
def test_engine_is_deterministic_idempotent():
a = cfop_default(P(), "EXACT_NCM", "MVA_RESOLVED", CFOPS)
assert a == cfop_default(P(), "EXACT_NCM", "MVA_RESOLVED", CFOPS)
REC = Path(__file__).resolve().parents[2] / "docs" / "NCM_HISTORICAL_RECONCILIATION_V1.json"
@pytest.mark.skipif(not REC.exists(), reason="reconciliation artifact not generated")
def test_reconciliation_accounts_for_every_mismatch_and_finds_no_technical_bug():
d = json.loads(REC.read_text(encoding="utf-8"))
assert sum(d["classification"].values()) == d["mismatches_total"]
assert d["classification"].get("NORMALIZATION_BUG", 0) == 0
assert d["trusted_corpus"]["TRUSTED_HISTORICAL_ITEMS"] > 0
@@ -0,0 +1,257 @@
"""Hub side of the enrichment: feature gates, resolve endpoint, audit, human override, blockers, UI route, waiting-for-configuration."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.enrichment import api as enrich_api
from central.app.models import AuditEvent, BlockedProduct, Installation, Job, ProductEnrichment, ProductFieldAudit
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_product_enrichment_resolver import COSMOS, Fake, G1, G4, seed
from central.tests.test_rtc_security import _become, _post
from central.tests.test_zero_touch_hub import H, _client, hb
def setup(monkeypatch, mode="ENABLED"):
client, engine = _client()
hb(client)
with Session(engine) as s:
seed(s)
inst = s.scalars(select(Installation)).one()
inst.enrichment_mode = mode
s.commit()
monkeypatch.setattr(enrich_api, "PROVIDERS_OVERRIDE", [Fake("cosmos", COSMOS(), True), Fake("other", COSMOS(), True)])
return client, engine
def resolve(client, code="N1", gtin=G1, desc="REFRIGERANTE COLA LATA 350ML", current=None, ev=None):
body = {"product_code": code, "gtin": gtin, "description": desc, "current": current or {}}
if ev is not None:
body["ev"] = ev
return client.post("/v1/products/resolve", json=body, headers=H).json()
def test_disabled_installation_is_not_processed_and_never_calls_providers(monkeypatch):
client, engine = setup(monkeypatch, "DISABLED")
body = resolve(client)
assert body["state"] == "FEATURE_DISABLED" and enrich_api.PROVIDERS_OVERRIDE[0].calls == 0
with Session(engine) as s:
assert s.scalars(select(ProductEnrichment)).all() == []
def test_enabled_resolve_returns_field_decisions_and_records_state(monkeypatch):
client, engine = setup(monkeypatch)
body = resolve(client)
assert body["state"] == "MULTI_SOURCE_MATCH" and body["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE"
assert body["mode"] == "ENABLED" and "COD_ORIGEM_MERCADORIA" not in body["fields"]
with Session(engine) as s:
row = s.scalars(select(ProductEnrichment)).one()
assert row.product_code == "N1" and row.status == "RESOLVED" and row.gtin.endswith(G1)
def test_applied_report_is_audited_and_human_override_is_final(monkeypatch):
client, engine = setup(monkeypatch)
resolve(client)
fields = [{"field": "CLASSIFICACAO_FISCAL", "outcome": "APPLIED", "old": None, "new": "22021000", "rule": "NCM_TWO_INDEPENDENT_SOURCES", "kb_status": "VERIFIED_KB", "evidence": ["PROVIDER:cosmos"]}]
assert client.post("/v1/products/enrichment/applied", json={"product_code": "N1", "fields": fields, "dataset_versions": {"NCM": {"sha256": "a"}}}, headers=H).json()["ok"]
client.post("/v1/products/enrichment/applied", json={"product_code": "N1", "fields": [{"field": "CEST", "outcome": "HUMAN_OVERRIDDEN", "old": "0300700", "new": None}]}, headers=H)
again = resolve(client)
assert again["fields"]["CEST"]["status"] == "HUMAN_OVERRIDDEN"
with Session(engine) as s:
audit = s.scalars(select(ProductFieldAudit).order_by(ProductFieldAudit.id)).all()
assert [(a.field, a.outcome) for a in audit] == [("CLASSIFICACAO_FISCAL", "APPLIED"), ("CEST", "HUMAN_OVERRIDDEN")]
assert audit[0].dataset_versions == {"NCM": {"sha256": "a"}} and audit[0].old_value is None
assert client.post("/v1/products/enrichment/applied", json={"product_code": "UNKNOWN", "fields": []}, headers=H).status_code == 404
def test_resolve_wires_field_contract_fiscal_completion_fields_alongside_ncm(monkeypatch):
"""field_contract.resolve_product was previously only exercised by its own unit tests. This proves the LIVE /v1/products/resolve endpoint
now calls it too: once the NCM auto-fills, CFOP/CST/CSOSN/PIS-COFINS/... decisions ride along in the same response - no second engine, no
separate round trip - using company_uf/company_crt already auto-discovered from the Agent's heartbeat (Installation.company_uf/company_crt)."""
from central.app.enrichment import fiscal_completion as fcomp
from central.app.models import FiscalContextEntry
client, engine = setup(monkeypatch)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
inst.company_uf, inst.company_crt, inst.company_code = "BA", "1", "000001"
s.commit()
tenant_id = inst.tenant_id
body = resolve(client)
assert body["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE"
for field in fcomp.FISCAL_COMPLETION_FIELDS: # every fiscal-completion field gets an explicit decision, never silently missing
assert field in body["fields"], field
assert body["fields"]["CSOSN"]["status"] == "NEEDS_REVIEW" and body["fields"]["CSOSN"]["missing_key"] == "icms_treatment" # 0 FiscalContextEntry rows: correctly blocked, not guessed
with Session(engine) as s:
s.add(FiscalContextEntry(tenant_id=tenant_id, company_key="000001", scope="COMPANY", scope_key="", key="icms_treatment", value="TAXED", source="OPERATOR"))
s.commit()
again = resolve(client, code="N9")
assert again["fields"]["CSOSN"]["status"] == "AUTO_FILL_SAFE" and again["fields"]["CSOSN"]["value"] == "102" # same operator answer, field independently resolved once context exists
def test_resolve_accepts_agents_targeted_product_evidence_end_to_end(monkeypatch):
"""The Agent's targeted, product_code-scoped recipes/composed/purchases/stock evidence (firebird_reader.read_product_evidence) rides the
resolve payload as `ev` and reaches field_contract.infer_role - proven end-to-end through the real HTTP route, not just the fiscal_completion
unit tests. Recipe evidence alone proves OWN_PRODUCTION with zero operator questions."""
client, engine = setup(monkeypatch)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
inst.company_uf, inst.company_crt, inst.company_code = "BA", "1", "000001"
s.commit()
body = resolve(client, code="N10", ev={"recipes": ["N10"], "composed": [], "purchases": {}, "stock_entry": {}, "stock_in": {}})
assert body["fields"]["CFOP"]["status"] == "NEEDS_REVIEW" and body["fields"]["CFOP"].get("missing_key") != "product_role"
def test_unknown_gtin_creates_a_human_blocker_in_portuguese(monkeypatch):
client, engine = setup(monkeypatch)
monkeypatch.setattr(enrich_api, "PROVIDERS_OVERRIDE", [Fake("cosmos", {}, True)])
assert resolve(client, "N2", G4, "PRODUTO XYZ")["state"] == "NOT_FOUND"
with Session(engine) as s:
b = s.scalars(select(BlockedProduct).where(BlockedProduct.field == "ENRICHMENT")).one()
assert b.reason_code == "NOT_FOUND" and "não encontrado" in b.evidence_summary["message"]
def test_authorization_endpoint_and_job_gating(monkeypatch):
client, engine = setup(monkeypatch)
assert client.get("/v1/products/enrichment/authorization", headers=H).json()["enrichment_mode"] == "ENABLED"
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
inst.features_configured = False
s.commit()
assert client.get("/v1/jobs/next", headers=H).json() is None # WAITING_FEATURE_CONFIGURATION: nothing is handed out
body = client.get("/v1/agents/me/authorizations", headers=H).json()
assert body["features"]["features_configured"] is False
def test_features_route_is_admin_only_csrf_and_audited():
client, agent_id, _ = _panel_client()
_become(client, "PADRAO")
assert _post(client, f"/agents/{agent_id}/features", enrichment_mode="DRY_RUN", corrector_enabled="1").status_code == 403
_become(client, "ADMIN")
assert _post(client, f"/agents/{agent_id}/features", enrichment_mode="ENABLED", corrector_enabled="1").status_code == 422 # needs typed confirmation
assert _post(client, f"/agents/{agent_id}/features", enrichment_mode="DRY_RUN", corrector_enabled="1").status_code == 303
assert client.post(f"/agents/{agent_id}/features", data={"csrf_token": "bad", "enrichment_mode": "DRY_RUN"}, follow_redirects=False).status_code in (400, 403)
with client._session_factory() as s:
inst = s.scalars(select(Installation)).one()
assert inst.enrichment_mode == "DRY_RUN" and inst.features_configured and inst.corrector_enabled
assert "INSTALLATION_FEATURES" in [e.action for e in s.scalars(select(AuditEvent))]
page = client.get(f"/agents/{agent_id}")
assert "Funcionalidades" in page.text and "Enriquecimento" in page.text
def test_dashboard_shows_sources_without_tokens_and_official_freshness(monkeypatch):
from central.app.enrichment import observability, providers
monkeypatch.setattr(observability, "build_providers", lambda _s: providers.build_providers(type("S", (), {"oscbr_token": "SEGREDO-NAO-VAZAR", "allow_paid_providers": False})()))
client, agent_id, _ = _panel_client()
page = client.get("/").text
assert "Fontes de produtos" in page and "oscbr" in page and "cosmos" not in page and "configurado: sim" in page and "upcitemdb" in page and "openfoodfacts" in page
assert "SEGREDO-NAO-VAZAR" not in page and "Fontes oficiais" in page and "NCM (Siscomex)" in page
def _second_agent(engine, mode="ENABLED", corrector=True):
from central.app.auth import token_hash
from central.app.models import Agent, Tenant
with Session(engine) as s:
first = s.scalars(select(Installation)).first()
agent = Agent(tenant_id=first.tenant_id, name="second", token_hash=token_hash("second-token"), enabled=True)
s.add(agent)
s.flush()
s.add(Installation(agent_id=agent.id, tenant_id=first.tenant_id, installation_id="00000000-0000-4000-8000-0000000000bb", display_name="B", features_configured=True, corrector_enabled=corrector, enrichment_mode=mode))
s.commit()
return {"Authorization": "Bearer second-token"}
def test_same_product_in_another_installation_costs_zero_extra_provider_requests(monkeypatch):
client, engine = setup(monkeypatch)
h2 = _second_agent(engine)
providers = enrich_api.PROVIDERS_OVERRIDE
first = resolve(client)
calls_after_first = [p.calls for p in providers]
second = client.post("/v1/products/resolve", json={"product_code": "X9", "gtin": G1, "description": "REFRIGERANTE COLA LATA 350ML", "current": {}}, headers=h2).json()
assert [p.calls for p in providers] == calls_after_first # other installation: served by KB, zero external calls
assert second["state"] == "EXACT_GTIN_VERIFIED" and second["fields"]["CLASSIFICACAO_FISCAL"]["value"] == first["fields"]["CLASSIFICACAO_FISCAL"]["value"]
with Session(engine) as s:
assert {r.installation_id for r in s.scalars(select(ProductEnrichment))} == {1, 2}
def test_installations_are_isolated_by_their_own_feature_flags(monkeypatch):
client, engine = setup(monkeypatch, "DISABLED")
h2 = _second_agent(engine, "ENABLED")
assert resolve(client)["state"] == "FEATURE_DISABLED"
body = client.post("/v1/products/resolve", json={"product_code": "N1", "gtin": G1, "description": "REFRIGERANTE COLA LATA 350ML", "current": {}}, headers=h2).json()
assert body["state"] != "FEATURE_DISABLED" and body["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE"
def test_quota_exhausted_gives_pending_state_and_retry_time_through_the_api(monkeypatch):
client, engine = setup(monkeypatch)
monkeypatch.setattr(enrich_api, "PROVIDERS_OVERRIDE", [Fake("cosmos", COSMOS(), True, daily_limit=1)])
resolve(client, "A1", G1)
body = resolve(client, "A2", G4, "OUTRO PRODUTO")
assert body["state"] == "PENDING_PROVIDER_QUOTA" and body["retry_at"]
with Session(engine) as s:
row = s.scalars(select(ProductEnrichment).where(ProductEnrichment.product_code == "A2")).one()
assert row.status == "PENDING" and row.next_attempt_at is not None
assert s.scalars(select(BlockedProduct).where(BlockedProduct.reason_code == "PENDING_PROVIDER_QUOTA")).one().field == "ENRICHMENT"
def test_corrector_switch_off_stops_jobs_but_not_enrichment(monkeypatch):
client, engine = setup(monkeypatch)
with Session(engine) as s:
s.scalars(select(Installation)).one().corrector_enabled = False
s.commit()
assert client.get("/v1/jobs/next", headers=H).json() is None
assert resolve(client)["state"] != "FEATURE_DISABLED"
import pytest
@pytest.mark.parametrize("corrector,mode,configured,jobs,enrich", [
(True, "DISABLED", True, True, False), # Corrector ON / Enrichment OFF
(False, "DRY_RUN", True, False, True), # Corrector OFF / DRY_RUN
(False, "ENABLED", True, False, True), # Corrector OFF / ENABLED
(True, "ENABLED", True, True, True), # both ON
(False, "DISABLED", False, False, False), # no features -> WAITING_FEATURE_CONFIGURATION
])
def test_feature_mode_matrix(monkeypatch, corrector, mode, configured, jobs, enrich):
client, engine = setup(monkeypatch, mode)
with Session(engine) as s:
i = s.scalars(select(Installation)).one()
i.corrector_enabled, i.features_configured = corrector, configured
from central.app.jobs import create_job
from central.app.models import Agent
create_job(s, s.scalars(select(Agent)).one(), "SYNC_INCREMENTAL", {})
s.commit()
assert (client.get("/v1/jobs/next", headers=H).json() is not None) == jobs
assert (resolve(client)["state"] != "FEATURE_DISABLED") == enrich
feats = client.get("/v1/agents/me/authorizations", headers=H).json()["features"]
assert feats["features_configured"] == configured
def test_provider_panel_shows_every_state_and_column(monkeypatch):
from datetime import datetime, timedelta, timezone
from central.app.models import ProviderState, ProviderUsageDaily
client, agent_id, _ = _panel_client()
now = datetime.now(timezone.utc)
with client._session_factory() as s:
s.add(ProviderState(provider="upcitemdb", configured=True, status="RATE_LIMITED", quota_kind="REMOTE_QUOTA", daily_limit=100, remaining=0, reset_at=now + timedelta(hours=3), last_success_at=now, last_error="RATE_LIMITED: HTTP 429", last_latency_ms=140))
s.add(ProviderState(provider="openfoodfacts", configured=True, status="DEGRADED", last_latency_ms=655))
s.add(ProviderState(provider="openproductsfacts", configured=True, status="AUTH_ERROR"))
s.add(ProviderState(provider="openbeautyfacts", configured=True, status="OFFLINE"))
s.add(ProviderUsageDaily(provider="upcitemdb", day=now.strftime("%Y-%m-%d"), requests=100, cache_hits=30, cache_misses=70))
s.commit()
page = client.get("/").text
for label in ("Limite atingido", "Instável", "Credencial recusada", "Indisponível", "Não configurado", "100 / 100", "restam 0", "renova", "último sucesso", "último erro", "140 ms", "cache 30%"):
assert label in page, label
def test_dashboard_kb_panel_and_provider_daily_columns(monkeypatch):
from central.app.enrichment import kb as kbm
from central.app.models import KbProduct
client, agent_id, _ = _panel_client()
with client._session_factory() as s:
p = kbm.upsert_product(s, "07891000100103", "COLA LATA 350ML")
kbm.add_evidence(s, p, "ncm", "22021000", "c", family=kbm.CLIENT_FAMILY, client_label="A", lineage={"docs": 900, "years": {"2024": 900}})
kbm.add_evidence(s, p, "ncm", "19059090", "c", family=kbm.CLIENT_FAMILY, client_label="B", lineage={"docs": 30, "years": {"2025": 30}})
kbm.evaluate(s, p, "ncm", lambda v: True)
s.commit()
page = client.get("/").text
assert "Base de conhecimento de produtos" in page and "NCMs divergentes" in page and "22021000" in page and "19059090" in page
assert "achados" in page and "próximo reset" in page or "hoje:" in page
@@ -0,0 +1,327 @@
"""Product enrichment resolver: 12 resolution scenarios, quota/cache/circuit, provider fixtures. No internet."""
import json
from datetime import datetime, timedelta, timezone
import httpx
import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.enrichment import kb
from central.app.enrichment.gtin import check_digit_ok
from central.app.enrichment.providers import Kodebar, Oscbr, Cosmos, OpenFoodFacts, Provider, ProviderError, ProviderProduct, UpcItemDb, build_providers
from central.app.enrichment.quota import ProviderGateway
from central.app.enrichment.resolver import resolve
from central.app.models import (Base, CestEntry, CestNcmEntry, Dataset, GtinLookupCache, KbKnowledge, NcmEntry, ProviderUsageDaily, TipiEntry)
def gtin(body12: str) -> str:
total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(body12)))
return body12 + str((10 - total % 10) % 10)
G1, G2, G3, G4 = gtin("789100010010"), gtin("789100010020"), gtin("789100010030"), gtin("789100010040")
class Fake(Provider):
def __init__(self, name, data=None, ncm_provider=False, error=None, daily_limit=None):
super().__init__(client=httpx.Client())
self.name, self.data, self.provides_ncm, self.error, self.calls = name, data or {}, ncm_provider, error, 0
self.daily_limit = daily_limit
def lookup_gtin(self, g):
self.calls += 1
if self.error:
raise self.error
d = self.data.get(g) or self.data.get(g.lstrip('0'))
return ProviderProduct(self.name, g, True, **d) if d else ProviderProduct(self.name, g, False)
def seed(session):
ncm = Dataset(kind="NCM", provider="Siscomex", sha256="a" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=3)
session.add(ncm)
session.flush()
for code in ("22021000", "19059090", "21069090"):
session.add(NcmEntry(dataset_id=ncm.id, code=code, description="x", valid_from="01/04/2022", valid_to="31/12/9999"))
session.add(NcmEntry(dataset_id=ncm.id, code="11111111", description="expired", valid_from="01/01/2000", valid_to="31/12/2001"))
tipi = Dataset(kind="TIPI_IPI", provider="RFB", sha256="b" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
cest = Dataset(kind="CEST", provider="CONFAZ", sha256="c" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add_all([tipi, cest])
session.flush()
session.add(TipiEntry(dataset_id=tipi.id, ncm="22021000", ex=None, description="x", ipi_rate=0.0, ipi_rate_text="0", source_row=1))
entry = CestEntry(dataset_id=cest.id, code="03.007.00", description="REFRIGERANTE COLA LATA 350ML refrigerantes", annex="I")
session.add(entry)
session.flush()
session.add(CestNcmEntry(cest_entry_id=entry.id, ncm_code="22021000"))
session.commit()
@pytest.fixture()
def env():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
session = Session(engine)
seed(session)
yield session
session.close()
def run(session, providers, g, desc, current=None, **kw):
return resolve(session, ProviderGateway(session, providers), gtin_raw=g, description=desc, current=current or {}, **kw)
COSMOS = lambda ncm="22021000": {G1: dict(title="REFRIGERANTE COLA LATA 350ML", brand="X", ncm=ncm)}
UPC = {G1: dict(title="Refrigerante Cola Lata 350ml", brand="X")}
def test_1_gtin_verified_kb_no_provider_call(env):
p = kb.upsert_product(env, kb.gtin_fingerprint("0" + G1), "COLA", brand=None) if False else kb.upsert_product(env, "0" + G1, "REFRIGERANTE COLA LATA 350ML")
kb.add_evidence(env, p, "ncm", "22021000", "provider:cosmos", family="PROVIDER:cosmos")
kb.add_evidence(env, p, "ncm", "22021000", "provider:other", family="PROVIDER:other")
env.commit()
fake = Fake("cosmos", COSMOS(), True)
r = run(env, [fake], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["state"] == "EXACT_GTIN_VERIFIED" and fake.calls == 0
f = r["fields"]
assert f["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE" and f["CLASSIFICACAO_FISCAL"]["value"] == "22021000"
assert f["CEST"]["value"] == "0300700" and f["IPI"]["value"] == "0.0"
def test_2_two_independent_providers_agree_multi_source(env):
a, b = Fake("cosmos", COSMOS(), True), Fake("other", {G1: dict(title="REFRIGERANTE COLA LATA 350ML", ncm="22021000")}, True)
r = run(env, [a, b], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["state"] == "MULTI_SOURCE_MATCH"
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "AUTO_FILL_SAFE"
# promoted: a second installation is served from the KB alone
a2 = Fake("cosmos", COSMOS(), True)
r2 = run(env, [a2], G1, "REFRIGERANTE COLA LATA 350ML")
assert r2["state"] == "EXACT_GTIN_VERIFIED" and a2.calls == 0
def test_3_single_source_ncm_is_suggestion_only(env):
r = run(env, [Fake("cosmos", COSMOS(), True)], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "SUGGESTION_ONLY"
assert r["fields"]["CEST"]["status"] == "INSUFFICIENT_EVIDENCE" and r["fields"]["IPI"]["status"] == "INSUFFICIENT_EVIDENCE"
def test_4_identity_only_sources_never_give_ncm(env):
r = run(env, [Fake("upcitemdb", UPC), Fake("openfoodfacts", UPC)], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["state"] == "MULTI_SOURCE_MATCH"
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "INSUFFICIENT_EVIDENCE"
def test_5_conflicting_ncm_zero_write(env):
a, b = Fake("cosmos", COSMOS("22021000"), True), Fake("other", COSMOS("19059090"), True)
r = run(env, [a, b], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["state"] == "CONFLICT"
assert all(v["status"] != "AUTO_FILL_SAFE" for v in r["fields"].values())
def test_6_ncm_not_in_official_dataset_rejected(env):
a, b = Fake("cosmos", COSMOS("99999999"), True), Fake("other", COSMOS("99999999"), True)
r = run(env, [a, b], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "INSUFFICIENT_EVIDENCE"
a, b = Fake("cosmos", COSMOS("11111111"), True), Fake("other", COSMOS("11111111"), True) # expired NCM is not vigente
assert run(env, [a, b], gtin("789100010050"), "X")["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "INSUFFICIENT_EVIDENCE"
def test_7_invalid_gtin_never_calls_providers(env):
fake = Fake("cosmos", COSMOS(), True)
r = run(env, [fake], G1[:-1] + str((int(G1[-1]) + 1) % 10), "PIPOCA")
assert r["gtin_status"] == "INVALID_GTIN" and r["state"] == "INVALID_GTIN" and fake.calls == 0
def test_8_gtin_not_found_anywhere(env):
r = run(env, [Fake("cosmos", {}, True), Fake("upcitemdb")], G2, "PRODUTO QUALQUER")
assert r["state"] == "NOT_FOUND" and r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "INSUFFICIENT_EVIDENCE"
def test_9_name_only_exact_verified_fingerprint(env):
p = kb.upsert_product(env, "0" + G1, "REFRIGERANTE COLA ORIGINAL LATA 350 ML")
for fam in ("PROVIDER:a", "PROVIDER:b"):
kb.add_evidence(env, p, "ncm", "22021000", "x", family=fam)
env.commit()
r = run(env, [Fake("cosmos")], "", "REFRIGERANTE COLA ORIGINAL LATA 350ML")
assert r["state"] == "EXACT_KB_NAME_VERIFIED" and r["fields"]["CLASSIFICACAO_FISCAL"]["value"] == "22021000"
def test_10_generic_name_never_fills(env):
p = kb.upsert_product(env, "0" + G1, "REFRIGERANTE")
for fam in ("PROVIDER:a", "PROVIDER:b"):
kb.add_evidence(env, p, "ncm", "22021000", "x", family=fam)
env.commit()
r = run(env, [], "", "REFRIGERANTE")
assert r["state"] == "AMBIGUOUS" and all(v["status"] != "AUTO_FILL_SAFE" for v in r["fields"].values())
def test_11_name_matching_different_ncms_is_conflict(env):
for g, ncm in ((G1, "22021000"), (G3, "21069090")):
p = kb.upsert_product(env, "0" + g, "SUCO NATURAL LARANJA GARRAFA 1L")
for fam in ("PROVIDER:a", "PROVIDER:b"):
kb.add_evidence(env, p, "ncm", ncm, "x", family=fam)
env.commit()
r = run(env, [], "", "SUCO NATURAL LARANJA GARRAFA 1L")
assert r["state"] == "CONFLICT" and all(v["status"] == "CONFLICT" for v in r["fields"].values())
def test_12_already_filled_fields_are_never_touched(env):
p = kb.upsert_product(env, "0" + G1, "REFRIGERANTE COLA LATA 350ML")
for fam in ("PROVIDER:a", "PROVIDER:b"):
kb.add_evidence(env, p, "ncm", "22021000", "x", family=fam)
env.commit()
r = run(env, [], G1, "REFRIGERANTE COLA LATA 350ML", current={"CLASSIFICACAO_FISCAL": "19059090", "CEST": "", "IPI": None})
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "ALREADY_SET"
def test_client_consensus_needs_two_distinct_clients(env):
p = kb.upsert_product(env, "0" + G1, "X")
kb.add_evidence(env, p, "ncm", "22021000", "client", family=kb.CLIENT_FAMILY, client_label="A")
kb.add_evidence(env, p, "ncm", "22021000", "client", family=kb.CLIENT_FAMILY, client_label="A")
assert kb.evaluate(env, p, "ncm", lambda v: True)["status"] == kb.OBSERVED
kb.add_evidence(env, p, "ncm", "22021000", "client", family=kb.CLIENT_FAMILY, client_label="B")
assert kb.evaluate(env, p, "ncm", lambda v: True)["status"] == kb.CORROBORATED # consensus alone is one family, never authority
kb.add_evidence(env, p, "ncm", "22021000", "provider", family="PROVIDER:cosmos")
assert kb.evaluate(env, p, "ncm", lambda v: True)["status"] == kb.VERIFIED_KB
def test_cache_spares_quota_across_installations(env):
fake = Fake("upcitemdb", UPC)
for _ in range(5):
run(env, [fake], G1, "REFRIGERANTE COLA LATA 350ML")
assert fake.calls == 1
usage = env.scalar(select(ProviderUsageDaily).where(ProviderUsageDaily.provider == "upcitemdb"))
assert usage.requests == 1 and usage.cache_hits == 4
def test_negative_cache(env):
fake = Fake("upcitemdb")
for _ in range(3):
run(env, [fake], G2, "PRODUTO X")
assert fake.calls == 1
def test_quota_exhausted_pending_with_retry(env):
fake = Fake("cosmos", COSMOS(), True, daily_limit=1)
run(env, [fake], G1, "REFRIGERANTE COLA LATA 350ML")
r = run(env, [fake], G3, "OUTRO PRODUTO")
assert r["state"] == "PENDING_PROVIDER_QUOTA" and r["retry_at"] is not None and fake.calls == 1
def test_circuit_breaker_and_timeout_is_not_a_crash(env):
fake = Fake("upcitemdb", error=ProviderError("TIMEOUT", "x"))
states = [run(env, [fake], gtin(f"78910001{i:04d}"), "P")["state"] for i in range(6)]
assert fake.calls == 3 # circuit opened after 3 failures, no more calls
assert set(states) == {"PENDING_PROVIDERS_OFFLINE"}
def test_cosmos_without_token_is_not_configured_and_costs_nothing(env):
cosmos = Cosmos(httpx.Client(transport=httpx.MockTransport(lambda r: pytest.fail("no call expected"))), None)
r = run(env, [cosmos], G1, "REFRIGERANTE COLA LATA 350ML")
assert r["providers"][0]["reason"] == "NOT_CONFIGURED" and r["state"] == "NOT_FOUND"
def test_paid_provider_guard():
class S: allow_paid_providers = False; cosmos_token = None
assert [p.name for p in build_providers(S())] == ["oscbr", "kodebar", "upcitemdb", "openfoodfacts", "openproductsfacts", "openbeautyfacts"]
# ---- provider contract tests against recorded fixtures (httpx.MockTransport, no network) ----------------------------------------------------------
def _client(handler):
return httpx.Client(transport=httpx.MockTransport(handler))
def test_upcitemdb_contract():
seen = {}
def handler(request):
seen["url"] = str(request.url)
return httpx.Response(200, json={"code": "OK", "total": 1, "items": [{"ean": G1, "title": "Cola 350ml", "brand": "X", "category": "Beverages", "size": "350ml"}]},
headers={"X-RateLimit-Limit": "100", "X-RateLimit-Remaining": "99", "X-RateLimit-Reset": "1900000000"})
p = UpcItemDb(_client(handler)).lookup_gtin(G1)
assert p.found and p.title == "Cola 350ml" and p.ncm is None and "trial/lookup" in seen["url"]
assert p.headers["x-ratelimit-remaining"] == "99"
assert not UpcItemDb(_client(lambda r: httpx.Response(200, json={"code": "OK", "total": 0, "items": []}))).lookup_gtin(G1).found
def test_openfoodfacts_contract_and_user_agent():
seen = {}
def handler(request):
seen["ua"] = request.headers.get("user-agent")
return httpx.Response(200, json={"status": 1, "product": {"product_name": "Biscoito", "brands": "Marca, Outra", "quantity": "130 g"}})
off = OpenFoodFacts(httpx.Client(transport=httpx.MockTransport(handler), headers={"User-Agent": "GTIFiscalHub/1.0 (+contact)"}))
p = off.lookup_gtin(G1)
assert p.found and p.brand == "Marca" and "GTIFiscalHub" in seen["ua"]
assert not OpenFoodFacts(_client(lambda r: httpx.Response(200, json={"status": 0, "status_verbose": "product not found"}))).lookup_gtin(G1).found
def test_cosmos_contract_and_errors():
body = {"description": "COCA COLA 350ML", "brand": {"name": "Coca"}, "ncm": {"code": "2202.10.00"}, "cest": {"code": "03.007.00"}}
p = Cosmos(_client(lambda r: httpx.Response(200, json=body)), "tok").lookup_gtin(G1)
assert p.ncm == "22021000" and p.cest == "0300700" and p.brand == "Coca"
assert not Cosmos(_client(lambda r: httpx.Response(404, json={})), "tok").lookup_gtin(G1).found
for status, kind in ((429, "RATE_LIMITED"), (401, "AUTH_ERROR"), (503, "SERVER_ERROR")):
with pytest.raises(ProviderError) as e:
Cosmos(_client(lambda r, s=status: httpx.Response(s)), "tok").lookup_gtin(G1)
assert e.value.kind == kind
def boom(request): raise httpx.ReadTimeout("t")
with pytest.raises(ProviderError) as e:
Cosmos(_client(boom), "tok").lookup_gtin(G1)
assert e.value.kind == "TIMEOUT"
def test_oscbr_optional_and_contract():
calls = []
assert not Oscbr(_client(lambda r: pytest.fail("no call expected"))).configured
with pytest.raises(ProviderError):
Oscbr(_client(lambda r: pytest.fail("no call expected"))).lookup_gtin(G1)
def handler(request):
calls.append(str(request.url))
if request.url.path == "/oauth/token":
assert request.headers["authorization"].startswith("Basic ")
return httpx.Response(200, json={"token": "tkn"})
assert request.headers["authorization"] == "Bearer tkn"
return httpx.Response(200, json={"ean": G1, "nome": "COLA 350ML", "marca": "X", "ncm": "2202.10.00", "cest": "03.007.00"})
p = Oscbr(_client(handler), None, "u", "p")
first, second = p.lookup_gtin(G1), p.lookup_gtin(G1)
assert first.ncm == "22021000" and first.cest == "0300700" and second.found
assert sum(1 for c in calls if "oauth" in c) == 1 # token reused for ~55 min
bad = Oscbr(_client(lambda r: httpx.Response(401)), None, "u", "p")
with pytest.raises(ProviderError) as e:
bad.lookup_gtin(G1)
assert e.value.kind == "AUTH_ERROR"
def test_kodebar_identity_only_optional_and_contract():
assert not Kodebar(_client(lambda r: pytest.fail("no call expected"))).configured
def handler(request):
assert request.headers["x-api-key"] == "k" and request.url.params["gtin"] == G1
return httpx.Response(200, json={"gtin": G1, "nome": "COCA COLA 350ML", "marca": "Coca", "thumbnail": "u", "barcode_url": "b", "qr_code_url": "q"})
p = Kodebar(_client(handler), "k").lookup_gtin(G1)
assert p.found and p.title == "COCA COLA 350ML" and p.ncm is None and p.cest is None # identity only, never NCM
assert not Kodebar(_client(lambda r: httpx.Response(200, json={})), "k").lookup_gtin(G1).found
with pytest.raises(ProviderError) as e:
Kodebar(_client(lambda r: httpx.Response(429)), "k").lookup_gtin(G1)
assert e.value.kind == "RATE_LIMITED"
def test_backfill_respects_operational_reserve(env):
fake = Fake("cosmos", COSMOS(), True, daily_limit=10)
gw = ProviderGateway(env, [fake])
outs = [gw.lookup(fake, gtin(f"78970000{i:04d}"), "BACKFILL", 20).state for i in range(10)]
assert outs.count("SKIPPED") == 2 and fake.calls == 8 # 20% of 10 kept for new products
assert gw.lookup(fake, gtin("789700009999"), "OPERATIONAL").state in ("FOUND", "NOT_FOUND")
def test_technical_placeholder_ncm_is_treated_as_not_classified_but_real_values_are_not(env):
p = kb.upsert_product(env, "0" + G1, "COCA-COLA ORIGINAL LATA 350 ML")
for fam in ("PROVIDER:a", "PROVIDER:b"):
kb.add_evidence(env, p, "ncm", "22021000", "x", family=fam)
env.commit()
for value, expected in (("00000000", "AUTO_FILL_SAFE"), ("0000.00.00", "AUTO_FILL_SAFE"), ("22021000", "ALREADY_SET"), ("99999999", "ALREADY_SET"), ("0", "ALREADY_SET"), ("", "AUTO_FILL_SAFE")):
r = run(env, [], G1, "COCA-COLA ORIGINAL LATA 350 ML", current={"CLASSIFICACAO_FISCAL": value})
assert r["fields"]["CLASSIFICACAO_FISCAL"]["status"] == expected, value
r = run(env, [], G1, "COCA-COLA ORIGINAL LATA 350 ML", current={"CLASSIFICACAO_FISCAL": "00000000"})
assert r["fields"]["IPI"]["value"] == "0.0" # CEST/IPI follow the NCM that is about to replace the filler
@@ -0,0 +1,61 @@
import json
import pytest
from central.app.ai.questionnaire import build_questionnaire, group_questions, validate_answers
def test_questionnaire_groups_by_scope_and_preserves_effects():
rows = [
{"QUESTION_ID": "Q1", "DATABASE": "ACME", "SCOPE": "COMPANY", "GROUP": "", "KEY": "entry_scope",
"VALID_OPTIONS": ["INTRA", "INTER", "MIXED_OR_DYNAMIC"], "PRODUCTS_AFFECTED": 2, "CELLS": 3,
"QUESTION": "Escopo?", "EXAMPLES": "P1; P2"},
{"QUESTION_ID": "Q2", "DATABASE": "ACME", "SCOPE": "GROUP", "GROUP": "G1", "KEY": "product_role",
"VALID_OPTIONS": ["RESALE"], "PRODUCTS_AFFECTED": 1, "CELLS": 1, "QUESTION": "Papel?"},
]
result = build_questionnaire(rows)
assert [item["question_id"] for item in result] == ["Q1", "Q2"]
assert result[0]["affected"] == {"products": 2, "cells": 3}
assert "MIXED_OR_DYNAMIC" in result[0]["options"]
assert result[1]["scope_key"] == "G1"
def test_answers_are_validated_without_persisting_and_mixed_scope_is_explicit():
questions = build_questionnaire([{
"QUESTION_ID": "Q1", "DATABASE": "ACME", "SCOPE": "COMPANY", "GROUP": "", "KEY": "entry_scope",
"VALID_OPTIONS": ["INTRA", "INTER", "MIXED_OR_DYNAMIC"], "QUESTION": "Escopo?",
}])
answers = validate_answers(questions, {"Q1": "MIXED_OR_DYNAMIC"})
assert answers["entries"] == [{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC", "reason": None, "company_key": "ACME"}]
assert answers["effects"][0]["behavior"] == "no_static_cfop_entrada"
with pytest.raises(ValueError, match="nothing was persisted"):
validate_answers(questions, {"Q1": "OUTSIDE"})
def test_store_ipi_profile_requires_explicit_choice_and_only_plans_rollback():
questions = build_questionnaire([{
"QUESTION_ID": "QIPI-ACME", "DATABASE": "ACME", "SCOPE": "COMPANY", "GROUP": "", "KEY": "store_ipi_profile",
"VALID_OPTIONS": ["POPULATE_REFERENCE_RATE", "LEAVE_NOT_APPLICABLE"], "QUESTION": "IPI?",
"PRODUCTS_AFFECTED": 4, "CELLS": 4,
}])
result = validate_answers(questions, {"QIPI-ACME": "LEAVE_NOT_APPLICABLE"})
assert result["entries"][0]["value"] == "LEAVE_NOT_APPLICABLE"
assert result["rollback_plan"] == [{"database": "ACME", "field": "IPI", "affected_cells": 4}]
assert json.dumps(result)
def test_equivalent_questions_are_grouped_with_aggregated_impact():
questions = build_questionnaire([
{"QUESTION_ID": "Q1", "DATABASE": "ACME", "SCOPE": "GROUP", "GROUP": "REFRIGERANTES", "KEY": "product_role", "VALID_OPTIONS": ["RESALE"], "CELLS": 10, "PRODUCTS_AFFECTED": 3, "QUESTION": "Qual o papel?"},
{"QUESTION_ID": "Q2", "DATABASE": "ACME", "SCOPE": "GROUP", "GROUP": "REFRIGERANTES", "KEY": "product_role", "VALID_OPTIONS": ["RESALE"], "CELLS": 20, "PRODUCTS_AFFECTED": 5, "QUESTION": "Qual o papel?"},
])
groups = group_questions(questions)
assert len(groups) == 1 and groups[0]["question_ids"] == ["Q1", "Q2"]
assert groups[0]["affected"] == {"products": 8, "cells": 30}
@@ -0,0 +1,80 @@
"""Missão 5 / Fase 1.2: 'Tabelas oficiais' admin screen - list, diff, approve/reject (CSRF, audited)."""
from sqlalchemy import select
from central.app.models import AuditEvent, Dataset
from central.app.reference_registry import register_draft
from central.tests.test_control_panel_pages import _panel_client
from central.tests.test_rtc_security import _become, _post
def _seed_draft(client, **kw):
with client._session_factory() as s:
d = register_draft(s, kind=kw.get("kind", "NCM"), provider=kw.get("provider", "Siscomex"), source_url="https://x",
sha256=kw.get("sha256", "a" * 64), version_label=kw.get("version_label", "v1"), record_count=kw.get("record_count", 100),
parse_errors=kw.get("parse_errors") or {})
return d.id
def test_page_requires_admin():
client, agent_id, _ = _panel_client()
_become(client, "PADRAO")
resp = client.get("/admin/reference-datasets", follow_redirects=False)
assert resp.status_code == 403
def test_admin_sees_the_draft_and_its_diff():
client, agent_id, _ = _panel_client()
_seed_draft(client)
_become(client, "ADMIN")
page = client.get("/admin/reference-datasets").text
assert "Tabelas oficiais" in page and "NCM" in page and "DRAFT" in page
assert "Primeira vers" in page # first version of this kind, no vigente to diff against
def test_approve_promotes_and_is_audited():
client, agent_id, _ = _panel_client()
dataset_id = _seed_draft(client)
_become(client, "ADMIN")
resp = _post(client, f"/admin/reference-datasets/{dataset_id}/decide", action="approve")
assert resp.status_code == 303
with client._session_factory() as s:
assert s.get(Dataset, dataset_id).status == "VIGENTE"
acts = [e.action for e in s.scalars(select(AuditEvent))]
assert "REFERENCE_DATASET_APPROVE" in acts
def test_reject_never_promotes():
client, agent_id, _ = _panel_client()
dataset_id = _seed_draft(client)
_become(client, "ADMIN")
assert _post(client, f"/admin/reference-datasets/{dataset_id}/decide", action="reject").status_code == 303
with client._session_factory() as s:
assert s.get(Dataset, dataset_id).status == "REJECTED"
def test_approve_with_parse_errors_is_refused_with_422():
client, agent_id, _ = _panel_client()
dataset_id = _seed_draft(client, parse_errors={"MINIMUM_ENTRIES": "too few"})
_become(client, "ADMIN")
resp = _post(client, f"/admin/reference-datasets/{dataset_id}/decide", action="approve")
assert resp.status_code == 422
with client._session_factory() as s:
assert s.get(Dataset, dataset_id).status == "DRAFT"
def test_non_admin_cannot_decide_and_it_is_audited_as_a_denial():
client, agent_id, _ = _panel_client()
dataset_id = _seed_draft(client)
_become(client, "PADRAO")
resp = _post(client, f"/admin/reference-datasets/{dataset_id}/decide", action="approve")
assert resp.status_code == 403
with client._session_factory() as s:
assert s.get(Dataset, dataset_id).status == "DRAFT"
def test_wrong_csrf_is_refused():
client, agent_id, _ = _panel_client()
dataset_id = _seed_draft(client)
_become(client, "ADMIN")
resp = client.post(f"/admin/reference-datasets/{dataset_id}/decide", data={"csrf_token": "bad", "action": "approve"}, follow_redirects=False)
assert resp.status_code in (400, 403)
@@ -0,0 +1,53 @@
from datetime import datetime, timezone
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.datasets.service import DatasetService
from central.app.main import create_app
from central.app.models import AnpProductCode, Base, CfopEntry, Dataset, FcpRate, NcmUtribEntry
def _client_with_reference_data():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
with Session(engine) as session:
dataset = Dataset(kind="CFOP", provider="p", sha256="a" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add(dataset); session.flush()
session.add(CfopEntry(dataset_id=dataset.id, code="1101", title="Compra", description="desc"))
d2 = Dataset(kind="NCM_UTRIB", provider="p", sha256="b" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add(d2); session.flush()
session.add(NcmUtribEntry(dataset_id=d2.id, ncm="01012100", utrib_abbreviation="UN", utrib_description="Unidade"))
d3 = Dataset(kind="ANP", provider="p", sha256="c" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add(d3); session.flush()
session.add(AnpProductCode(dataset_id=d3.id, cprod_anp="110203073"))
d4 = Dataset(kind="FCP", provider="p", sha256="d" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add(d4); session.flush()
session.add(FcpRate(dataset_id=d4.id, uf_code="29", uf="BA", uf_name="BAHIA", aliq_1="Fixo:2.00", observacao="obs"))
session.commit()
app = create_app(session_factory=lambda: Session(engine), bootstrap_token="test-token")
return TestClient(app)
def test_reference_endpoints_found_and_not_found():
client = _client_with_reference_data()
headers = {"Authorization": "Bearer test-token"}
assert client.get("/v1/reference/cfop/1101", headers=headers).json()["found"] is True
assert client.get("/v1/reference/cfop/9999", headers=headers).json()["found"] is False
assert client.get("/v1/reference/ncm_utrib/01012100", headers=headers).json()["found"] is True
assert client.get("/v1/reference/ncm_utrib/99999999", headers=headers).json()["found"] is False
assert client.get("/v1/reference/anp/110203073", headers=headers).json()["found"] is True
assert client.get("/v1/reference/anp/000000000", headers=headers).json()["found"] is False
fcp = client.get("/v1/reference/fcp/BA", headers=headers).json()
assert fcp["found"] is True
assert fcp["aliq_1"] == "Fixo:2.00"
assert client.get("/v1/reference/fcp/ZZ", headers=headers).json()["found"] is False
@@ -0,0 +1,91 @@
"""Missão 5 / Fase 1: Reference Data Registry - DRAFT-first ingestion, admin approval, diff summary.
Never auto-activates (unlike the 9 legacy dataset kinds in central/app/datasets/service.py, which
this module never touches)."""
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from central.app.models import Base, Dataset
from central.app.reference_registry import RegistryError, approve_draft, diff_against_vigente, list_registry, register_draft, reject_draft
def engine():
e = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(e)
return e
def test_register_draft_never_auto_activates():
with Session(engine()) as s:
d = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=15156)
assert d.status == "DRAFT"
assert s.scalar(select(Dataset).where(Dataset.kind == "NCM", Dataset.status == "VIGENTE")) is None
def test_resubmitting_the_same_sha256_returns_the_existing_row_not_a_duplicate():
with Session(engine()) as s:
d1 = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=15156)
d2 = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1-again", record_count=99999)
assert d1.id == d2.id and d2.version_label == "v1" # untouched, not overwritten by the resubmission
assert len(list(s.scalars(select(Dataset)))) == 1
def test_approve_promotes_to_vigente_and_supersedes_the_previous_one():
with Session(engine()) as s:
first = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=100)
approve_draft(s, first.id, admin_name="admin1")
assert s.get(Dataset, first.id).status == "VIGENTE"
second = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="b" * 64, version_label="v2", record_count=101)
diff = diff_against_vigente(s, second)
assert diff["current_vigente"]["version_label"] == "v1" and diff["record_count_delta"] == 1 and not diff["is_first_version"]
approve_draft(s, second.id, admin_name="admin2")
assert s.get(Dataset, first.id).status == "OBSOLETO"
assert s.get(Dataset, second.id).status == "VIGENTE"
assert s.get(Dataset, second.id).approved_by == "admin2"
def test_approve_refuses_a_dataset_with_parse_errors():
with Session(engine()) as s:
d = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=1, parse_errors={"MINIMUM_ENTRIES": "too few"})
try:
approve_draft(s, d.id, admin_name="admin1")
assert False, "expected RegistryError"
except RegistryError as e:
assert "parse errors" in str(e)
assert s.get(Dataset, d.id).status == "DRAFT"
def test_approve_refuses_a_non_draft_dataset():
with Session(engine()) as s:
d = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=1)
approve_draft(s, d.id, admin_name="admin1")
try:
approve_draft(s, d.id, admin_name="admin2")
assert False, "expected RegistryError"
except RegistryError as e:
assert "only a DRAFT" in str(e)
def test_reject_never_touches_the_current_vigente():
with Session(engine()) as s:
first = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=1)
approve_draft(s, first.id, admin_name="admin1")
bad = register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="b" * 64, version_label="v2-bad", record_count=1)
reject_draft(s, bad.id, admin_name="admin2")
assert s.get(Dataset, bad.id).status == "REJECTED"
assert s.get(Dataset, first.id).status == "VIGENTE"
def test_diff_against_vigente_for_the_first_ever_version_of_a_kind():
with Session(engine()) as s:
d = register_draft(s, kind="CEST", provider="Confaz", source_url="https://x", sha256="c" * 64, version_label="v1", record_count=1051)
diff = diff_against_vigente(s, d)
assert diff["is_first_version"] is True and diff["current_vigente"] is None
def test_list_registry_orders_by_kind_then_newest_first():
with Session(engine()) as s:
register_draft(s, kind="NCM", provider="Siscomex", source_url="https://x", sha256="a" * 64, version_label="v1", record_count=1)
register_draft(s, kind="CEST", provider="Confaz", source_url="https://x", sha256="c" * 64, version_label="v1", record_count=1)
rows = list_registry(s)
assert {r.kind for r in rows} == {"NCM", "CEST"}
@@ -0,0 +1,77 @@
import importlib.util
import sys
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.auth import authenticate, token_hash
from central.app.models import Base
SCRIPT = Path(__file__).parents[1] / "scripts" / "register_agent.py"
def load_module():
spec = importlib.util.spec_from_file_location("register_agent", SCRIPT)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
register_agent_module = load_module()
def test_register_agent_creates_tenant_and_hashed_token_only():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
info, token = register_agent_module.register_agent(session, "Restaurante X", "loja-01")
found = authenticate(session, token)
assert found is not None
assert found.id == info["id"]
assert found.token_hash == token_hash(token)
assert found.token_hash != token # only the hash is ever persisted
# Mirrors the real CLI's main(): reading the returned info AFTER the
# session (and its `with` block) has closed. This is exactly the scenario
# that broke when register_agent() returned live ORM objects instead of
# plain values (DetachedInstanceError) — must keep working unattached.
assert info["enabled"] is True
assert isinstance(info["tenant_id"], int)
def test_register_agent_reuses_existing_tenant():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
info1, _ = register_agent_module.register_agent(session, "Restaurante X", "loja-01")
info2, _ = register_agent_module.register_agent(session, "Restaurante X", "loja-02")
assert info1["tenant_id"] == info2["tenant_id"]
assert info1["id"] != info2["id"]
def test_register_agent_generates_unique_tokens():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
_, token1 = register_agent_module.register_agent(session, "A", "a1")
_, token2 = register_agent_module.register_agent(session, "A", "a2")
assert token1 != token2
def test_disabled_agent_cannot_authenticate():
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.auth import authenticate, token_hash
from central.app.models import Agent, Base, Tenant
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as s:
tenant = Tenant(name="t"); s.add(tenant); s.flush()
agent = Agent(tenant_id=tenant.id, name="a", token_hash=token_hash("secret-token")); s.add(agent); s.commit()
assert authenticate(s, "secret-token").id == agent.id
agent.enabled = False; s.commit()
assert authenticate(s, "secret-token") is None
assert authenticate(s, "wrong") is None
+116
View File
@@ -0,0 +1,116 @@
"""Semantic candidate retrieval V2: hierarchical official corpus + KB-neighbor bridge + hybrid ranking."""
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.models import Base, Dataset, KbKnowledge, KbObservation, KbProduct, NcmEntry, Tenant
def _engine():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return engine
def _seed(session):
ncm = Dataset(kind="NCM", provider="Siscomex", sha256="n" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=7)
session.add(ncm); session.flush()
rows = [
("22", "BEBIDAS, LIQUIDOS ALCOOLICOS E VINAGRES"),
("2202", "AGUAS, INCLUIDAS AS AGUAS GASEIFICADAS E MINERAIS"),
("2202.10", "AGUAS MINERAIS E GASEIFICADAS"),
("22021000", "AGUAS MINERAIS NAO GASEIFICADAS"),
("61", "VESTUARIO E SEUS ACESSORIOS, DE MALHA"),
("6105", "CAMISAS DE MALHA, DE USO MASCULINO"),
("61051000", "DE ALGODAO"),
]
for code, description in rows:
session.add(NcmEntry(dataset_id=ncm.id, code=code.replace(".", ""), description=description, valid_from="", valid_to=""))
session.flush()
# two tenants already selling mineral water with a validated NCM
tenant_a, tenant_b = Tenant(name="A"), Tenant(name="B")
session.add_all([tenant_a, tenant_b]); session.flush()
p1 = KbProduct(fingerprint="f1", normalized_description="AGUA MINERAL SEM GAS 500ML")
p2 = KbProduct(fingerprint="f2", normalized_description="AGUA MINERAL GARRAFA 1L")
p3 = KbProduct(fingerprint="f3", normalized_description="CAMISA MASCULINA MALHA ALGODAO")
session.add_all([p1, p2, p3]); session.flush()
for product, ncm_value, tenant in ((p1, "22021000", tenant_a), (p2, "22021000", tenant_b), (p3, "61051000", tenant_a)):
session.add(KbKnowledge(kb_product_id=product.id, knowledge_key="validated_ncm", value=ncm_value, source="OFFICIAL_DATASET", confidence=1.0))
session.add(KbObservation(kb_product_id=p1.id, tenant_id=tenant_a.id, company_key="A", erp_code="1", database_label="A", observed_fields={}))
session.add(KbObservation(kb_product_id=p2.id, tenant_id=tenant_b.id, company_key="B", erp_code="2", database_label="B", observed_fields={}))
session.add(KbObservation(kb_product_id=p3.id, tenant_id=tenant_a.id, company_key="A", erp_code="3", database_label="A", observed_fields={}))
session.commit()
return ncm.sha256
def test_enriched_corpus_carries_ancestor_context():
from central.app.ai.retrieval import EnrichedNcmCorpus
engine = _engine()
with Session(engine) as session:
_seed(session)
corpus = EnrichedNcmCorpus.build(session)
doc = corpus.document("22021000")
# chapter + position + subposition context enriches the very short legal text (stemmed)
assert "BEBIDA" in doc and "AGUA" in doc
assert "VESTUARIO" not in doc
def test_hierarchical_retrieval_prefers_right_positions():
from central.app.ai.retrieval import EnrichedNcmCorpus, hierarchical_candidates
engine = _engine()
with Session(engine) as session:
_seed(session)
corpus = EnrichedNcmCorpus.build(session)
codes = [code for code, _, _ in hierarchical_candidates(corpus, "AGUA MINERAL", k=2)]
assert codes[0] == "22021000"
def test_kb_neighbor_bridge_maps_commercial_to_ncm():
from central.app.ai.retrieval import KbNeighborIndex, kb_neighbor_candidates
engine = _engine()
with Session(engine) as session:
_seed(session)
index = KbNeighborIndex.build(session)
neighbors = kb_neighbor_candidates(index, "AGUA MINERAL PET 500ML", k=5)
assert neighbors and neighbors[0][0] == "22021000"
# cross-tenant agreement is reported, never auto-majority
assert neighbors[0][3] >= 2
def test_hybrid_merges_official_and_kb_rankings():
from central.app.ai.retrieval import EnrichedNcmCorpus, KbNeighborIndex, hybrid_candidates
engine = _engine()
with Session(engine) as session:
_seed(session)
corpus = EnrichedNcmCorpus.build(session)
index = KbNeighborIndex.build(session)
hybrid = hybrid_candidates(corpus, index, "CAMISA MASCULINA ALGODAO", k=2)
assert hybrid[0][0] == "61051000"
scores = hybrid_candidates(corpus, index, "AGUA MINERAL SEM GAS", k=2)
assert scores[0][0] == "22021000"
def test_lexicon_expands_abbreviations_supported_by_kb():
from central.app.ai.retrieval import derive_abbreviation_lexicon, expand_stems
corpus = {
"REFRIGERANTE COCA 2L": "22021000",
"REFRI GUARANA 1L": "22021000",
"REFRI LARANJA 350ML": "22021000",
"CAMISA POLO ALGODAO": "61051000",
"CAM POLO MASC": "61051000",
"CAM POLO AZUL": "61051000",
"BOTA COURO MASC": "64039900",
}
lexicon = derive_abbreviation_lexicon(corpus, min_support=2)
assert lexicon.get("REFRI") == "REFRIGERANTE"
assert lexicon.get("CAM") == "CAMISA"
expanded = expand_stems({"REFRI", "COLA"}, lexicon)
assert "REFRIGERANTE" in expanded
def test_commercial_tokens_strip_units_and_sizes():
from central.app.ai.retrieval import commercial_tokens
tokens = commercial_tokens("CREME TRATAMENTO NOVEX 1KG CX 12 COR AZUL TAM M REF 453")
assert "NOVEX" in tokens and "CREME" in tokens and "TRATAMENTO" in tokens
assert "1KG" not in tokens and "CX" not in tokens and "COR" not in tokens and "TAM" not in tokens
assert "AZUL" not in tokens and "REF" not in tokens and "453" not in tokens
assert "12" not in tokens
+31
View File
@@ -0,0 +1,31 @@
def test_retrieval_v3_gtin_evidence_is_candidate_only():
from central.app.ai.retrieval_v3 import retrieval_v3_candidates
class Corpus:
pass
corpus = Corpus()
corpus.documents = {}
corpus.descriptions = {}
corpus.positions = {}
corpus.children = {}
class Index:
entries = []
# Empty V2 corpus cannot manufacture an NCM, proving no candidate is invented.
assert retrieval_v3_candidates(corpus, Index(), "produto", {"gtin_ncm": "22011000"}) == []
def test_prepare_candidate_set_is_fingerprinted_and_non_decisional():
from central.app.ai.retrieval_v3 import prepare_candidate_set
work = type("WorkItem", (), {"task_type": "INVALID_NCM", "dedup_key": "x", "payload": {"description": "Agua mineral"}})()
prepared = prepare_candidate_set(work, [("22011000", "AGUAS MINERAIS", 0.9, 0.8, 0.1, 2)], dataset_versions={"ncm": "v1"})
assert prepared["top20_candidate_ncms"][0]["ncm"] == "22011000"
assert prepared["input_fingerprint"]
assert "selected_ncm" not in prepared
def test_product_role_context_is_compact_and_evidence_only():
from central.app.ai.retrieval_v3 import compact_product_role_context
result = compact_product_role_context([{"description": "Coca Cola"}, {"description": "coca-cola"}, {"description": "Pepsi"}], limit=2)
assert len(result["examples"]) == 2 and result["decision"] is None and result["evidence_only"] is True
@@ -0,0 +1,62 @@
from datetime import datetime, timezone
from functools import lru_cache
from pathlib import Path
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.datasets import bootstrap_v1_extra as parsers
from central.app.datasets.service import DatasetService
from central.app.models import Base, RtcCstIbsCbsEntry
ROOT = Path(__file__).parents[2] / "sources" / "raw" / "bootstrap_v1"
CCLASS = ROOT / "cClassTrib 2026-06-22.xlsx"
CBS = ROOT / "IT 2026.002 v.1.00 - Aliquotas da CBS.xlsx"
pytestmark = pytest.mark.skipif(not (CCLASS.exists() and CBS.exists()), reason="RTC bootstrap files not present locally")
@lru_cache(maxsize=None)
def _cclass():
return parsers.parse_rtc_cclasstrib(CCLASS.read_bytes())
def _session():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return Session(engine)
def test_cclasstrib_keys_unique_and_rich_fields_kept():
p = _cclass()
codes = [e["cclasstrib"] for e in p.entries]
assert len(codes) == len(set(codes)) >= 100
assert all(len(c) == 6 for c in codes)
assert p.entries[0]["indicators"] and p.entries[0]["d_ini_vig"]
assert {e["cst"] for e in p.entries} <= {e["cst"] for e in p.extra_entries}
def test_cbs_rates_2026_numeric_and_2027_pending():
p = parsers.parse_rtc_cbs_rates(CBS.read_bytes())
by_year = {e["year"]: e for e in p.entries}
assert by_year[2026]["rate"] == pytest.approx(0.009)
assert by_year[2027]["rate"] is None and by_year[2027]["rate_text"]
def test_import_idempotent_lookup_and_versions():
with _session() as s:
svc = DatasetService(s)
assert svc.dataset_versions()["rtc_cclasstrib"] is None
now = datetime.now(timezone.utc)
a = svc.import_rtc_cclasstrib(_cclass(), "file://x", now)
b = svc.import_rtc_cclasstrib(_cclass(), "file://x", now)
assert a.id == b.id and b.status == "ACTIVE" and b.record_count == len(_cclass().entries)
assert s.query(RtcCstIbsCbsEntry).count() == len(_cclass().extra_entries)
assert svc.rtc_cclasstrib(code="000001")
cbs_payload = parsers.parse_rtc_cbs_rates(CBS.read_bytes())
c = svc.import_rtc_cbs_rates(cbs_payload, "file://y", now)
assert svc.import_rtc_cbs_rates(cbs_payload, "file://y", now).id == c.id
assert svc.rtc_cbs_rate(2026).rate == pytest.approx(0.009)
v = svc.dataset_versions()
assert v["rtc_cclasstrib"] and v["rtc_cbs_rates"]
+150
View File
@@ -0,0 +1,150 @@
"""Security review of the centrally controlled cClassTrib write: who can change the mode, what the Agent can and cannot decide, tenant scoping, no secrets in audits."""
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.models import AuditEvent, Installation, RtcCandidate, RtcWriteAudit
from central.tests.test_control_panel_pages import _panel_client, _seed_rtc
from central.tests.test_zero_touch_hub import H, OPERATIONAL, _client, age_last_sync, finish, hb
def test_new_rtc_routes_require_an_operator_session():
client, agent_id, _ = _panel_client()
client.cookies.clear()
for method, path in (("get", f"/agents/{agent_id}/rtc"), ("post", f"/agents/{agent_id}/rtc-mode"), ("post", f"/agents/{agent_id}/rtc-candidates"), ("post", f"/agents/{agent_id}/rtc-rollback")):
response = getattr(client, method)(path, follow_redirects=False)
assert response.status_code in (303, 401, 403), (path, response.status_code)
assert "login" in response.headers.get("location", "login") or response.status_code in (401, 403)
def test_default_mode_is_disabled_and_the_agent_cannot_choose_it():
client, engine = _client()
hb(client)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_write_mode"] == "DISABLED"
# a forged result cannot change the mode, approve candidates or request rollbacks: none of those are read from the payload
forged = {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_write_mode": "ENABLED", "authorizations": {"rtc_write_mode": "ENABLED", "rtc_approved_codes": ["X"]},
"rtc_auto_apply": {"mode": "ENABLED", "hub_mode": "ENABLED", "preview": [{"product_code": "X", "proposed": "200003", "rule_id": "RTC-R3", "status": "APPROVED"}]}}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": forged}, headers=H)
with Session(engine) as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
assert s.scalars(select(RtcCandidate)).all() == [] # DISABLED stores no preview at all
def test_preview_from_the_agent_never_arrives_approved_and_never_approves_itself():
client, engine = _client()
hb(client)
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = "APPROVAL_REQUIRED"
s.commit()
item = {"product_code": "X1", "description": "D", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "b", "status": "APPROVED"}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": True}, "rtc_auto_apply": {"preview": [item]}})
with Session(engine) as s:
assert [c.status for c in s.scalars(select(RtcCandidate)).all()] == ["PENDING"]
age_last_sync(engine, 4000); hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"]["rtc_approved_codes"] == []
def test_a_rollback_result_cannot_touch_another_installations_audit():
client, engine = _client()
hb(client)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
foreign = RtcWriteAudit(tenant_id=inst.tenant_id, installation_id=inst.id + 999, agent_id=inst.agent_id, product_code="F", field="CCLASSTRIB", outcome="APPLIED", new_value="200003", action="APPLY", rollback_state="REQUESTED")
# the FK is not enforced by the SQLite test DB; the scoping is what is under test
s.add(foreign); s.commit(); foreign_id = foreign.id
done = {"outcome": "ROLLED_BACK", "product_code": "F", "field": "CCLASSTRIB", "old_value": "200003", "new_value": None, "action": "ROLLBACK", "rollback_of": foreign_id}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"rollbacks": [done]}})
with Session(engine) as s:
assert s.get(RtcWriteAudit, foreign_id).rollback_state == "REQUESTED" # untouched
def test_rollback_and_candidate_actions_are_scoped_to_the_operators_installation_and_audited():
client, agent_id, _ = _panel_client()
_seed_rtc(client, agent_id, n=3)
csrf = client.cookies.get("gti_csrf")
with client._session_factory() as s:
other = RtcWriteAudit(tenant_id=1, installation_id=987654, agent_id=agent_id, product_code="Z", field="CCLASSTRIB", outcome="APPLIED", new_value="200003", action="APPLY")
s.add(other); s.commit(); other_id = other.id
assert client.post(f"/agents/{agent_id}/rtc-rollback", data={"csrf_token": csrf, "audit_id": other_id, "confirm": "REVERTER"}).status_code == 409
client.post(f"/agents/{agent_id}/rtc-candidates?all=1", data={"csrf_token": csrf, "action": "approve"}, follow_redirects=False)
with client._session_factory() as s:
actions = [a.action for a in s.scalars(select(AuditEvent)).all()]
assert "RTC_CANDIDATES_APPROVE" in actions
def test_audit_rows_never_carry_credentials_or_tokens():
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "10", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY", "password": "p", "token": "t",
"evidence": {"reason": "r"}}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"applied": [applied]}})
with Session(engine) as s:
row = s.scalars(select(RtcWriteAudit)).one()
blob = repr([getattr(row, c.name) for c in RtcWriteAudit.__table__.columns])
assert "password" not in blob.lower() and "'t'" not in blob and "Bearer" not in blob
assert not any("pass" in c.name or "token" in c.name or "secret" in c.name for c in RtcWriteAudit.__table__.columns)
def test_agent_can_reread_only_its_own_current_authorization_and_it_follows_the_operator():
client, engine = _client()
hb(client)
assert client.get("/v1/agents/me/authorizations").status_code in (401, 403) # requires the Agent credential
assert client.get("/v1/agents/me/authorizations", headers=H).json()["rtc_write_mode"] == "DISABLED"
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = "ENABLED"
s.commit()
body = client.get("/v1/agents/me/authorizations", headers=H).json()
assert body["rtc_write_mode"] == "ENABLED" and set(body) == {"rtc_write_mode", "rtc_approved_codes", "rtc_rollbacks", "features", "cfop_ippt_write_mode"}
def _become(client, role):
"""Turn the logged-in test operator into a Gerenciador SSO collaborator with the given perfil (identity source of truth is the SSO)."""
from central.app.models import AdminUser
with client._session_factory() as s:
admin = s.scalars(select(AdminUser)).first()
admin.auth_source, admin.password_hash, admin.external_subject, admin.external_role = "GTI_GERENCIADOR", None, "sso-subject-1", role
s.commit()
def _post(client, path, **data):
return client.post(path, data={"csrf_token": client.cookies.get("gti_csrf"), **data}, follow_redirects=False)
def test_write_modes_are_restricted_to_gerenciador_admins_and_every_denial_is_audited():
client, agent_id, _ = _panel_client()
_become(client, "PADRAO")
for mode, extra in (("ENABLED", {"confirm": "PREENCHER"}), ("APPROVAL_REQUIRED", {})):
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode=mode, **extra).status_code == 403
assert _post(client, f"/agents/{agent_id}/rtc-candidates", action="approve", all="1").status_code == 403
assert _post(client, f"/agents/{agent_id}/rtc-candidates", action="apply_approved").status_code == 403
assert _post(client, f"/agents/{agent_id}/rtc-rollback", audit_id="1", confirm="REVERTER").status_code == 403
with client._session_factory() as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "DISABLED"
assert len([e for e in s.scalars(select(AuditEvent)) if e.action == "RTC_DENIED"]) == 5
# read-only modes stay available to a normal collaborator, and the page tells them why the rest is disabled
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="DRY_RUN").status_code == 303
page = client.get(f"/agents/{agent_id}/rtc").text
assert "Somente leitura para o seu perfil" in page and "disabled" in page
def test_a_normal_collaborator_cannot_switch_off_a_privileged_mode_either():
client, agent_id, _ = _panel_client()
with client._session_factory() as s:
s.scalars(select(Installation)).one().rtc_write_mode = "ENABLED"
s.commit()
_become(client, "PADRAO")
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="DISABLED").status_code == 403
with client._session_factory() as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "ENABLED"
def test_gerenciador_admin_can_change_modes_and_the_operator_is_recorded():
client, agent_id, _ = _panel_client()
_become(client, "ADMIN")
assert _post(client, f"/agents/{agent_id}/rtc-mode", mode="ENABLED", confirm="PREENCHER").status_code == 303
with client._session_factory() as s:
assert s.scalars(select(Installation)).one().rtc_write_mode == "ENABLED"
event = [e for e in s.scalars(select(AuditEvent)) if e.action == "RTC_WRITE_MODE"][-1]
assert event.context["to"] == "ENABLED" and event.context["role"] == "ADMIN" and event.context["operator"]
+235
View File
@@ -0,0 +1,235 @@
"""SSO from GTI Gerenciador into the Hub (central/app/sso.py, web.py)."""
import base64
import dataclasses
import time
import uuid
from datetime import datetime, timedelta, timezone
from urllib.parse import parse_qs, urlsplit
import jwt
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from fastapi.testclient import TestClient
from sqlalchemy import select
from central.app import config, sso
from central.app.human_auth import verify_password
from central.app.main import create_app
from central.app.models import AdminUser, AuditEvent, SsoUsedAssertion, WebSession
from central.tests.test_control_plane import seed, setup_db
START_URL = "https://gtigerenciador.gtigrupo.com.br/sso/fiscal"
PRIVATE = Ed25519PrivateKey.generate()
PUBLIC_B64 = base64.urlsafe_b64encode(PRIVATE.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)).decode().rstrip("=")
@pytest.fixture(autouse=True)
def sso_settings(monkeypatch):
monkeypatch.setattr(config, "settings", dataclasses.replace(config.settings, sso_public_key=PUBLIC_B64, sso_start_url=START_URL, web_cookie_secure=True, federated_session_seconds=3600))
@pytest.fixture
def hub():
factory = setup_db()
seed(factory) # creates the LOCAL break-glass admin "operator"
client = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None), base_url="https://testserver")
return client, factory
def assertion(state, *, sub="funcionario:7", key=PRIVATE, algorithm="EdDSA", aud=sso.AUDIENCE, iss=sso.ISSUER, iat=None, lifetime=60, jti=None, **extra):
now = int(iat if iat is not None else time.time())
claims = {"iss": iss, "aud": aud, "sub": sub, "iat": now, "nbf": now, "exp": now + lifetime, "jti": jti or str(uuid.uuid4()),
"nonce": state, "login": "maria", "name": "Maria Teste", "perfil": "PADRAO", **extra}
return jwt.encode(claims, key, algorithm=algorithm)
def start(client, next_path="/"):
response = client.get("/login", params={"next": next_path}, follow_redirects=False)
return response, response.cookies.get(sso.STATE_COOKIE) or client.cookies.get(sso.STATE_COOKIE)
def callback(client, token, next_path="/"):
return client.post("/auth/sso/callback", data={"assertion": token, "next": next_path}, follow_redirects=False)
def audits(factory, action):
with factory() as session:
return session.scalars(select(AuditEvent).where(AuditEvent.action == action)).all()
def test_login_starts_sso_at_the_panel_with_a_browser_bound_state(hub):
client, _ = hub
response, state = start(client, "/agents")
assert response.status_code == 303
target = urlsplit(response.headers["location"])
assert f"{target.scheme}://{target.netloc}{target.path}" == START_URL
query = parse_qs(target.query)
assert query["next"] == ["/agents"] and query["state"] == [state] and len(state) >= 16
cookie = next(h for h in response.headers.get_list("set-cookie") if h.startswith(sso.STATE_COOKIE))
assert "HttpOnly" in cookie and "Secure" in cookie and "SameSite=none" in cookie and "Path=/auth/sso" in cookie
def test_active_employee_enters_without_password_and_is_provisioned(hub):
client, factory = hub
_, state = start(client, "/agents")
response = callback(client, assertion(state), "/agents")
assert response.status_code == 303 and response.headers["location"] == "/agents"
cookies = response.headers.get_list("set-cookie")
session_cookie = next(h for h in cookies if h.startswith(config.settings.web_cookie_name))
assert "HttpOnly" in session_cookie and "Secure" in session_cookie and "SameSite=lax" in session_cookie
csrf_cookie = next(h for h in cookies if h.startswith("gti_csrf"))
assert "Secure" in csrf_cookie and "HttpOnly" not in csrf_cookie
page = client.get("/agents")
assert page.status_code == 200 and "Maria Teste" in page.text and 'name="password"' not in page.text
with factory() as session:
user = session.scalar(select(AdminUser).where(AdminUser.external_subject == "funcionario:7"))
assert user.auth_source == "GTI_GERENCIADOR" and user.password_hash is None and user.external_role == "PADRAO"
web_session = session.scalar(select(WebSession).where(WebSession.admin_user_id == user.id))
lifetime = web_session.expires_at.replace(tzinfo=timezone.utc) - datetime.now(timezone.utc)
assert timedelta(minutes=55) < lifetime <= timedelta(hours=1)
success = audits(factory, "SSO_LOGIN_SUCCESS")
assert len(success) == 1 and success[0].target_id == "funcionario:7" and "ip" in success[0].context
def test_second_login_reuses_the_provisioned_user_and_refreshes_its_name(hub):
client, factory = hub
_, state = start(client)
callback(client, assertion(state))
_, state = start(client)
callback(client, assertion(state, name="Maria T. Souza", perfil="ADMIN"))
with factory() as session:
users = session.scalars(select(AdminUser).where(AdminUser.auth_source == "GTI_GERENCIADOR")).all()
assert len(users) == 1 and users[0].display_name == "Maria T. Souza" and users[0].external_role == "ADMIN"
@pytest.mark.parametrize("builder,reason", [
(lambda s: assertion(s, iat=time.time() - 600), "EXPIRED"),
(lambda s: assertion(s, key=Ed25519PrivateKey.generate()), "INVALID_SIGNATURE"),
(lambda s: assertion(s, aud="outro-sistema"), "WRONG_AUDIENCE"),
(lambda s: assertion(s, iss="impostor"), "WRONG_ISSUER"),
(lambda s: jwt.encode({"iss": sso.ISSUER, "aud": sso.AUDIENCE, "sub": "funcionario:7", "nonce": s}, "segredo", algorithm="HS256"), "INVALID_ALGORITHM"),
(lambda s: assertion(s, lifetime=3600), "LIFETIME_TOO_LONG"),
(lambda s: assertion(s, sub="admin"), "INVALID_SUBJECT"),
(lambda s: assertion("outro-navegador-0123456789"), "STATE_MISMATCH"),
(lambda s: "nao-e-um-jwt", "MALFORMED"),
])
def test_invalid_assertions_are_rejected_and_audited(hub, builder, reason):
client, factory = hub
_, state = start(client)
token = builder(state)
response = callback(client, token)
assert response.status_code == 401 and config.settings.web_cookie_name not in response.cookies
rejected = audits(factory, "SSO_LOGIN_REJECTED")
assert [event.context["reason"] for event in rejected] == [reason]
assert token not in str(rejected[0].context)
with factory() as session:
assert session.scalar(select(AdminUser).where(AdminUser.auth_source == "GTI_GERENCIADOR")) is None
def test_assertion_without_the_state_cookie_is_rejected(hub):
client, factory = hub
_, state = start(client)
client.cookies.delete(sso.STATE_COOKIE)
assert callback(client, assertion(state)).status_code == 401
assert audits(factory, "SSO_LOGIN_REJECTED")[0].context["reason"] == "STATE_MISMATCH"
def test_replayed_assertion_is_rejected(hub):
client, factory = hub
_, state = start(client)
token = assertion(state)
assert callback(client, token).status_code == 303
client.cookies.clear()
client.cookies.set(sso.STATE_COOKIE, state) # an attacker replaying with the matching state
assert callback(client, token).status_code == 401
assert audits(factory, "SSO_LOGIN_REJECTED")[0].context["reason"] == "REPLAY"
with factory() as session:
assert len(session.scalars(select(SsoUsedAssertion)).all()) == 1
@pytest.mark.parametrize("malicious", ["//evil.com/x", "https://evil.com", "javascript:alert(1)", "/\\evil.com", "data:text/html,x", "agents"])
def test_next_never_leaves_the_hub(hub, malicious):
client, _ = hub
_, state = start(client)
response = callback(client, assertion(state), malicious)
assert response.status_code == 303 and response.headers["location"] == "/"
login = client.get("/login", params={"next": malicious}, follow_redirects=False)
assert parse_qs(urlsplit(login.headers["location"]).query)["next"] == ["/"]
def test_user_disabled_in_the_hub_cannot_sso(hub):
client, factory = hub
_, state = start(client)
callback(client, assertion(state))
with factory() as session:
user = session.scalar(select(AdminUser).where(AdminUser.external_subject == "funcionario:7"))
user.is_active = False; session.commit()
client.cookies.clear()
_, state = start(client)
assert callback(client, assertion(state)).status_code == 401
assert audits(factory, "SSO_LOGIN_REJECTED")[0].context["reason"] == "DISABLED_IN_FISCAL"
assert client.get("/", follow_redirects=False).status_code == 303
def test_federated_account_has_no_password_login(hub):
client, _ = hub
_, state = start(client)
callback(client, assertion(state))
client.cookies.clear()
response = client.post("/login", data={"username": "gerenciador:7", "password": "qualquer-coisa-longa"}, follow_redirects=False)
assert response.status_code == 401
assert verify_password("x", None) is False
def test_break_glass_local_admin_still_works_and_is_audited(hub):
client, factory = hub
assert client.get("/login/local").status_code == 200
response = client.post("/login", data={"username": "operator", "password": "a-secure-password"}, follow_redirects=False)
assert response.status_code == 303
assert client.get("/").status_code == 200
assert len(audits(factory, "LOGIN_LOCAL_SUCCESS")) == 1
client.cookies.clear()
client.post("/login", data={"username": "operator", "password": "errada-mas-longa"})
assert len(audits(factory, "LOGIN_LOCAL_FAILURE")) == 1
def test_unauthenticated_page_goes_to_sso_and_keeps_the_destination(hub):
client, _ = hub
response = client.get("/blockers?status=open", follow_redirects=False)
assert response.status_code == 303
assert parse_qs(urlsplit(response.headers["location"]).query)["next"] == ["/blockers?status=open"]
def test_logout_ends_only_the_hub_session_and_csrf_is_enforced(hub):
client, factory = hub
_, state = start(client)
callback(client, assertion(state))
assert client.post("/logout", data={}).status_code == 403 # no CSRF token
assert client.get("/").status_code == 200
response = client.post("/logout", data={"csrf_token": client.cookies.get("gti_csrf")})
assert response.status_code == 200 and "Você saiu do GTI Fiscal" in response.text and "gtigerenciador.gtigrupo.com.br" in response.text
assert client.get("/", follow_redirects=False).status_code == 303
assert len(audits(factory, "LOGOUT")) == 1
_, state = start(client) # coming back through the panel signs in again, no password
assert callback(client, assertion(state)).status_code == 303
def test_expired_federated_session_sends_back_through_sso(hub):
client, factory = hub
_, state = start(client)
callback(client, assertion(state))
with factory() as session:
for row in session.scalars(select(WebSession)).all():
row.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1)
session.commit()
response = client.get("/jobs", follow_redirects=False)
assert response.status_code == 303 and response.headers["location"].startswith("/login?")
def test_without_sso_configuration_the_hub_keeps_the_local_login(hub, monkeypatch):
monkeypatch.setattr(config, "settings", dataclasses.replace(config.settings, sso_public_key=""))
client, factory = hub
assert client.get("/login").status_code == 200
assert callback(client, assertion("x" * 20)).status_code == 401
assert audits(factory, "SSO_LOGIN_REJECTED")[0].context["reason"] == "SSO_DISABLED"
+29
View File
@@ -0,0 +1,29 @@
from io import BytesIO
import openpyxl
def test_tipi_parser_requires_official_columns_and_reads_ncm_rows():
from central.app.datasets.tipi import parse_tipi_xlsx
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.append(["NCM", "EX", "DESCRIÇÃO", "ALÍQUOTA (%)"])
sheet.append(["0101.21.00", None, "Reprodutores", "0"])
raw = BytesIO(); workbook.save(raw)
payload = parse_tipi_xlsx(raw.getvalue())
assert payload.entries[0].ncm == "01012100"
assert payload.entries[0].rate == "0"
def test_tipi_parser_preserves_ex_in_the_ncm_key():
from central.app.datasets.tipi import parse_tipi_xlsx
workbook = openpyxl.Workbook(); sheet = workbook.active
sheet.append(["NCM", "EX", "DESCRIÇÃO", "ALÍQUOTA (%)"])
sheet.append(["0101.21.00", "01", "Com EX", "NT"])
raw = BytesIO(); workbook.save(raw)
entry = parse_tipi_xlsx(raw.getvalue()).entries[0]
assert entry.ex == "01"
assert entry.rate == "NT"
@@ -0,0 +1,240 @@
from datetime import date
from central.app.operational_state import classify_product_state, summarize_operational_states
from central.app.unified_fiscal_coverage import (blocker_category, canonical_cst, evaluate_product, incidence_kind, legal_effective_context, pis_cofins_applicability, rtc_enabled,
validate_cclasstrib, validate_cst)
CATALOG = {"classes": {"000001": "000", "200003": "200", "410004": "410", "999999": None, "555555": "555"}, "cst": {"000", "200", "410"}, "duplicates": 0}
ON = {"enabled": True, "catalog": CATALOG, "catalog_valid": True}
OFF = {"enabled": False, "catalog": CATALOG, "catalog_valid": True}
SIMPLES, NORMAL = {"crt": "1"}, {"crt": "3"}
EMPTY = {"CODIGO": "1", "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "NENHUMA"}
FIELDS = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS")
def kinds(result):
return {b["type"] for b in result["blockers"]}
def warns(result):
return {w["type"] for w in result["warnings"]}
# ---------------------------------------------------------------- RTC on / off
def test_rtc_off_ignores_cclasstrib_completely():
for value in (None, "", "12x", "999999", "000001"):
r = evaluate_product({**EMPTY, "CCLASSTRIB": value}, SIMPLES, OFF)
assert r["blockers"] == [] and r["applicability"]["RTC"] == "NOT_APPLICABLE" and "CCLASSTRIB" not in r["mandatory_fields"]
assert evaluate_product(EMPTY, SIMPLES, None)["blockers"] == []
def test_rtc_on_requires_and_validates_cclasstrib():
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON)) == {"CCLASSTRIB_MISSING"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "12x"}, NORMAL, ON)) == {"CCLASSTRIB_INVALID_FORMAT"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "123456"}, NORMAL, ON)) == {"CCLASSTRIB_NOT_IN_CATALOG"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "999999"}, NORMAL, ON)) == {"RTC_CST_MISSING"} # class without CST
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": "555555"}, NORMAL, ON)) == {"RTC_CST_MISSING"} # class points to a CST that is not in RTC_CST (orphan)
ok = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, NORMAL, ON)
assert ok["blockers"] == [] and ok["applicability"]["RTC"] == "REQUIRED" and "CCLASSTRIB" in ok["mandatory_fields"]
def test_rtc_on_with_invalid_catalog_blocks_with_a_single_clear_reason():
r = evaluate_product({**EMPTY, "CCLASSTRIB": "200003"}, NORMAL, {"enabled": True, "catalog": CATALOG, "catalog_valid": False})
assert kinds(r) == {"RTC_CATALOG_INVALID"}
def test_rtc_vigencia_is_used_only_when_the_catalog_carries_it():
cat = {**CATALOG, "validity": {"200003": (date(2027, 1, 1), None), "410004": (date(2025, 1, 1), date(2025, 12, 31))}}
assert validate_cclasstrib("200003", cat, date(2026, 9, 24)) == "EXPIRED" and validate_cclasstrib("410004", cat, date(2026, 9, 24)) == "EXPIRED"
assert validate_cclasstrib("000001", cat, date(2026, 9, 24)) == "VALID" and validate_cclasstrib("200003", CATALOG, date(2026, 9, 24)) == "VALID"
def test_rtc_enabled_needs_schema_and_erp_switch():
caps = {"HAS_PRODUCT_CCLASSTRIB": True, "HAS_RTC_RELATIONSHIP": True, "RTC_ENABLED_BY_PARAM": True}
assert rtc_enabled(caps) and not rtc_enabled({**caps, "RTC_ENABLED_BY_PARAM": False}) and not rtc_enabled({**caps, "HAS_RTC_RELATIONSHIP": False}) and not rtc_enabled({})
def test_cclasstrib_is_not_applicable_for_simples_before_its_effective_date_even_when_rtc_is_on():
result = evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON, date(2026, 9, 25))
assert result["applicability"]["RTC"] == "NOT_APPLICABLE"
assert result["basis"]["RTC"] == "SIMPLES_CCLASSTRIB_NOT_EFFECTIVE"
assert "CCLASSTRIB" not in result["mandatory_fields"]
assert not [blocker for blocker in result["blockers"] if blocker["category"] == "RTC"]
def test_not_applicable_cclasstrib_ignores_any_existing_value():
for value in (None, "", "12x", "999999", "200003"):
result = evaluate_product({**EMPTY, "CCLASSTRIB": value}, SIMPLES, ON, date(2026, 9, 25))
assert result["applicability"]["RTC"] == "NOT_APPLICABLE"
assert "CCLASSTRIB" not in result["mandatory_fields"]
assert not [blocker for blocker in result["blockers"] if blocker["category"] == "RTC"]
def test_cclasstrib_becomes_applicable_on_each_regime_effective_date():
for crt in ("1", "2", "4"):
before = evaluate_product({**EMPTY, "CCLASSTRIB": None}, {"crt": crt}, ON, date(2026, 12, 31))
effective = evaluate_product({**EMPTY, "CCLASSTRIB": None}, {"crt": crt}, ON, date(2027, 1, 1))
assert before["applicability"]["RTC"] == "NOT_APPLICABLE"
assert "CCLASSTRIB_MISSING" in kinds(effective)
normal_before = evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON, date(2026, 8, 2))
normal_effective = evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON, date(2026, 8, 3))
assert normal_before["applicability"]["RTC"] == "NOT_APPLICABLE"
assert "CCLASSTRIB_MISSING" in kinds(normal_effective)
def test_cclasstrib_is_not_applicable_when_company_regime_is_unresolved():
for company in (None, {"crt": None}, {"crt": "9"}):
result = evaluate_product({**EMPTY, "CCLASSTRIB": None}, company, ON, date(2026, 9, 25))
assert result["applicability"]["RTC"] == "NOT_APPLICABLE"
assert not [blocker for blocker in result["blockers"] if blocker["category"] == "RTC"]
def test_non_applicable_cclasstrib_does_not_mask_other_fiscal_blockers():
product = {**EMPTY, "CCLASSTRIB": None, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}
result = evaluate_product(product, SIMPLES, ON, date(2026, 9, 25))
assert kinds(result) == {"PIS_CST_MISSING", "COFINS_CST_MISSING"}
def test_legal_context_is_informational_and_never_creates_a_blocker():
simples, normal = legal_effective_context("1"), legal_effective_context("3")
assert simples["informational_only"] and simples["ibs_cbs_in_documents"] == "MANDATORY_FROM_2027-01-01" and normal["ibs_cbs_in_documents"] == "MANDATORY_FROM_2026-08-03"
assert simples["cclasstrib_orientation"] == "PENDING_OFFICIAL_NT" and any("Ato Conjunto RFB/CGIBS n 4/2026" in x for x in simples["sources"])
assert legal_effective_context(None)["ibs_cbs_in_documents"] == "UNKNOWN_REGIME"
# Global RTC support does not make Simples cClassTrib mandatory before its effective date.
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, ON, date(2026, 9, 25))) == set()
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, NORMAL, ON, date(2026, 9, 25))) == {"CCLASSTRIB_MISSING"}
assert kinds(evaluate_product({**EMPTY, "CCLASSTRIB": None}, SIMPLES, OFF, date(2026, 9, 25))) == set()
# ---------------------------------------------------------------- PIS / COFINS applicability is NOT circular
def test_applicability_never_depends_on_whether_the_six_fields_are_empty():
typed = {f: "49" if "COD" in f else "0" for f in FIELDS}
for crt in ("1", "2", "4", "3", None):
for base in (EMPTY, {"CODIGO": "1", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}):
empty_state = pis_cofins_applicability(base, crt)
filled_state = pis_cofins_applicability({**base, **typed}, crt)
assert empty_state == filled_state, (crt, base) # same verdict with the fields empty or typed: emptiness proves nothing
def test_empty_fields_with_applicable_context_is_a_missing_blocker():
for crt in ("1", "3"):
r = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {"crt": crt}, OFF)
assert r["applicability"]["PIS_COFINS"] == "REQUIRED" and kinds(r) == {"PIS_CST_MISSING", "COFINS_CST_MISSING"}
def test_empty_fields_with_provably_not_applicable_context_is_not_applicable():
for crt in ("1", "2", "4"):
r = evaluate_product(EMPTY, {"crt": crt}, OFF)
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["basis"]["PIS_COFINS"] == "SIMPLES_DAS_NO_SPECIAL_TREATMENT" and r["blockers"] == [] and not r["needs_review"]
assert evaluate_product({"CODIGO": "1"}, SIMPLES, OFF)["blockers"] == [] # columns absent in an old schema, Simples: nothing to require
def test_empty_fields_with_insufficient_context_need_review_and_never_default_to_complete():
for company in (NORMAL, {"crt": None}, None):
r = evaluate_product({"CODIGO": "1", "PISCOFINS": "", "INCIDENCIA_PISCOFINS": ""}, company, OFF)
assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == []
state = classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=r)["state"]
assert state == "NEEDS_REVIEW" # counted as UNCLASSIFIED, never inflating coverage
def test_taxed_regime_with_explicit_no_special_treatment_is_optional_and_typed_values_must_be_valid():
r = evaluate_product(EMPTY, NORMAL, OFF)
assert r["applicability"]["PIS_COFINS"] == "OPTIONAL" and r["blockers"] == []
assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "2x"}, NORMAL, OFF)) == {"PIS_CST_INVALID"}
def test_unknown_incidence_text_needs_review_in_every_regime():
for crt in ("1", "3"):
r = evaluate_product({**EMPTY, "INCIDENCIA_PISCOFINS": "OUTRA COISA"}, {"crt": crt}, OFF)
assert r["applicability"]["PIS_COFINS"] == "NEEDS_REVIEW" and r["needs_review"] and r["blockers"] == []
def test_simples_typed_garbage_is_ignored_as_a_warning_not_a_blocker():
row = {**EMPTY, "COD_ENTRADA_PIS": "2", "COD_SAIDA_PIS": "2", "ALIQ_PIS": "2", "COD_ENTRADA_COFINS": "2", "COD_SAIDA_COFINS": "2", "ALIQ_COFINS": "2"}
r = evaluate_product(row, SIMPLES, OFF)
assert r["applicability"]["PIS_COFINS"] == "NOT_APPLICABLE" and r["blockers"] == []
assert warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"} # "2" is one digit of official "02": normalisable
bad = evaluate_product({**EMPTY, "COD_ENTRADA_COFINS": "000", "COD_SAIDA_COFINS": "060"}, SIMPLES, OFF)
assert bad["blockers"] == [] and warns(bad) == {"CST_COFINS_INVALID_IGNORED"}
# ---------------------------------------------------------------- CST normalisation
def test_canonical_cst_accepts_only_one_digit_with_an_official_zero_padded_form():
assert canonical_cst("4") == "04" and canonical_cst("04") == "04" and canonical_cst(" 4 ") == "04" and canonical_cst("9") == "09" and canonical_cst("1") == "01"
for text in ("0", "049", "000", "060", "2x", "x", "", None, "10", "12", "00"):
assert canonical_cst(text) is None, text
assert validate_cst("4") == "VALID_AFTER_NORMALIZATION" and validate_cst("04") == "VALID" and validate_cst("049") == "INVALID" and validate_cst("") == "EMPTY"
def test_unpadded_cst_is_semantically_valid_warns_and_is_used_canonically_for_the_rules():
row = {**EMPTY, "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
r = evaluate_product(row, NORMAL, OFF)
assert r["blockers"] == [] and warns(r) == {"CST_PIS_NON_CANONICAL_FORMAT", "CST_COFINS_NON_CANONICAL_FORMAT"}
mono = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "4", "COD_SAIDA_COFINS": "4"}
assert kinds(evaluate_product(mono, NORMAL, OFF)) == set() # "4" -> "04" is compatible with MONOFASICA
assert kinds(evaluate_product({**mono, "COD_SAIDA_PIS": "1", "ALIQ_PIS": "1,65"}, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # "1" -> "01" is not
assert kinds(evaluate_product({**EMPTY, "COD_SAIDA_PIS": "049"}, NORMAL, OFF)) == {"PIS_CST_INVALID"} # ambiguous: still a blocker
# ---------------------------------------------------------------- PIS / COFINS rules
def test_declared_monophasic_product_requires_a_valid_exit_cst():
good = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}
r = evaluate_product(good, SIMPLES, OFF)
assert r["blockers"] == [] and r["applicability"]["PIS_COFINS"] == "REQUIRED" and set(r["mandatory_fields"]) == {"COD_SAIDA_PIS", "COD_SAIDA_COFINS"}
assert kinds(evaluate_product({**good, "COD_SAIDA_PIS": None}, SIMPLES, OFF)) == {"PIS_CST_MISSING"}
assert kinds(evaluate_product({**good, "COD_SAIDA_COFINS": "01", "ALIQ_COFINS": "7,6"}, SIMPLES, OFF)) == {"COFINS_CST_INCIDENCE_MISMATCH"}
def test_st_incidence_with_taxable_cst_is_a_mismatch_and_pis_cofins_are_independent():
row = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "SUBST.TRIBUTÁRIA", "COD_ENTRADA_PIS": "73", "COD_SAIDA_PIS": "01", "ALIQ_PIS": "0,65", "COD_ENTRADA_COFINS": "73", "COD_SAIDA_COFINS": "05"}
assert kinds(evaluate_product(row, NORMAL, OFF)) == {"PIS_CST_INCIDENCE_MISMATCH"} # only PIS is wrong; COFINS 05 is right
def test_rate_rules_follow_the_cst():
base = {**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "NENHUMA"}
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "01", "COD_SAIDA_COFINS": "01"}, NORMAL, OFF)) == {"PIS_RATE_MISSING", "COFINS_RATE_MISSING"}
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "01", "ALIQ_PIS": "1,65", "COD_SAIDA_COFINS": "01", "ALIQ_COFINS": "7,6"}, NORMAL, OFF)) == set()
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == set()
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "ALIQ_PIS": "abc", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == {"PIS_RATE_INVALID"}
assert kinds(evaluate_product({**base, "COD_SAIDA_PIS": "49", "ALIQ_PIS": "150", "COD_SAIDA_COFINS": "49"}, SIMPLES, OFF)) == {"PIS_RATE_INVALID"}
def test_inconsistent_flags():
assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "N", "INCIDENCIA_PISCOFINS": "MONOFÁSICA", "COD_SAIDA_PIS": "04", "COD_SAIDA_COFINS": "04"}, SIMPLES, OFF))
assert "PIS_COFINS_FLAG_INCONSISTENT" in kinds(evaluate_product({**EMPTY, "PISCOFINS": "S"}, SIMPLES, OFF))
def test_incidence_text_is_encoding_and_accent_tolerant():
for text in ("MONOFÁSICA", "MONOFASICA", "MONOF�SICA", "monofásica"):
assert incidence_kind(text) == "MONOFASICA"
assert incidence_kind("SUBST.TRIBUTÁRIA") == "SUBSTITUICAO" and incidence_kind(None) == "NONE" and incidence_kind(" nenhuma ") == "NONE" and incidence_kind("???") == "UNKNOWN"
# ---------------------------------------------------------------- ONE definition of complete, ONE queue
def test_blocker_categories_feed_the_single_dashboard_breakdown():
assert {blocker_category(t) for t in ("PIS_CST_INVALID", "PIS_COFINS_FLAG_INCONSISTENT")} == {"PIS"} and blocker_category("COFINS_RATE_MISSING") == "COFINS"
assert {blocker_category(t) for t in ("CCLASSTRIB_MISSING", "RTC_CST_MISSING", "RTC_CATALOG_INVALID")} == {"RTC"} and blocker_category("XYZ") == "OTHER"
def test_unified_coverage_invariant_and_rtc_toggle_changes_only_rtc_products():
products = [{**EMPTY, "CODIGO": "1"}, {**EMPTY, "CODIGO": "2", "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, {**EMPTY, "CODIGO": "3", "CCLASSTRIB": "200003"},
{**EMPTY, "CODIGO": "4", "INCIDENCIA_PISCOFINS": "???"}]
def run(rtc):
states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=evaluate_product(p, SIMPLES, rtc)) for p in products]
return summarize_operational_states(states)
off, on = run(OFF), run(ON)
for s in (off, on):
assert s["TOTAL_PRODUCTS"] == s["COMPLETE_PRODUCTS"] + s["SAFE_BLOCKED"] + s["FAILED_TECHNICAL"] + s["UNCLASSIFIED"] == 4
assert off["COMPLETE_PRODUCTS"] == 2 and off["BLOCKED_FISCAL_DATA"] == 1 and off["UNCLASSIFIED"] == 1 # RTC off: cClassTrib never counts
assert on["COMPLETE_PRODUCTS"] == 2 and on["BLOCKED_FISCAL_DATA"] == 1 and on["UNCLASSIFIED"] == 1 # RTC on, but Simples cClassTrib is not effective yet
def test_fiscal_blockers_never_hide_or_override_a_decision_blocker():
fiscal = evaluate_product({**EMPTY, "PISCOFINS": "S", "INCIDENCIA_PISCOFINS": "MONOFÁSICA"}, SIMPLES, OFF)
state = classify_product_state({"CODIGO": "1"}, {"status": "INVALID_NCM"}, fiscal=fiscal)
assert state["state"] == "BLOCKED_MISSING_EVIDENCE" # the NCM blocker stays the primary state
assert classify_product_state({"CODIGO": "1"}, {"status": "EXACT_NCM"}, fiscal=None)["state"] == "COMPLETE"
@@ -0,0 +1,63 @@
"""'Atualização disponível' must compare the Agent with the real default release, semantically."""
from datetime import datetime, timezone
from central.app.control_plane import dashboard_summary, list_installations
from central.app.installation_identity import compute_version_drift, outdated_versions, parse_version, reference_agent_version
from central.app.models import AgentRelease
from central.tests.test_control_plane import setup_db
from central.tests.test_operational_view_models import NOW, _installation
def _release(session, version, status="PUBLISHED", platform="windows"):
session.add(AgentRelease(version=version, platform=platform, architecture="x64", filename=f"GTIFiscalAgent-Setup-{version}.exe", storage_path=f"{version}.exe",
size_bytes=1, sha256=version.ljust(64, "0")[:64], status=status))
session.flush()
def test_versions_compare_semantically_not_as_strings():
assert compute_version_drift("1.1.7", "1.1.7") == "CURRENT"
assert compute_version_drift("1.1.10", "1.1.9") == "CURRENT" # string compare would say older
assert compute_version_drift("1.1.6", "1.1.7") == "OUTDATED"
assert compute_version_drift("1.2.0", "1.1.7") == "CURRENT" # ahead of the release is not "outdated"
assert compute_version_drift("1.0.0rc1", "1.0.0") == "OUTDATED" # pre-release sorts below its final
assert compute_version_drift(None, "1.1.7") == "UNKNOWN"
assert parse_version("garbage") is None
def test_reference_is_the_highest_published_release_and_ignores_revoked_and_drafts():
with setup_db()() as session:
_release(session, "1.1.5", "REVOKED")
_release(session, "1.1.6")
_release(session, "1.1.7")
_release(session, "1.9.9", "REVOKED")
_release(session, "2.0.0", "DRAFT")
_release(session, "3.0.0", platform="linux")
assert reference_agent_version(session) == "1.1.7"
def test_agent_on_the_default_release_is_current_everywhere_even_with_a_stale_snapshot():
with setup_db()() as session:
_release(session, "1.1.7")
_release(session, "1.1.6", "REVOKED")
# snapshot was written as OUTDATED by an older heartbeat (compared with a stale reference)
_installation(session, 1, version="1.1.7")
_installation(session, 2, version="1.1.6")
session.commit()
assert reference_agent_version(session) == "1.1.7"
assert outdated_versions(session, "1.1.7") == ["1.1.6"]
rows = {item["agent_version"]: item for item in list_installations(session, now=NOW)["items"]}
assert rows["1.1.7"]["version_drift"] == "CURRENT" and rows["1.1.6"]["version_drift"] == "OUTDATED"
assert [i["agent_version"] for i in list_installations(session, version="outdated", now=NOW)["items"]] == ["1.1.6"]
summary = dashboard_summary(session, now=NOW)
assert [a["agent_version"] for a in summary["outdated_agents"]] == ["1.1.6"]
def test_a_newer_published_release_makes_the_previous_one_outdated():
with setup_db()() as session:
_release(session, "1.1.7")
_installation(session, 1, version="1.1.7")
session.commit()
assert list_installations(session, version="outdated", now=NOW)["total"] == 0
_release(session, "1.1.8")
session.commit()
assert list_installations(session, version="outdated", now=NOW)["total"] == 1
+377
View File
@@ -0,0 +1,377 @@
"""Zero-touch control plane: central scheduler (INITIAL_SYNC once, SYNC_INCREMENTAL
per interval), single-job concurrency, central pause/resume, coverage snapshot from
the job result, idempotency. Everything is Hub-side — no local toggle involved."""
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from central.app.jobs import claim_next_job, create_job, recover_stale_running
from central.app.models import Agent, AgentSnapshot, AuditEvent, Installation, Job
from central.tests.test_jobs import _client
H = {"Authorization": "Bearer test-token"}
INST = "00000000-0000-4000-8000-0000000000aa"
OPERATIONAL = {"COMPLETE": 480, "CORRECTED": 0, "BLOCKED_MISSING_EVIDENCE": 31, "BLOCKED_CONFLICT": 10, "BLOCKED_COMPANY_CONTEXT": 0, "BLOCKED_SCHEMA": 0,
"FAILED_TECHNICAL": 0, "TOTAL_PRODUCTS": 521, "SAFE_BLOCKED": 41}
def hb(client):
return client.post("/v1/agents/heartbeat", json={"installation_id": INST, "status": "IDLE", "agent_version": "1.1.3"}, headers=H)
def jobs(engine, kind=None):
with Session(engine) as s:
q = select(Job).order_by(Job.id)
if kind: q = q.where(Job.kind == kind)
return s.scalars(q).all()
def finish(client, engine, summary=None):
job = client.get("/v1/jobs/next", headers=H).json()
assert job, "expected a claimable job"
body = {"result_summary": summary or {"operational": OPERATIONAL, "products_processed": 521, "products_changed": 521, "blocked_products": [], "resolved_products": []}}
assert client.post(f"/v1/jobs/{job['job_id']}/complete", json=body, headers=H).status_code == 200
return job
def age_last_sync(engine, seconds):
with Session(engine) as s:
for job in s.scalars(select(Job)).all():
job.finished_at = datetime.now(timezone.utc) - timedelta(seconds=seconds)
job.created_at = datetime.now(timezone.utc) - timedelta(seconds=seconds)
s.commit()
def test_first_heartbeat_creates_exactly_one_initial_sync():
client, engine = _client()
body = hb(client).json()
assert body["processing_enabled"] is True and body["sync_interval_seconds"] == 1800
for _ in range(4):
hb(client)
initial = jobs(engine, "INITIAL_SYNC")
assert len(initial) == 1 and initial[0].status == "PENDING" and initial[0].payload["reason"] == "INITIAL_SYNC"
def test_heartbeat_metadata_stores_auto_discovered_company_crt_and_contribuinte_ipi():
"""company_crt and company_contribuinte_ipi were never sent at heartbeat before (a real transport gap); once the Agent reports them
(auto-discovered from C000004, never guessed), the Hub stores them on Installation - fiscal_completion.build_company reads them directly."""
client, engine = _client()
client.post("/v1/agents/heartbeat", json={"installation_id": INST, "status": "IDLE", "agent_version": "1.1.3", "company_uf": "BA", "company_crt": "1", "company_contribuinte_ipi": "S"}, headers=H)
with Session(engine) as s:
inst = s.scalar(select(Installation))
assert inst.company_uf == "BA" and inst.company_crt == "1" and inst.company_contribuinte_ipi == "S"
def test_initial_sync_completion_sets_coverage_and_invariant():
client, engine = _client()
hb(client); finish(client, engine)
with Session(engine) as s:
snap = s.scalar(select(AgentSnapshot)); inst = s.scalar(select(Installation))
assert (snap.total_products, snap.fiscally_complete, snap.safe_blocked, snap.failed_products) == (521, 480, 41, 0)
assert snap.fiscally_complete + snap.safe_blocked + snap.failed_products == snap.total_products # coverage invariant closes
assert snap.blockers == {"BLOCKED_MISSING_EVIDENCE": 31, "BLOCKED_CONFLICT": 10}
assert inst.last_sync_at is not None and inst.last_sync_changed == 521
for _ in range(3):
hb(client)
assert len(jobs(engine)) == 1 # no second initial sync, and not due for an incremental yet
def test_incremental_sync_scheduled_once_per_interval():
client, engine = _client()
hb(client); finish(client, engine)
age_last_sync(engine, 1801)
hb(client); hb(client); hb(client)
inc = jobs(engine, "SYNC_INCREMENTAL")
assert len(inc) == 1 and inc[0].payload["reason"] == "SCHEDULED"
assert not jobs(engine, "REPROCESS_ALL") # full reprocess never runs automatically
def test_central_pause_stops_scheduling_and_claiming_but_heartbeat_continues():
client, engine = _client()
hb(client)
with Session(engine) as s:
s.scalar(select(Installation)).processing_enabled = False; s.commit()
body = hb(client).json()
assert body["processing_enabled"] is False and body["enabled"] is True # heartbeat keeps working
assert client.get("/v1/jobs/next", headers=H).json() is None # the PENDING initial sync is NOT handed out while paused
with Session(engine) as s:
s.scalar(select(Installation)).processing_enabled = True; s.commit()
assert client.get("/v1/jobs/next", headers=H).json()["kind"] == "INITIAL_SYNC" # resume: jobs flow again
def test_only_one_job_runs_at_a_time_and_queue_is_fifo():
client, engine = _client()
hb(client)
with Session(engine) as s:
agent = s.scalar(select(Agent))
create_job(s, agent, "REPROCESS_PRODUCTS", {"codigos": ["1"]})
first = client.get("/v1/jobs/next", headers=H).json()
assert first["kind"] == "INITIAL_SYNC"
assert client.get("/v1/jobs/next", headers=H).json() is None # second job waits: one fiscal job per Firebird
client.post(f"/v1/jobs/{first['job_id']}/complete", json={"result_summary": {}}, headers=H)
assert client.get("/v1/jobs/next", headers=H).json()["kind"] == "REPROCESS_PRODUCTS"
def test_reprocess_all_and_sync_now_are_not_duplicated():
client, engine = _client()
hb(client); finish(client, engine)
with Session(engine) as s:
agent = s.scalar(select(Agent))
a = create_job(s, agent, "SYNC_INCREMENTAL", {"reason": "MANUAL_SYNC_NOW"}); b = create_job(s, agent, "SYNC_INCREMENTAL")
assert a.id == b.id # double click = one job
full1 = create_job(s, agent, "REPROCESS_ALL"); full2 = create_job(s, agent, "REPROCESS_ALL")
assert full1.id == full2.id and full1.id != a.id # a full reprocess still queues behind the incremental, never twice
assert create_job(s, agent, "SYNC_INCREMENTAL").id == a.id
def test_stale_running_job_does_not_block_the_queue_forever():
client, engine = _client()
hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
with Session(engine) as s:
row = s.get(Job, job["job_id"]); row.started_at = datetime.now(timezone.utc) - timedelta(hours=2); s.commit()
create_job(s, s.scalar(select(Agent)), "REPROCESS_PRODUCTS", {"codigos": ["1"]})
assert client.get("/v1/jobs/next", headers=H).json()["kind"] == "REPROCESS_PRODUCTS"
with Session(engine) as s:
assert s.get(Job, job["job_id"]).status == "FAILED"
def test_job_events_recorded_without_secrets():
client, engine = _client()
hb(client); finish(client, engine)
with Session(engine) as s:
from central.app.models import AgentEvent
types = {e.event_type for e in s.scalars(select(AgentEvent)).all()}
assert {"HEARTBEAT", "JOB_STARTED", "JOB_COMPLETED"} <= types
def test_failed_job_records_failure_and_does_not_reschedule_immediately():
client, engine = _client()
hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
client.post(f"/v1/jobs/{job['job_id']}/fail", json={"error": "Firebird offline"}, headers=H)
for _ in range(3):
hb(client)
assert len(jobs(engine)) == 1 # backoff = the interval; no error loop against a broken Firebird
with Session(engine) as s:
assert s.scalar(select(AgentSnapshot)).last_job_status == "FAILED"
# ---------- panel: central control buttons + status ----------
def _panel():
from central.tests.test_control_panel_pages import _panel_client
client, agent_id, _ = _panel_client()
return client, agent_id, client._session_factory
def test_panel_shows_processing_status_and_the_operational_buttons():
client, agent_id, _ = _panel()
body = client.get(f"/agents/{agent_id}").text
for label in ("Sincronizar agora", "Reprocessar tudo", "Pausar processamento", "Última sincronização", "Sincronização agendada"):
assert label in body
assert "Ativo" in body and "a cada 30 min" in body and "Retomar processamento" not in body
def test_panel_sync_now_pause_resume_are_csrf_protected_audited_and_idempotent():
client, agent_id, factory = _panel()
csrf = client.cookies.get("gti_csrf")
assert client.post(f"/agents/{agent_id}/sync-now", data={"csrf_token": "wrong"}).status_code == 403
assert client.post(f"/agents/{agent_id}/sync-now", data={"csrf_token": csrf}, follow_redirects=False).status_code == 303
assert client.post(f"/agents/{agent_id}/sync-now", data={"csrf_token": csrf}, follow_redirects=False).status_code == 303 # double click
with factory() as s:
incs = s.scalars(select(Job).where(Job.kind == "SYNC_INCREMENTAL")).all()
assert len(incs) == 1 and incs[0].payload["reason"] == "MANUAL_SYNC_NOW"
client.post(f"/agents/{agent_id}/pause", data={"csrf_token": csrf})
with factory() as s:
assert s.scalar(select(Installation).where(Installation.agent_id == agent_id)).processing_enabled is False
paused = client.get(f"/agents/{agent_id}").text
assert "Pausado" in paused and "Retomar processamento" in paused and "disabled" in paused # Sync-now disabled while paused
client.post(f"/agents/{agent_id}/resume", data={"csrf_token": csrf})
with factory() as s:
assert s.scalar(select(Installation).where(Installation.agent_id == agent_id)).processing_enabled is True
actions = [a.action for a in s.scalars(select(AuditEvent)).all()]
assert {"SYNC_NOW", "PROCESSING_PAUSE", "PROCESSING_RESUME"} <= set(actions)
def test_panel_controls_require_login():
from fastapi.testclient import TestClient
from central.app.main import create_app
from central.tests.test_control_plane import seed, setup_db
factory = setup_db(); agent_id, _ = seed(factory)
anon = TestClient(create_app(database_probe=lambda: True, session_factory=factory, bootstrap_token=None))
for action in ("sync-now", "pause", "resume"):
assert anon.post(f"/agents/{agent_id}/{action}", data={}, follow_redirects=False).status_code == 303 # bounced to login, nothing executed
def _fiscal_blocker(code, field, reason, fp):
return {"company": "", "product_code": code, "gtin": None, "description": "P" + code, "blocker_type": "BLOCKED_FISCAL_DATA", "field": field, "reason_code": reason, "evidence_summary": {}, "input_fingerprint": fp}
def test_fiscal_blockers_share_the_single_queue_and_are_resolved_by_the_next_full_pass():
"""PIS/COFINS/RTC blockers are ordinary blockers; when a later FULL pass no longer reports them (data fixed / RTC switched off) they close automatically."""
from central.app.models import BlockedProduct
client, engine = _client()
hb(client)
operational = {**OPERATIONAL, "COMPLETE": 519, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0, "BLOCKED_FISCAL_DATA": 2, "SAFE_BLOCKED": 2, "TOTAL_PRODUCTS": 521}
first = {"operational": operational, "blocked_products": [_fiscal_blocker("10", "PIS", "PIS_CST_INVALID", "a"), _fiscal_blocker("11", "RTC", "CCLASSTRIB_MISSING", "b")], "resolved_products": [],
"fiscal_context": {"rtc_enabled": True, "evaluated_all": True}}
finish(client, engine, first)
with Session(engine) as s:
rows = s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()
assert sorted((r.product_code, r.field) for r in rows) == [("10", "PIS"), ("11", "RTC")]
snap = s.scalars(select(AgentSnapshot)).one()
assert (snap.fiscally_complete, snap.safe_blocked, snap.total_products) == (519, 2, 521)
age_last_sync(engine, 4000)
hb(client)
second = {"operational": {**OPERATIONAL, "COMPLETE": 520, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0, "BLOCKED_FISCAL_DATA": 1, "SAFE_BLOCKED": 1},
"blocked_products": [_fiscal_blocker("10", "PIS", "PIS_CST_INVALID", "a")], "resolved_products": [], "fiscal_context": {"rtc_enabled": False, "evaluated_all": True}}
finish(client, engine, second)
with Session(engine) as s:
open_rows = s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()
assert [(r.product_code, r.field) for r in open_rows] == [("10", "PIS")] # RTC blocker closed because RTC is off now
closed = s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_not(None))).all()
assert [(r.product_code, r.resolution_source) for r in closed] == [("11", "AUTO_REEVALUATION")]
def test_partial_reprocess_never_closes_fiscal_blockers_of_other_products():
from central.app.models import BlockedProduct
client, engine = _client()
hb(client)
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [_fiscal_blocker("10", "PIS", "PIS_CST_INVALID", "a")], "resolved_products": [], "fiscal_context": {"evaluated_all": True}})
age_last_sync(engine, 4000)
hb(client)
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": {"evaluated_all": False}})
with Session(engine) as s:
assert len(s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all()) == 1
def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_duplicate():
from central.app.models import BlockedProduct
client, engine = _client()
hb(client)
rtc_missing = _fiscal_blocker("20", "RTC", "CCLASSTRIB_MISSING", "r1")
pis_invalid = _fiscal_blocker("21", "PIS", "PIS_CST_INVALID", "p1")
op = {**OPERATIONAL, "BLOCKED_MISSING_EVIDENCE": 0, "BLOCKED_CONFLICT": 0}
finish(client, engine, {"operational": {**op, "COMPLETE": 519, "BLOCKED_FISCAL_DATA": 2, "SAFE_BLOCKED": 2}, "blocked_products": [rtc_missing, pis_invalid, dict(rtc_missing)], "resolved_products": [],
"fiscal_context": {"rtc_enabled": True, "evaluated_all": True}})
with Session(engine) as s:
assert len(s.scalars(select(BlockedProduct)).all()) == 2 # the repeated blocker is one row
age_last_sync(engine, 4000)
hb(client)
# RTC switched off in the ERP AND the PIS value was normalised/fixed: the next full pass reports nothing
finish(client, engine, {"operational": {**op, "COMPLETE": 521}, "blocked_products": [], "resolved_products": [], "fiscal_context": {"rtc_enabled": False, "evaluated_all": True}})
with Session(engine) as s:
rows = s.scalars(select(BlockedProduct)).all()
assert [r.resolved_at is not None for r in rows] == [True, True] and {r.resolution_source for r in rows} == {"AUTO_REEVALUATION"}
assert s.scalars(select(BlockedProduct).where(BlockedProduct.resolved_at.is_(None))).all() == []
age_last_sync(engine, 4000)
hb(client)
# RTC comes back on: the SAME blocker (same fingerprint) reopens instead of being duplicated
finish(client, engine, {"operational": {**op, "COMPLETE": 520, "BLOCKED_FISCAL_DATA": 1, "SAFE_BLOCKED": 1}, "blocked_products": [rtc_missing], "resolved_products": [], "fiscal_context": {"rtc_enabled": True, "evaluated_all": True}})
with Session(engine) as s:
rows = s.scalars(select(BlockedProduct)).all()
assert len(rows) == 2 and [(r.product_code, r.resolved_at is None) for r in rows] == [("20", True), ("21", False)]
def test_job_carries_the_central_rtc_authorization_default_off_and_follows_the_installation():
client, engine = _client()
hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert {k: v for k, v in job["authorizations"].items() if k != "features"} == {"rtc_write_mode": "DISABLED", "rtc_approved_codes": [], "rtc_rollbacks": [], "cfop_ippt_write_mode": "DISABLED"} # never on by default
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {}}, headers=H)
with Session(engine) as s:
inst = s.scalars(select(Installation)).one()
inst.rtc_write_mode = "ENABLED"
s.commit()
age_last_sync(engine, 4000)
hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"]["rtc_write_mode"] == "ENABLED"
def test_rtc_write_audits_are_persisted_with_server_side_identity_and_never_duplicated_per_job():
from central.app.models import AgentEvent, RtcWriteAudit
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "10", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "evidence": {"reason": "ANNEX_I_CLEAN_ITEM_UNIQUE_DEFAULT_CLASS"},
"official_source_version": {"cclasstrib_sha256": "ab" * 32}, "local_catalog_hash": "cd" * 32, "timestamp": "2026-09-24T20:00:00+00:00", "job_id": 999, "tenant_id": 12345, "agent_id": 777}
aborted = {"outcome": "ABORTED", "product_code": "11", "field": "CCLASSTRIB", "reason": "RTC_NO_LONGER_ENABLED", "rule_id": "RTC-R4", "timestamp": "2026-09-24T20:00:01+00:00"}
other_field = {"outcome": "APPLIED", "product_code": "12", "field": "IPI", "new_value": "1.0"} # anything but CCLASSTRIB is refused
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "AUTO_APPLY", "applied": [applied, other_field], "aborted": [aborted], "aborted_reasons": {"RTC_NO_LONGER_ENABLED": 1}}})
with Session(engine) as s:
rows = s.scalars(select(RtcWriteAudit).order_by(RtcWriteAudit.id)).all()
assert [(r.product_code, r.outcome, r.old_value, r.new_value, r.rule_id) for r in rows] == [("10", "APPLIED", None, "200003", "RTC-R3"), ("11", "ABORTED", None, None, "RTC-R4")]
inst = s.scalars(select(Installation)).one()
assert all(r.installation_id == inst.id and r.agent_id == inst.agent_id and r.tenant_id == inst.tenant_id for r in rows) # identity comes from the authenticated context, not the payload
assert rows[0].local_catalog_hash == "cd" * 32 and rows[0].official_source_version["cclasstrib_sha256"] == "ab" * 32 and rows[0].job_id != 999
events = [e for e in s.scalars(select(AgentEvent)).all() if e.event_type == "RTC_AUTO_APPLY"]
assert len(events) == 1 and events[0].structured_context["applied"] == 1 and events[0].severity == "WARNING"
def test_report_only_jobs_leave_no_audit_rows():
from central.app.models import RtcWriteAudit
client, engine = _client()
hb(client)
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"mode": "REPORT_ONLY", "applied": [], "aborted": [], "ready_total": 280}})
with Session(engine) as s:
assert s.scalars(select(RtcWriteAudit)).all() == []
def _set_mode(engine, mode):
with Session(engine) as s:
s.scalars(select(Installation)).one().rtc_write_mode = mode
s.commit()
def test_preview_is_stored_only_when_the_central_mode_wants_it_and_approvals_travel_in_the_next_job():
from central.app.models import RtcCandidate
client, engine = _client()
hb(client)
preview = [{"product_code": f"P{i}", "description": f"ITEM {i}", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "RTC-R3:ANNEX"} for i in range(3)]
ctx = {"rtc_enabled": True, "evaluated_all": True}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx, "rtc_auto_apply": {"mode": "REPORT_ONLY", "preview": preview, "preview_truncated": False}})
with Session(engine) as s:
assert s.scalars(select(RtcCandidate)).all() == [] # DISABLED: nothing is stored
_set_mode(engine, "APPROVAL_REQUIRED")
age_last_sync(engine, 4000); hb(client)
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx, "rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "preview": preview, "preview_truncated": False}})
with Session(engine) as s:
rows = s.scalars(select(RtcCandidate).order_by(RtcCandidate.product_code)).all()
assert [(r.product_code, r.status, r.proposed) for r in rows] == [("P0", "PENDING", "200003"), ("P1", "PENDING", "200003"), ("P2", "PENDING", "200003")]
rows[1].status = "APPROVED"
s.commit()
age_last_sync(engine, 4000); hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_approved_codes"] == ["P1"] # only what a human approved
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "fiscal_context": ctx,
"rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "preview": [p for p in preview if p["product_code"] != "P2"], "preview_truncated": False,
"applied": [{"outcome": "APPLIED", "product_code": "P1", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY", "rule_version": "rtc-rules-v2",
"company_crt": "3", "authorization_origin": "HUB_MODE:APPROVAL_REQUIRED", "description_snapshot": "ITEM 1", "rtc_status": "ENABLED", "local_catalog_state": "MATCH_SAFE"}]}}}, headers=H)
with Session(engine) as s:
status = {r.product_code: r.status for r in s.scalars(select(RtcCandidate)).all()}
assert status == {"P0": "PENDING", "P1": "APPLIED", "P2": "STALE"} # applied is recorded; a product no longer eligible on a full evaluation becomes STALE
from central.app.models import RtcWriteAudit
audit = s.scalars(select(RtcWriteAudit)).one()
assert (audit.rule_version, audit.company_crt, audit.authorization_origin, audit.description_snapshot, audit.action, audit.local_catalog_state) == ("rtc-rules-v2", "3", "HUB_MODE:APPROVAL_REQUIRED", "ITEM 1", "APPLY", "MATCH_SAFE")
assert audit.installation_uuid
def test_rollback_request_travels_in_the_job_and_its_result_closes_the_original_audit():
from central.app.models import RtcWriteAudit
client, engine = _client()
hb(client)
applied = {"outcome": "APPLIED", "product_code": "P1", "field": "CCLASSTRIB", "old_value": None, "new_value": "200003", "rule_id": "RTC-R3", "action": "APPLY"}
finish(client, engine, {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"applied": [applied]}})
with Session(engine) as s:
audit = s.scalars(select(RtcWriteAudit)).one(); audit.rollback_state = "REQUESTED"; audit_id = audit.id
s.commit()
age_last_sync(engine, 4000); hb(client)
job = client.get("/v1/jobs/next", headers=H).json()
assert job["authorizations"]["rtc_rollbacks"] == [{"audit_id": audit_id, "product_code": "P1", "written_value": "200003", "rule_id": "RTC-R3"}]
done = {"outcome": "ROLLED_BACK", "product_code": "P1", "field": "CCLASSTRIB", "old_value": "200003", "new_value": None, "rule_id": "RTC-R3", "action": "ROLLBACK", "rollback_of": audit_id}
client.post(f"/v1/jobs/{job['job_id']}/complete", json={"result_summary": {"operational": OPERATIONAL, "blocked_products": [], "resolved_products": [], "rtc_auto_apply": {"rollbacks": [done]}}}, headers=H)
with Session(engine) as s:
rows = s.scalars(select(RtcWriteAudit).order_by(RtcWriteAudit.id)).all()
assert [(r.action, r.outcome, r.rollback_state, r.rollback_of) for r in rows] == [("APPLY", "APPLIED", "DONE", None), ("ROLLBACK", "ROLLED_BACK", None, audit_id)]