feat: deterministic cClassTrib resolver + gated CCLASSTRIB auto-apply (default OFF), RTC panel summary; 1.1.9

- resolver (rules RTC-R0..R4/A1..A3/G1..G2) over the official cClassTrib table 2026-06-22 and LC 214/2025 annexes; local vs official catalog comparison
- ONE root-cause RTC blocker per product (missing/unresolved/ambiguous/conflict/official mismatch/catalog outdated/evidence missing)
- auto-apply: C000025.CCLASSTRIB only, three switches (FIREBIRD_WRITES + RTC_AUTO_APPLY_ENABLED + Hub authorization), per-product re-read and re-decision, never overwrites, rowcount 1, audit table rtc_write_audits (migration 0027)
- copy-only write E2E (280/280 applied, only CCLASSTRIB changed, 2nd run 0 writes, stale decision 0 writes); real databases untouched
- docs: cclasstrib_resolution, rtc_auto_apply, unified_fiscal_completeness

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 21:18:34 -03:00
co-authored by Claude Sonnet 5
parent 88f4318072
commit 9f08998800
51 changed files with 16461 additions and 47 deletions
+43
View File
@@ -264,3 +264,46 @@ def test_rtc_on_then_off_and_pis_fix_leave_no_stale_open_blockers_and_never_dupl
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 job["authorizations"] == {"rtc_auto_apply": False} # 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_auto_apply_authorized = True
s.commit()
age_last_sync(engine, 4000)
hb(client)
assert client.get("/v1/jobs/next", headers=H).json()["authorizations"] == {"rtc_auto_apply": True}
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() == []