Enforce quality gates on output publication

This commit is contained in:
2026-08-04 11:33:11 +02:00
parent 075d2fc087
commit 2d93ad07d2
4 changed files with 221 additions and 23 deletions
+103
View File
@@ -7,6 +7,7 @@ from sqlalchemy.orm import sessionmaker
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.change_sequence import ChangeSequenceEntry
from govoplan_core.core.datasources import (
CAPABILITY_DATASOURCE_ORIGINS,
DatasourceAccessError,
@@ -154,6 +155,7 @@ class DatasourceLifecycleTests(unittest.TestCase):
DatasourceMaterializationRecord.__table__,
DatasourceStageRecord.__table__,
DatasourcePublicationRecord.__table__,
ChangeSequenceEntry.__table__,
],
)
self.Session = sessionmaker(bind=self.engine)
@@ -175,6 +177,7 @@ class DatasourceLifecycleTests(unittest.TestCase):
DatasourcePayloadRecord.__table__,
DatasourceGovernanceReferenceRecord.__table__,
DatasourceRecord.__table__,
ChangeSequenceEntry.__table__,
],
)
self.engine.dispose()
@@ -651,6 +654,106 @@ class DatasourceLifecycleTests(unittest.TestCase):
),
)
def test_publication_enforces_quality_before_persisting_any_effect(self) -> None:
producer = principal(scopes=(SOURCE_WRITE_SCOPE,))
blocked = DatasourcePublicationRequest(
producer_module="dataflow",
producer_run_ref="dataflow-run:blocked",
idempotency_key="blocked-output",
name="Blocked output",
source_name="blocked_output",
rows=({"id": 1},),
governance=DatasourceGovernance(
quality_policy={
"version": "minimum-two-v1",
"rules": [
{
"id": "minimum-two",
"type": "row_count",
"minimum": 2,
}
],
},
),
)
with self.assertRaisesRegex(
DatasourceValidationError,
r"quality or schema validation \(quality\.row_count\)",
):
self.provider.publish_rows(self.session, producer, request=blocked)
self.assertEqual(0, self.session.query(DatasourceRecord).count())
self.assertEqual(
0,
self.session.query(DatasourceMaterializationRecord).count(),
)
self.assertEqual(
0,
self.session.query(DatasourcePublicationRecord).count(),
)
def test_publication_preserves_quality_and_schema_evidence(self) -> None:
producer = principal(scopes=(SOURCE_WRITE_SCOPE,))
first = self.provider.publish_rows(
self.session,
producer,
request=DatasourcePublicationRequest(
producer_module="dataflow",
producer_run_ref="dataflow-run:first",
idempotency_key="governed-output-first",
name="Governed output",
source_name="governed_output",
rows=({"id": 1},),
governance=DatasourceGovernance(
quality_policy={
"version": "governed-output-v1",
"rules": [
{
"id": "unique-id",
"type": "unique",
"fields": ["id"],
},
],
"schema_policy": {"field_added_required": "warning"},
},
),
),
)
second = self.provider.publish_rows(
self.session,
producer,
request=DatasourcePublicationRequest(
producer_module="dataflow",
producer_run_ref="dataflow-run:second",
idempotency_key="governed-output-second",
target_datasource_ref=first.datasource.ref,
rows=({"id": 2, "result": "match"},),
),
)
validation = second.materialization.provenance["publication_validation"]
self.assertTrue(validation["valid"])
self.assertEqual("governed-output-v1", validation["policy_version"])
self.assertEqual(
"warning",
validation["schema_change"]["classification"],
)
self.assertEqual(
"schema.field_added_required",
validation["warnings"][0]["code"],
)
record = self.session.get(
DatasourcePublicationRecord,
second.ref.removeprefix("publication:"),
)
self.assertIsNotNone(record)
assert record is not None
self.assertEqual(
validation["policy_hash"],
record.details_["validation"]["policy_hash"],
)
def test_publication_requires_source_write_scope(self) -> None:
with self.assertRaises(DatasourceAccessError):
self.provider.publish_rows(