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:
@@ -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() == []
|
||||
|
||||
Reference in New Issue
Block a user