from __future__ import annotations from pathlib import Path import unittest PROJECT_ROOT = Path(__file__).resolve().parents[1] UP_PATH = PROJECT_ROOT / "database" / "017_daily_price_review.sql" DOWN_PATH = PROJECT_ROOT / "database" / "017_daily_price_review.down.sql" class DailyPriceReviewMigrationTests(unittest.TestCase): def test_forward_migration_adds_only_review_lifecycle_and_manual_lineage(self) -> None: sql = UP_PATH.read_text(encoding="utf-8") lowered = sql.lower() for required in ( "current_database() <> 'booking_test'", "migrations 008 through 016 must be applied before 017", "CREATE TABLE ingestion.daily_review_cases", "CREATE TABLE ingestion.daily_review_items", "CREATE TABLE ingestion.daily_review_events", "processing_run_id bigint NOT NULL UNIQUE", "daily_review_items_key_unique", "'awaiting_review'", "'review_required'", "'recorded_review'", "numeric(18,2)", "'manual_review'", "manual_override_sha256", "manually_priced_rows", "daily_versions_source_rule_override_unique", "daily_review_items_mutability_guard", "daily_review_cases_immutability_guard", "daily_review_events_immutability_guard", "PRICE_REVIEW_REQUIRED", "PRICE_REVIEW_UPDATED", "PRICE_REVIEW_FINALIZED", "PRICE_REVIEW_CANCELLED", "PRICE_REVIEW_GENERATION_FAILED", "PRICE_REVIEW_FAILED", "result_delivery_mode = 'artifact_callback'", "result_delivery_mode = 'direct_mcp'", ): with self.subTest(required=required): self.assertIn(required, sql) review_section = lowered[ lowered.index("create table ingestion.daily_review_cases") : lowered.index( "alter table finance.daily_versions" ) ] for forbidden in ("full_name", "confirmation_no", "res_comment", "trace_text", "remarks"): with self.subTest(forbidden=forbidden): self.assertNotIn(forbidden, review_section) self.assertIn( "GRANT SELECT, INSERT, UPDATE ON ingestion.daily_review_cases, ingestion.daily_review_items TO arr_app", sql, ) self.assertIn( "GRANT SELECT, INSERT ON ingestion.daily_review_events TO arr_app", sql, ) self.assertNotIn("DROP SCHEMA", sql) def test_down_migration_refuses_to_discard_review_or_manual_facts(self) -> None: sql = DOWN_PATH.read_text(encoding="utf-8") for required in ( "current_database() <> 'booking_test'", "SELECT 1 FROM ingestion.daily_review_cases", "review_case_id IS NOT NULL", "manual_override_sha256 IS NOT NULL", "run_status = 'awaiting_review'", "attempt_status = 'review_required'", "delivery_status = 'recorded_review'", "refusing destructive rollback: daily review or manual-pricing facts exist", "DROP TABLE ingestion.daily_review_events", "DROP TABLE ingestion.daily_review_items", "DROP TABLE ingestion.daily_review_cases", "daily_review_events_immutability_guard", "result_delivery_mode = 'artifact_callback'", "result_delivery_mode = 'direct_mcp'", ): with self.subTest(required=required): self.assertIn(required, sql) def test_followup_artifact_kind_migration_is_forward_only_after_manifest_facts(self) -> None: root = Path(__file__).resolve().parents[1] up = (root / "database" / "018_daily_review_manual_override_artifact.sql").read_text( encoding="utf-8" ) down = ( root / "database" / "018_daily_review_manual_override_artifact.down.sql" ).read_text(encoding="utf-8") self.assertIn("ARR migration 017 must be applied first", up) self.assertIn("'manual_override_json'", up) self.assertIn("artifacts_artifact_kind_check", up) self.assertIn("ARR migration 018 is already applied", up) self.assertIn("artifact_kind = 'manual_override_json'", down) self.assertIn("refusing rollback: immutable manual-override artifacts exist", down) if __name__ == "__main__": unittest.main(verbosity=2)