test_data_rule_schema.py 5.1 KB

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