Add durable reconciliation row evidence
This commit is contained in:
+123
-1
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user