from __future__ import annotations import argparse import contextlib import copy import hashlib import io import json import sys import tempfile import unittest from pathlib import Path from openpyxl import load_workbook from arr_ingestion.contracts import IngestionError from arr_ingestion.validation import DeliveryValidator, ProcessorPolicy from tests.test_arr_opera_daily_ingest import ( manual_override_manifest, reservation, run_processor, xml_document, ) PROJECT_ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = PROJECT_ROOT / "arr-opera-daily-ingest" SCRIPTS = SKILL_ROOT / "scripts" sys.path.insert(0, str(SCRIPTS)) import process_daily as processor # noqa: E402 class MemoryStore: def __init__(self, objects: dict[str, bytes]): self.objects = objects self.fetch_count = 0 def materialize(self, object_key: str, destination: Path, max_bytes: int) -> None: self.fetch_count += 1 if object_key not in self.objects: raise IngestionError("ARTIFACT_NOT_FOUND", "delivery artifact is unavailable") value = self.objects[object_key] if len(value) > max_bytes: raise IngestionError("ARTIFACT_TOO_LARGE", "delivery artifact exceeds its size limit") destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(value) def artifact_ref(key: str, filename: str, value: bytes, mime_type: str) -> dict[str, object]: return { "object_key": key, "original_filename": filename, "sha256": hashlib.sha256(value).hexdigest(), "byte_size": len(value), "mime_type": mime_type, } def build_delivery( xml_text: str, root: Path, *, manual_override: dict[str, object] | None = None, review_job_id: str | None = None, review_case_id: str | None = None, ) -> tuple[bytes, MemoryStore, dict[str, bytes]]: _exit_code, xml_path, output_dir, result, structured = run_processor( xml_text, root, manual_override=manual_override, review_job_id=review_job_id, review_case_id=review_case_id, ) status = result["status"] values = { "source_xml": xml_path.read_bytes(), "result_json": (output_dir / "result.json").read_bytes(), "structured_result_json": (output_dir / "structured-result.json").read_bytes(), } if status == "success": values["daily_report"] = (output_dir / result["outputs"]["daily_report"]).read_bytes() elif status == "failed": values["exception_report"] = (output_dir / result["outputs"]["exception_report"]).read_bytes() if structured["artifacts"]["manual_override_json"] is not None: values["manual_override_json"] = (root / "manual-override.json").read_bytes() keys = {role: f"arr/jobs/job-001/{role}/{role}" for role in values} objects = {keys[role]: value for role, value in values.items()} xlsx_mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" envelope = { "delivery_schema_version": "1.0", "delivery_id": "delivery-001", "job_id": "job-001", "attempt_no": 1, "status": status, "processor_version": processor.PROCESSOR_VERSION, "rule_set_sha256": processor.rule_set_sha256(), "result_schema_version": processor.RESULT_VERSION, "business_date": result["business_date"], "artifacts": { "source_xml": artifact_ref(keys["source_xml"], xml_path.name, values["source_xml"], "application/xml"), "daily_report": ( artifact_ref( keys["daily_report"], result["outputs"]["daily_report"], values["daily_report"], xlsx_mime, ) if status == "success" else None ), "result_json": artifact_ref( keys["result_json"], "result.json", values["result_json"], "application/json" ), "structured_result_json": artifact_ref( keys["structured_result_json"], "structured-result.json", values["structured_result_json"], "application/json", ), "exception_report": ( artifact_ref( keys["exception_report"], result["outputs"]["exception_report"], values["exception_report"], xlsx_mime, ) if status == "failed" else None ), "manual_override_json": ( artifact_ref( keys["manual_override_json"], "manual-override.json", values["manual_override_json"], "application/json", ) if "manual_override_json" in values else None ), }, } return json.dumps(envelope, ensure_ascii=False).encode("utf-8"), MemoryStore(objects), values def policy() -> ProcessorPolicy: return ProcessorPolicy( processor_version=processor.PROCESSOR_VERSION, rule_set_sha256=processor.rule_set_sha256(), skill_root=SKILL_ROOT, python_binary=sys.executable, ) class DeliveryValidationTests(unittest.TestCase): def success_xml(self) -> str: return xml_document( reservation(1, rate_code="NOT-ALLOWED", rate_amount="INVALID"), reservation(2, res_comment="SYN-GROUP", rooms="2", departure="2026-07-30"), ) def test_success_delivery_is_independently_replayed_from_object_artifacts(self): with tempfile.TemporaryDirectory() as temp_dir: raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir)) verified = DeliveryValidator(store, policy()).validate(raw) self.assertEqual(verified.envelope.status, "success") self.assertEqual(verified.structured_payload["output_rows"], 1) self.assertEqual(store.fetch_count, 4) def test_declared_hash_mismatch_is_rejected_before_replay(self): with tempfile.TemporaryDirectory() as temp_dir: raw, store, _values = build_delivery(self.success_xml(), Path(temp_dir)) envelope = json.loads(raw) envelope["artifacts"]["daily_report"]["sha256"] = "0" * 64 tampered = json.dumps(envelope).encode("utf-8") with self.assertRaisesRegex(IngestionError, "identity does not match") as raised: DeliveryValidator(store, policy()).validate(tampered) self.assertEqual(raised.exception.code, "ARTIFACT_HASH_MISMATCH") def test_semantically_tampered_daily_is_rejected_even_when_hashes_match(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) raw, store, _values = build_delivery(self.success_xml(), root / "processor") envelope = json.loads(raw) reference = envelope["artifacts"]["daily_report"] key = reference["object_key"] workbook_path = root / "tampered.xlsx" workbook_path.write_bytes(store.objects[key]) workbook = load_workbook(workbook_path, data_only=False) try: total_col = processor.DAILY_HEADERS.index("TOTAL PRICE") + 1 workbook.active.cell(2, total_col).value = 999999 workbook.save(workbook_path) finally: workbook.close() changed = workbook_path.read_bytes() store.objects[key] = changed reference["sha256"] = hashlib.sha256(changed).hexdigest() reference["byte_size"] = len(changed) structured_key = envelope["artifacts"]["structured_result_json"]["object_key"] structured = json.loads(store.objects[structured_key]) structured["artifacts"]["daily_report"]["sha256"] = reference["sha256"] structured["artifacts"]["daily_report"]["byte_size"] = reference["byte_size"] structured_bytes = (json.dumps(structured, ensure_ascii=False, indent=2) + "\n").encode("utf-8") store.objects[structured_key] = structured_bytes structured_ref = envelope["artifacts"]["structured_result_json"] structured_ref["sha256"] = hashlib.sha256(structured_bytes).hexdigest() structured_ref["byte_size"] = len(structured_bytes) with self.assertRaisesRegex(IngestionError, "rejected the delivery") as raised: DeliveryValidator(store, policy()).validate(json.dumps(envelope).encode("utf-8")) self.assertEqual(raised.exception.code, "OUTPUT_VALIDATION_FAILED") def test_failed_delivery_is_retained_but_never_declared_activation_eligible(self): failure_xml = xml_document( reservation(1, departure="2026-07-26", res_comment="SYN-FAIL") ) with tempfile.TemporaryDirectory() as temp_dir: raw, store, _values = build_delivery(failure_xml, Path(temp_dir)) verified = DeliveryValidator(store, policy()).validate(raw) self.assertEqual(verified.envelope.status, "failed") self.assertFalse(verified.structured_payload["activation_eligible"]) self.assertEqual(verified.structured_payload["output_rows"], 0) envelope = json.loads(raw) key = envelope["artifacts"]["structured_result_json"]["object_key"] payload = json.loads(store.objects[key]) payload["activation_eligible"] = True changed = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8") store.objects[key] = changed envelope["artifacts"]["structured_result_json"]["sha256"] = hashlib.sha256(changed).hexdigest() envelope["artifacts"]["structured_result_json"]["byte_size"] = len(changed) with self.assertRaisesRegex(IngestionError, "identity is invalid"): DeliveryValidator(store, policy()).validate(json.dumps(envelope).encode("utf-8")) def test_review_delivery_is_replayed_without_daily_or_finance_artifacts(self): review_xml = xml_document( reservation(1, rate_code="GRPA1", rate_amount="900"), reservation(2, rate_code="GRPA1", rate_amount="1800"), ) with tempfile.TemporaryDirectory() as temp_dir: raw, store, _values = build_delivery(review_xml, Path(temp_dir)) verified = DeliveryValidator(store, policy()).validate(raw) self.assertEqual(verified.envelope.status, "review_required") self.assertEqual(verified.structured_payload["candidate_rows"], 1) self.assertEqual(verified.structured_payload["review_required_rows"], 1) self.assertEqual(verified.structured_payload["review_issue_count"], 1) self.assertIsNone(verified.envelope.artifacts["daily_report"]) self.assertIsNone(verified.envelope.artifacts["exception_report"]) self.assertIsNone(verified.envelope.artifacts["manual_override_json"]) self.assertEqual(store.fetch_count, 3) def test_final_manual_delivery_replays_frozen_manifest(self): source = xml_document( reservation(1, rate_code="GRPA1", rate_amount="900"), reservation(2, rate_code="GRPA1", rate_amount="1800"), ) review_case_id = "dailyreview-0123456789abcdef0123456789abcdef" with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) review = run_processor(source, root / "review") manifest = manual_override_manifest( review[1], review[4]["review_issues"], job_id="job-001", review_case_id=review_case_id, real_price="0.00", ) raw, store, _values = build_delivery( source, root / "final", manual_override=manifest, review_job_id="job-001", review_case_id=review_case_id, ) verified = DeliveryValidator(store, policy()).validate(raw) self.assertEqual(verified.envelope.status, "success") self.assertEqual(verified.structured_payload["review_case_id"], review_case_id) self.assertEqual(verified.structured_payload["manually_priced_rows"], 1) self.assertIsNotNone(verified.envelope.artifacts["manual_override_json"]) self.assertEqual(store.fetch_count, 5) def test_duplicate_json_keys_are_rejected(self): raw = b'{"delivery_schema_version":"1.0","delivery_schema_version":"1.0"}' with tempfile.TemporaryDirectory() as temp_dir: store = MemoryStore({}) with self.assertRaisesRegex(IngestionError, "JSON is invalid"): DeliveryValidator(store, policy()).validate(raw) if __name__ == "__main__": unittest.main(verbosity=2)