feat: close autonomous operations for blocked products

This commit is contained in:
gti
2026-09-22 14:51:54 -03:00
parent dca4847c21
commit 3a36ac630b
16 changed files with 666 additions and 3 deletions
@@ -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"
+50
View File
@@ -0,0 +1,50 @@
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, Tenant
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
+17
View File
@@ -130,3 +130,20 @@ def test_reprocess_products_requires_codigos():
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, _ = _client()
headers = {"Authorization": "Bearer test-token"}
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
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"] == []
+33
View File
@@ -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"