Files
wyndham-ARR/tests/test_mcp_result_ingestion_migration.py
2026-07-29 16:38:05 +08:00

114 lines
4.7 KiB
Python

from __future__ import annotations
import re
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
UP_PATH = PROJECT_ROOT / "database" / "010_mcp_result_ingestion.sql"
DOWN_PATH = PROJECT_ROOT / "database" / "010_mcp_result_ingestion.down.sql"
class McpResultIngestionMigrationTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.up = UP_PATH.read_text(encoding="utf-8")
cls.down = DOWN_PATH.read_text(encoding="utf-8")
cls.up_lower = cls.up.lower()
cls.down_lower = cls.down.lower()
def test_up_is_additive_target_guarded_and_requires_mvp_v1(self) -> None:
self.assertIn("current_database() <> 'booking_test'", self.up)
self.assertIn("to_regclass('ingestion.processing_runs')", self.up)
self.assertIn("to_regclass('ingestion.processing_attempts')", self.up)
self.assertIn("to_regclass('finance.daily_versions')", self.up)
self.assertNotIn("drop schema", self.up_lower)
self.assertNotIn("truncate ", self.up_lower)
self.assertNotIn("delete from", self.up_lower)
def test_up_creates_attempt_bound_direct_submission_ledger(self) -> None:
for relation in (
"ingestion.result_submission_grants",
"ingestion.result_submissions",
):
self.assertIn(f"create table {relation}", self.up_lower)
self.assertNotIn("result_submission_batches", self.up_lower)
self.assertIn(
"foreign key (attempt_id, processing_run_id)",
self.up_lower,
)
self.assertIn("unique (attempt_id)", self.up_lower)
self.assertIn("interval '30 minutes'", self.up_lower)
self.assertIn(
"consumed_at between created_at and expires_at",
self.up_lower,
)
self.assertNotIn("write_token_sha256", self.up_lower)
self.assertNotIn("password", self.up_lower)
self.assertNotIn("access_key", self.up_lower)
def test_same_payload_hash_is_indexed_but_not_unique(self) -> None:
self.assertIn(
"create index result_submissions_payload_sha256_idx",
self.up_lower,
)
self.assertNotRegex(
self.up_lower,
re.compile(r"unique\s*\([^)]*payload_sha256", re.DOTALL),
)
self.assertIn("same xml/result may be processed repeatedly", self.up_lower)
def test_single_payload_is_bounded_counted_and_purged_at_terminal(self) -> None:
self.assertIn("payload_byte_size between 2 and 3145728", self.up_lower)
self.assertIn("payload_json ? 'records'", self.up_lower)
self.assertIn(
"jsonb_array_length(payload_json -> 'records') = record_count",
self.up_lower,
)
self.assertIn("submission_status = 'received'", self.up_lower)
self.assertIn("payload_json is not null", self.up_lower)
self.assertIn("payload_json is null", self.up_lower)
self.assertIn("payload_purged_at is not null", self.up_lower)
self.assertIn("submission_status <> 'expired'", self.up_lower)
self.assertIn("finished_at >= expires_at", self.up_lower)
self.assertNotIn("expected_batch_count", self.up_lower)
self.assertNotIn("received_batch_count", self.up_lower)
def test_existing_artifact_contract_is_preserved_as_default(self) -> None:
self.assertGreaterEqual(
self.up_lower.count("default 'artifact_callback'"),
2,
)
self.assertIn("result_delivery_mode = 'direct_mcp'", self.up_lower)
self.assertIn("result_artifact_id is null", self.up_lower)
self.assertIn("daily_report_artifact_id is null", self.up_lower)
self.assertIn("daily_report_artifact_id is not null", self.up_lower)
def test_down_refuses_data_and_restores_008_constraints(self) -> None:
self.assertIn("current_database() <> 'booking_test'", self.down)
self.assertIn(
"refusing rollback: migration 010 direct MCP state exists",
self.down,
)
self.assertIn("exists (select 1 from ingestion.result_submissions)", self.down_lower)
self.assertNotIn("result_submission_batches", self.down_lower)
self.assertLess(
self.down_lower.index("drop table ingestion.result_submissions"),
self.down_lower.index("drop table ingestion.result_submission_grants"),
)
self.assertIn("drop column result_delivery_mode", self.down_lower)
self.assertIn(
"add constraint processing_runs_terminal_shape check",
self.down_lower,
)
self.assertIn(
"add constraint daily_versions_status_shape check",
self.down_lower,
)
if __name__ == "__main__":
unittest.main(verbosity=2)