test_data_rule_schema.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. from __future__ import annotations
  2. import importlib.util
  3. from pathlib import Path
  4. import pytest
  5. ROOT = Path(__file__).resolve().parents[1]
  6. MIGRATION = (
  7. ROOT
  8. / "migrations"
  9. / "versions"
  10. / "20260723_110_ai_data_rules.py"
  11. )
  12. RUNTIME_MIGRATION = (
  13. ROOT
  14. / "migrations"
  15. / "versions"
  16. / "20260723_120_rule_execution_runtime.py"
  17. )
  18. PLAN_STATUS_MIGRATION = (
  19. ROOT
  20. / "migrations"
  21. / "versions"
  22. / "20260723_130_bound_plan_lifecycle.py"
  23. )
  24. EXPECTED_TABLES = {
  25. "data_rules",
  26. "data_rule_versions",
  27. "rule_generation_runs",
  28. "data_standards",
  29. "data_standard_versions",
  30. "standard_rule_bindings",
  31. "dataflow_versions",
  32. "dataflow_component_bindings",
  33. "rule_execution_plans",
  34. "rule_artifacts",
  35. "dataflow_deployments",
  36. "rule_runs",
  37. "rule_violation_samples",
  38. }
  39. def test_ai_data_rule_migration_defines_all_three_domain_layers():
  40. source = MIGRATION.read_text(encoding="utf-8")
  41. assert 'revision = "20260723_110"' in source
  42. assert 'down_revision = "20260722_110"' in source
  43. for table in EXPECTED_TABLES:
  44. assert f"CREATE TABLE public.{table}" in source
  45. def test_ai_data_rule_migration_enforces_immutable_versions_and_fixed_bindings():
  46. source = MIGRATION.read_text(encoding="utf-8")
  47. for expected in (
  48. "UNIQUE (rule_uid, version_no)",
  49. "UNIQUE (standard_uid, version_no)",
  50. "UNIQUE (dataflow_uid, version_no)",
  51. "standard_version_id UUID NOT NULL",
  52. "rule_version_id UUID NOT NULL",
  53. "dataflow_version_id UUID NOT NULL",
  54. "component_kind VARCHAR(30) NOT NULL",
  55. "package_hash CHAR(64)",
  56. "plan_hash CHAR(64)",
  57. "artifact_digest CHAR(64)",
  58. "context_hash CHAR(64)",
  59. "candidate_hash CHAR(64)",
  60. "workflow_version_id UUID",
  61. ):
  62. assert expected in source
  63. def test_ai_data_rule_migration_is_forward_preserving():
  64. source = MIGRATION.read_text(encoding="utf-8")
  65. downgrade = source.split("def downgrade()", 1)[1]
  66. assert "DROP TABLE" not in downgrade.upper()
  67. assert "pass" in downgrade
  68. def test_rule_execution_runtime_migration_adds_pinned_runtime_evidence():
  69. source = RUNTIME_MIGRATION.read_text(encoding="utf-8")
  70. assert 'revision = "20260723_120"' in source
  71. assert 'down_revision = "20260723_110"' in source
  72. for table in (
  73. "data_schema_snapshots",
  74. "dataflow_dataset_bindings",
  75. "rule_compile_evidence",
  76. "rule_test_evidence",
  77. ):
  78. assert f"CREATE TABLE public.{table}" in source
  79. for expected in (
  80. "rule_execution_plan_id UUID NOT NULL",
  81. "created_by UUID REFERENCES public.users(id)",
  82. "candidate JSONB",
  83. "UNIQUE (schema_ref, schema_hash)",
  84. "UNIQUE (dataflow_deployment_id, logical_ref)",
  85. ):
  86. assert expected in source
  87. def test_rule_execution_runtime_migration_remains_forward_preserving():
  88. source = RUNTIME_MIGRATION.read_text(encoding="utf-8")
  89. downgrade = source.split("def downgrade()", 1)[1]
  90. assert "DROP TABLE" not in downgrade.upper()
  91. assert "pass" in downgrade
  92. def test_bound_plan_status_migration_is_forward_only_and_keeps_compiled_valid():
  93. source = PLAN_STATUS_MIGRATION.read_text(encoding="utf-8")
  94. assert 'revision = "20260723_130"' in source
  95. assert 'down_revision = "20260723_120"' in source
  96. assert "'compiled','published','revoked'" in source
  97. assert "'tested'" not in source
  98. assert "raise RuntimeError" in source.split("def downgrade()", 1)[1]
  99. spec = importlib.util.spec_from_file_location(
  100. "bound_plan_status_migration",
  101. PLAN_STATUS_MIGRATION,
  102. )
  103. assert spec is not None and spec.loader is not None
  104. module = importlib.util.module_from_spec(spec)
  105. spec.loader.exec_module(module)
  106. with pytest.raises(RuntimeError, match="forward-only|cannot downgrade"):
  107. module.downgrade()