"""Acquisition stops before exceeding the same inventory bound used by audit.""" import hashlib import json from pathlib import Path import tempfile import unittest from unittest.mock import patch from integrations.ohip import collect_arr_source as source, collect_arr_day as day, capture_day_job as jobs from integrations.ohip.rate_info import RateInfoReader from tests.test_ohip_arr_collection import DAY, HOTEL, FakeService from tests.test_ohip_day_capture import DayService class ArchiveBudgetTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.root = Path(temporary.name) self.enterContext(patch.object(source, "MAX_ARCHIVE_BYTES", 16 * 1024)) self.enterContext(patch.object(source, "MAX_FINAL_METADATA_BYTES", 2 * 1024)) def test_budget_checked_before_file_creation_and_footer_space_reserved(self): archive = source.Archive(self.root / "capture") accepted = b"a" * (source.MAX_ARCHIVE_BYTES - source.MAX_FINAL_METADATA_BYTES) archive.write("response.bin", accepted) with self.assertRaisesRegex(source.CollectionError, "archive_byte_budget_exceeded"): archive.write("overflow.bin", b"x") self.assertFalse((archive.path / "overflow.bin").exists()) self.assertEqual((archive.path / "response.bin").read_bytes(), accepted) archive.write("rate-assessments.json", b" " * source.MAX_FINAL_METADATA_BYTES) self.assertEqual(archive.inventory_bytes, source.MAX_ARCHIVE_BYTES) archive.write("result.json", {"status": "failed", "files": archive.files.copy()}) self.assertEqual(sum(f["bytes"] for f in archive.files if f["name"] != "result.json"), source.MAX_ARCHIVE_BYTES) def test_manifest_and_footer_have_separate_finite_limits(self): archive = source.Archive(self.root / "capture") with patch.object(source, "MAX_MANIFEST_BYTES", 128): for name, payload, error in [("result.json", b"x" * 129, "archive_manifest_too_large"), ("rate-assessments.json", b"x" * (source.MAX_FINAL_METADATA_BYTES + 1), "archive_footer_too_large")]: with self.subTest(name=name), self.assertRaisesRegex(source.CollectionError, error): archive.write(name, payload) self.assertFalse((archive.path / name).exists()) self.assertEqual(archive.inventory_bytes, 0) def run_capture(self, v2): archive = source.Archive(self.root / "capture") service = DayService() if v2 else FakeService() def large_response(method, path, body): status, headers, raw = service(method, path, body) return status, headers, raw + b" " * source.MAX_ARCHIVE_BYTES reader = source.Reader(archive, HOTEL, large_response, sleep=lambda _: self.fail("must not retry")) if v2: result = day.collect(day.Options(DAY, DAY, DAY, HOTEL), archive, reader, RateInfoReader(archive, HOTEL, large_response, sleep=lambda _: self.fail("must not retry"))) else: result = source.collect(source.Options(DAY, HOTEL), archive, reader) saved = json.loads((archive.path / "result.json").read_bytes()) self.assertEqual(result["error"], "archive_byte_budget_exceeded") self.assertEqual(saved["error"], result["error"]) self.assertFalse(saved["candidate_capture_complete"]) self.assertFalse(saved["finance_ready"]) self.assertEqual(len(service.calls), 1) self.assertFalse(list(archive.path.glob("*.response.bin"))) self.assertLessEqual(sum(f["bytes"] for f in saved["files"]), source.MAX_ARCHIVE_BYTES) self.assertEqual(result["manifest_sha256"], hashlib.sha256((archive.path / "result.json").read_bytes()).hexdigest()) return archive def test_v1_overflow_stops_after_first_response_and_keeps_failure_manifest(self): self.run_capture(False) def test_v2_overflow_preserves_assessment_footer_and_failure_manifest(self): archive = self.run_capture(True) self.assertEqual(json.loads((archive.path / "rate-assessments.json").read_bytes()), {"records": []}) def test_oversize_assessment_footer_still_writes_failed_manifest(self): archive = source.Archive(self.root / "capture") service = DayService() with patch.object(source, "MAX_ARCHIVE_BYTES", 64 * 1024), \ patch.object(source, "MAX_FINAL_METADATA_BYTES", 32): result = day.collect(day.Options(DAY, DAY, DAY, HOTEL), archive, source.Reader(archive, HOTEL, service, sleep=lambda _: None), RateInfoReader(archive, HOTEL, service, sleep=lambda _: None)) self.assertFalse(result["candidate_capture_complete"]) self.assertFalse(result["all_rates_valid"]) self.assertEqual(result["error"], "archive_footer_too_large") self.assertEqual(result["rate_responses"], 3) self.assertFalse((archive.path / "rate-assessments.json").exists()) saved = json.loads((archive.path / "result.json").read_bytes()) self.assertEqual((saved["status"], saved["error"]), ("failed", "archive_footer_too_large")) self.assertNotIn("rate-assessments.json", [item["name"] for item in saved["files"]]) def test_day_job_records_bounded_failure_without_automatic_new_attempt(self): service = DayService() def transport(method, path, body): status, headers, raw = service(method, path, body) return status, headers, raw + b" " * source.MAX_ARCHIVE_BYTES def factory(archive, hotel): return (source.Reader(archive, hotel, transport, sleep=lambda _: self.fail("retry")), RateInfoReader(archive, hotel, transport, sleep=lambda _: self.fail("retry"))) result = jobs.run_batch(self.root / "jobs", "budget", day.Options(DAY, DAY, DAY, HOTEL), factory) self.assertEqual((result["status"], result["error"]), ("capture_failed", "archive_byte_budget_exceeded")) self.assertEqual(len(service.calls), 1) state = json.loads((self.root / "jobs/budget/state.json").read_bytes()) self.assertEqual(len(state["attempts"]), 1) self.assertEqual(state["attempts"][0]["status"], "failed") if __name__ == "__main__": unittest.main()