Add durable reconciliation row evidence

This commit is contained in:
2026-08-04 11:38:29 +02:00
parent 42f87ddc88
commit 3991c179a4
7 changed files with 257 additions and 11 deletions
+123 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import unittest
from govoplan_dataflow.backend.executor import execute_preview
from govoplan_dataflow.backend.executor import PipelineExecutionError, execute_preview
from govoplan_dataflow.backend.expressions import (
ExpressionError,
evaluate_expression,
@@ -389,6 +389,128 @@ class DataflowOperatorTests(unittest.TestCase):
["changed", "missing_observed", "missing_expected"],
[item["_reconciliation_status"] for item in result.rows],
)
changed = result.rows[0]
self.assertEqual(["1"], changed["_reconciliation_key"])
self.assertEqual(
{"id": "1", "amount": 10},
changed["_reconciliation_before"],
)
self.assertEqual(
{"id": "1", "amount": 11},
changed["_reconciliation_after"],
)
self.assertEqual(
[
{
"expected_field": "amount",
"observed_field": "amount",
"expected": 10,
"observed": 11,
}
],
changed["_reconciliation_changes"],
)
replay = execute_preview(graph, row_limit=100).rows[0]
self.assertEqual(
changed["_reconciliation_key_hash"],
replay["_reconciliation_key_hash"],
)
self.assertEqual(
changed["_reconciliation_input_hash"],
replay["_reconciliation_input_hash"],
)
changed_nodes = [
(
graph_node.model_copy(
update={
"config": {
**graph_node.config,
"rows": [
{"id": "1", "amount": 12},
{"id": "3", "amount": 30},
],
}
}
)
if graph_node.id == "observed"
else graph_node
)
for graph_node in graph.nodes
]
rerun = execute_preview(
graph.model_copy(update={"nodes": changed_nodes}),
row_limit=100,
).rows[0]
self.assertEqual(
changed["_reconciliation_key_hash"],
rerun["_reconciliation_key_hash"],
)
self.assertNotEqual(
changed["_reconciliation_input_hash"],
rerun["_reconciliation_input_hash"],
)
self.assertIsNone(result.rows[1]["_reconciliation_after"])
self.assertIsNone(result.rows[2]["_reconciliation_before"])
def test_reconciliation_rejects_ambiguous_keys_without_echoing_values(self) -> None:
graph = PipelineGraph(
nodes=[
node(
"expected",
"source.inline",
{
"source_name": "expected",
"rows": [
{"id": "private-key", "amount": 10},
{"id": "private-key", "amount": 11},
],
},
x=0,
),
node(
"observed",
"source.inline",
{"source_name": "observed", "rows": []},
x=0,
),
node(
"reconcile",
"reconcile.compare",
{
"left_keys": ["id"],
"right_keys": ["id"],
"compare_columns": ["amount"],
"right_prefix": "observed_",
},
x=300,
),
node("output", "output", {}, x=600),
],
edges=[
GraphEdge(
id="e1",
source="expected",
target="reconcile",
target_port="left",
),
GraphEdge(
id="e2",
source="observed",
target="reconcile",
target_port="right",
),
GraphEdge(id="e3", source="reconcile", target="output"),
],
)
with self.assertRaisesRegex(
PipelineExecutionError,
"not unique in the expected input",
) as raised:
execute_preview(graph, row_limit=100)
self.assertNotIn("private-key", str(raised.exception))
def test_parameterized_subflow_runs_a_pinned_graph(self) -> None:
nested = {