test_data_rule_schema.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. ARTIFACT_CATALOG_MIGRATION = (
  25. ROOT
  26. / "migrations"
  27. / "versions"
  28. / "20260723_140_rule_run_artifacts.py"
  29. )
  30. ARTIFACT_HANDOFF_MIGRATION = (
  31. ROOT
  32. / "migrations"
  33. / "versions"
  34. / "20260723_150_rule_artifact_handoff_state.py"
  35. )
  36. EXPECTED_TABLES = {
  37. "data_rules",
  38. "data_rule_versions",
  39. "rule_generation_runs",
  40. "data_standards",
  41. "data_standard_versions",
  42. "standard_rule_bindings",
  43. "dataflow_versions",
  44. "dataflow_component_bindings",
  45. "rule_execution_plans",
  46. "rule_artifacts",
  47. "dataflow_deployments",
  48. "rule_runs",
  49. "rule_violation_samples",
  50. }
  51. def test_ai_data_rule_migration_defines_all_three_domain_layers():
  52. source = MIGRATION.read_text(encoding="utf-8")
  53. assert 'revision = "20260723_110"' in source
  54. assert 'down_revision = "20260722_110"' in source
  55. for table in EXPECTED_TABLES:
  56. assert f"CREATE TABLE public.{table}" in source
  57. def test_ai_data_rule_migration_enforces_immutable_versions_and_fixed_bindings():
  58. source = MIGRATION.read_text(encoding="utf-8")
  59. for expected in (
  60. "UNIQUE (rule_uid, version_no)",
  61. "UNIQUE (standard_uid, version_no)",
  62. "UNIQUE (dataflow_uid, version_no)",
  63. "standard_version_id UUID NOT NULL",
  64. "rule_version_id UUID NOT NULL",
  65. "dataflow_version_id UUID NOT NULL",
  66. "component_kind VARCHAR(30) NOT NULL",
  67. "package_hash CHAR(64)",
  68. "plan_hash CHAR(64)",
  69. "artifact_digest CHAR(64)",
  70. "context_hash CHAR(64)",
  71. "candidate_hash CHAR(64)",
  72. "workflow_version_id UUID",
  73. ):
  74. assert expected in source
  75. def test_ai_data_rule_migration_is_forward_preserving():
  76. source = MIGRATION.read_text(encoding="utf-8")
  77. downgrade = source.split("def downgrade()", 1)[1]
  78. assert "DROP TABLE" not in downgrade.upper()
  79. assert "pass" in downgrade
  80. def test_rule_execution_runtime_migration_adds_pinned_runtime_evidence():
  81. source = RUNTIME_MIGRATION.read_text(encoding="utf-8")
  82. assert 'revision = "20260723_120"' in source
  83. assert 'down_revision = "20260723_110"' in source
  84. for table in (
  85. "data_schema_snapshots",
  86. "dataflow_dataset_bindings",
  87. "rule_compile_evidence",
  88. "rule_test_evidence",
  89. ):
  90. assert f"CREATE TABLE public.{table}" in source
  91. for expected in (
  92. "rule_execution_plan_id UUID NOT NULL",
  93. "created_by UUID REFERENCES public.users(id)",
  94. "candidate JSONB",
  95. "UNIQUE (schema_ref, schema_hash)",
  96. "UNIQUE (dataflow_deployment_id, logical_ref)",
  97. ):
  98. assert expected in source
  99. def test_rule_execution_runtime_migration_remains_forward_preserving():
  100. source = RUNTIME_MIGRATION.read_text(encoding="utf-8")
  101. downgrade = source.split("def downgrade()", 1)[1]
  102. assert "DROP TABLE" not in downgrade.upper()
  103. assert "pass" in downgrade
  104. def test_bound_plan_status_migration_is_forward_only_and_keeps_compiled_valid():
  105. source = PLAN_STATUS_MIGRATION.read_text(encoding="utf-8")
  106. assert 'revision = "20260723_130"' in source
  107. assert 'down_revision = "20260723_120"' in source
  108. assert "'compiled','published','revoked'" in source
  109. assert "'tested'" not in source
  110. assert "raise RuntimeError" in source.split("def downgrade()", 1)[1]
  111. spec = importlib.util.spec_from_file_location(
  112. "bound_plan_status_migration",
  113. PLAN_STATUS_MIGRATION,
  114. )
  115. assert spec is not None and spec.loader is not None
  116. module = importlib.util.module_from_spec(spec)
  117. spec.loader.exec_module(module)
  118. with pytest.raises(RuntimeError, match="forward-only|cannot downgrade"):
  119. module.downgrade()
  120. def test_rule_run_artifact_catalog_is_correlation_scoped_and_forward_preserving():
  121. source = ARTIFACT_CATALOG_MIGRATION.read_text(encoding="utf-8")
  122. assert 'revision = "20260723_140"' in source
  123. assert 'down_revision = "20260723_130"' in source
  124. assert "CREATE TABLE public.rule_run_artifacts" in source
  125. for expected in (
  126. "correlation_id UUID NOT NULL",
  127. "binding_id UUID NOT NULL",
  128. "artifact_ref VARCHAR(1000) NOT NULL",
  129. "artifact_digest CHAR(64) NOT NULL",
  130. "schema_fields JSONB NOT NULL",
  131. "expires_at TIMESTAMPTZ NOT NULL",
  132. "UNIQUE (correlation_id, binding_id, artifact_digest)",
  133. ):
  134. assert expected in source
  135. downgrade = source.split("def downgrade()", 1)[1]
  136. assert "DROP TABLE" not in downgrade.upper()
  137. assert "pass" in downgrade
  138. def test_artifact_handoff_state_migration_upgrades_old_140_forward_only():
  139. source = ARTIFACT_HANDOFF_MIGRATION.read_text(encoding="utf-8")
  140. assert 'revision = "20260723_150"' in source
  141. assert 'down_revision = "20260723_140"' in source
  142. for expected in (
  143. "binding_hash CHAR(64)",
  144. "handoff_status VARCHAR(20)",
  145. "ready_at TIMESTAMPTZ",
  146. "failed_at TIMESTAMPTZ",
  147. "updated_at TIMESTAMPTZ",
  148. "UNIQUE (correlation_id, binding_id, artifact_kind)",
  149. "'pending','ready','failed'",
  150. "SELECT binding_hash",
  151. ):
  152. assert expected in source
  153. assert "artifact_digest" in source
  154. assert "RAISE EXCEPTION" in source
  155. downgrade = source.split("def downgrade()", 1)[1]
  156. assert "DROP TABLE" not in downgrade.upper()
  157. assert "raise RuntimeError" in downgrade
  158. spec = importlib.util.spec_from_file_location(
  159. "rule_artifact_handoff_state_migration",
  160. ARTIFACT_HANDOFF_MIGRATION,
  161. )
  162. assert spec is not None and spec.loader is not None
  163. module = importlib.util.module_from_spec(spec)
  164. spec.loader.exec_module(module)
  165. with pytest.raises(RuntimeError, match="forward-only|cannot downgrade"):
  166. module.downgrade()