| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- from __future__ import annotations
- import copy
- import hashlib
- import json
- import pytest
- from app.core.common.identifiers import new_governance_uid
- from app.runner.nodes import NodeExecutionError
- PLAN = {"op": "not_null", "column": "mobile"}
- PLAN_HASH = hashlib.sha256(
- json.dumps(
- PLAN,
- sort_keys=True,
- separators=(",", ":"),
- ensure_ascii=False,
- ).encode("utf-8")
- ).hexdigest()
- def rule_node(node_type="quality.check"):
- node = {
- "id": "customer_mobile",
- "type": node_type,
- "purpose": "read" if node_type == "quality.check" else "write",
- "config": {
- "component_binding_id": new_governance_uid(),
- "rule_version_id": new_governance_uid(),
- "execution_plan_hash": PLAN_HASH,
- },
- }
- if node_type == "rule.apply":
- node["idempotency"] = {
- "strategy": "upsert",
- "key": "customer_id",
- }
- return node
- class Repository:
- def __init__(self, record=None):
- self.record = record
- self.calls = []
- def load(self, **kwargs):
- self.calls.append(kwargs)
- return copy.deepcopy(self.record)
- class Adapter:
- def __init__(self):
- self.calls = []
- def execute(self, *, plan, node, parameters, write_authorized):
- self.calls.append(
- {
- "plan": plan,
- "node": node,
- "parameters": parameters,
- "write_authorized": write_authorized,
- }
- )
- return {"rows_rejected": 3}
- def published_record(node, **overrides):
- value = {
- "component_binding_id": node["config"]["component_binding_id"],
- "rule_version_id": node["config"]["rule_version_id"],
- "backend": "quality_check",
- "plan": PLAN,
- "plan_hash": PLAN_HASH,
- "plan_status": "published",
- "rule_status": "published",
- "publication_audit_trusted": True,
- "logical_evidence_trusted": True,
- "physical_evidence_trusted": True,
- "component_kind": node["type"],
- "binding_idempotency": node.get("idempotency"),
- }
- value.update(overrides)
- return value
- def test_rule_executor_loads_only_published_plan_by_fixed_identifiers():
- from app.runner.rules import RulePlanExecutor
- node = rule_node()
- adapter = Adapter()
- repository = Repository(published_record(node))
- executor = RulePlanExecutor(
- repository,
- adapters={"quality_check": adapter},
- )
- result = executor.execute(node, {"partition": "2026-07-23"})
- assert result["rows_rejected"] == 3
- assert result["rule_version_id"] == node["config"]["rule_version_id"]
- assert repository.calls == [
- {
- "component_binding_id": node["config"]["component_binding_id"],
- "rule_version_id": node["config"]["rule_version_id"],
- "plan_hash": PLAN_HASH,
- }
- ]
- assert adapter.calls[0]["parameters"] == {"partition": "2026-07-23"}
- @pytest.mark.parametrize(
- "record",
- [
- None,
- {"plan_status": "compiled"},
- {"plan_status": "tested"},
- {"plan_status": "revoked"},
- {"rule_status": "deprecated"},
- {"plan_hash": "b" * 64},
- {"publication_audit_trusted": False},
- {"logical_evidence_trusted": False},
- {"physical_evidence_trusted": False},
- ],
- )
- def test_rule_executor_fails_closed_for_missing_revoked_or_mismatched_plan(record):
- from app.runner.rules import RulePlanExecutor
- node = rule_node()
- base = published_record(node)
- if record is not None:
- base.update(record)
- record = base
- executor = RulePlanExecutor(
- Repository(record),
- adapters={"quality_check": Adapter()},
- )
- with pytest.raises(NodeExecutionError):
- executor.execute(node, {})
- def test_rule_executor_rejects_inline_plan_or_unregistered_backend():
- from app.runner.rules import RulePlanExecutor
- node = rule_node()
- node["config"]["plan"] = {"op": "bypass"}
- executor = RulePlanExecutor(
- Repository(published_record(node)),
- adapters={},
- )
- with pytest.raises(NodeExecutionError):
- executor.execute(node, {})
- clean = rule_node()
- with pytest.raises(NodeExecutionError):
- RulePlanExecutor(
- Repository(published_record(clean, backend="generated_python")),
- adapters={},
- ).execute(clean, {})
- def test_mutating_rule_requires_governed_write_authorization_and_idempotency():
- from app.runner.rules import RulePlanExecutor
- node = rule_node("rule.apply")
- record = published_record(node, backend="sql_pushdown")
- executor = RulePlanExecutor(
- Repository(record),
- adapters={"sql_pushdown": Adapter()},
- )
- with pytest.raises(NodeExecutionError):
- executor.execute(node, {}, write_authorized=False)
- del node["idempotency"]
- with pytest.raises(NodeExecutionError):
- executor.execute(node, {}, write_authorized=True)
- def test_quality_node_dispatches_sql_plan_to_read_only_quality_adapter():
- from app.core.data_rules.compilers.sql import COMPILER_VERSION
- from app.runner.rules import RulePlanExecutor
- from tests.runner.test_rule_sql import sql_plan
- plan, plan_hash = sql_plan()
- node = rule_node()
- node["config"]["rule_version_id"] = plan["rule_version_id"]
- node["config"]["execution_plan_hash"] = plan_hash
- quality_adapter = Adapter()
- record = published_record(
- node,
- backend="sql_pushdown",
- compiler_version=COMPILER_VERSION,
- plan=plan,
- plan_hash=plan_hash,
- schema_hashes={
- "rule_spec_hash": plan["rule_spec_hash"],
- "input_schema_snapshot_id": plan[
- "input_schema_snapshot_id"
- ],
- "input_schema_hash": plan["input_schema_hash"],
- "output_schema_snapshot_id": plan[
- "output_schema_snapshot_id"
- ],
- "output_schema_hash": plan["output_schema_hash"],
- },
- canonical_rule_spec_hash=plan["rule_spec_hash"],
- canonical_input_schema_snapshot_id=plan[
- "input_schema_snapshot_id"
- ],
- canonical_input_schema_hash=plan["input_schema_hash"],
- canonical_output_schema_snapshot_id=plan[
- "output_schema_snapshot_id"
- ],
- canonical_output_schema_hash=plan["output_schema_hash"],
- canonical_input_data_source_uid=plan["data_source_uid"],
- canonical_output_data_source_uid=plan["data_source_uid"],
- canonical_input_object_ref="raw.customer",
- canonical_output_object_ref="clean.customer",
- canonical_input_dialect="postgresql",
- canonical_output_dialect="postgresql",
- canonical_input_access_mode="read",
- canonical_input_write_mode=None,
- canonical_output_access_mode="write",
- canonical_output_write_mode="append",
- )
- repository = Repository(record)
- class Evidence:
- heartbeat_interval_seconds = 10
- def __init__(self):
- self.resolved = []
- def start(self, **_kwargs):
- return new_governance_uid()
- def replay(self, _rule_run_id):
- return None
- def heartbeat(self, _rule_run_id, _lease_owner):
- return None
- def finish(self, _rule_run_id, _result):
- return None
- def resolve_sql_staging(self, receipt, **kwargs):
- self.resolved.append((receipt, kwargs))
- evidence = Evidence()
- executor = RulePlanExecutor(
- repository,
- adapters={"quality_check": quality_adapter},
- evidence_writer=evidence,
- )
- context = {
- "correlation_id": new_governance_uid(),
- "dataflow_uid": new_governance_uid(),
- "deployment_id": new_governance_uid(),
- "environment": "test",
- "workflow_version": 1,
- "node_id": node["id"],
- "task_jti": new_governance_uid(),
- }
- receipt = (
- "dataops-staging://01900000-0000-7000-8000-000000000099"
- )
- result = executor.execute(
- node,
- {"input_artifact": receipt},
- **context,
- )
- assert result["execution_plan_hash"] == plan_hash
- assert quality_adapter.calls[0]["write_authorized"] is False
- assert quality_adapter.calls[0]["parameters"] == {}
- assert evidence.resolved == [
- (
- receipt,
- {
- "deployment_id": context["deployment_id"],
- "correlation_id": context["correlation_id"],
- "input_binding_id": plan["input_binding_id"],
- },
- )
- ]
- @pytest.mark.parametrize(
- ("field", "value"),
- [
- ("canonical_input_access_mode", "write"),
- ("canonical_input_write_mode", "append"),
- ("canonical_output_access_mode", "read"),
- ("canonical_output_write_mode", None),
- ],
- )
- def test_sql_plan_rechecks_live_binding_access_before_execution(field, value):
- from app.core.data_rules.compilers.sql import COMPILER_VERSION
- from app.runner.rules import RulePlanExecutor
- from tests.runner.test_rule_sql import sql_plan
- plan, plan_hash = sql_plan()
- node = rule_node("rule.apply")
- node["config"]["rule_version_id"] = plan["rule_version_id"]
- node["config"]["execution_plan_hash"] = plan_hash
- values = {
- "backend": "sql_pushdown",
- "compiler_version": COMPILER_VERSION,
- "plan": plan,
- "plan_hash": plan_hash,
- "schema_hashes": {
- "rule_spec_hash": plan["rule_spec_hash"],
- "input_schema_snapshot_id": plan["input_schema_snapshot_id"],
- "input_schema_hash": plan["input_schema_hash"],
- "output_schema_snapshot_id": plan["output_schema_snapshot_id"],
- "output_schema_hash": plan["output_schema_hash"],
- },
- "canonical_rule_spec_hash": plan["rule_spec_hash"],
- "canonical_input_schema_snapshot_id": plan["input_schema_snapshot_id"],
- "canonical_input_schema_hash": plan["input_schema_hash"],
- "canonical_output_schema_snapshot_id": plan["output_schema_snapshot_id"],
- "canonical_output_schema_hash": plan["output_schema_hash"],
- "canonical_input_data_source_uid": plan["data_source_uid"],
- "canonical_output_data_source_uid": plan["data_source_uid"],
- "canonical_input_object_ref": "raw.customer",
- "canonical_output_object_ref": "clean.customer",
- "canonical_input_dialect": "postgresql",
- "canonical_output_dialect": "postgresql",
- "canonical_input_access_mode": "read",
- "canonical_input_write_mode": None,
- "canonical_output_access_mode": "write",
- "canonical_output_write_mode": "append",
- }
- values[field] = value
- record = published_record(node, **values)
- with pytest.raises(
- NodeExecutionError,
- match="canonical attestation",
- ):
- RulePlanExecutor(
- Repository(record),
- adapters={"sql_pushdown": Adapter()},
- ).execute(node, {}, write_authorized=True)
|