Add version-pinned reconciliation decisions

This commit is contained in:
2026-08-04 11:45:35 +02:00
parent 3991c179a4
commit 314e7c3edf
15 changed files with 724 additions and 7 deletions
+1
View File
@@ -30,6 +30,7 @@ class DataflowNodeLibraryTests(unittest.TestCase):
"limit",
"quality.rules",
"reconcile.compare",
"reconcile.decisions",
"subflow",
"output",
},
+143
View File
@@ -512,6 +512,149 @@ class DataflowOperatorTests(unittest.TestCase):
self.assertNotIn("private-key", str(raised.exception))
def test_reconciliation_decisions_apply_only_to_exact_input_evidence(self) -> None:
records = [
{
"case_id": "A-1",
"_reconciliation_key_hash": "a" * 64,
"_reconciliation_input_hash": "b" * 64,
},
{
"case_id": "A-2",
"_reconciliation_key_hash": "c" * 64,
"_reconciliation_input_hash": "d" * 64,
},
]
decisions = [
{
"key_hash": "a" * 64,
"input_hash": "b" * 64,
"decision_ref": "decision:1",
"action": "accept",
"actor_ref": "account:reviewer",
"decided_at": "2026-08-04T09:00:00Z",
"reason": "Verified against source evidence.",
"correction": None,
},
{
"key_hash": "c" * 64,
"input_hash": "e" * 64,
"decision_ref": "decision:2",
"action": "correct",
"actor_ref": "account:reviewer",
"decided_at": "2026-08-04T09:05:00Z",
"reason": "Prior amount was wrong.",
"correction": {"amount": 25},
},
{
"key_hash": "f" * 64,
"input_hash": "1" * 64,
"decision_ref": "decision:orphaned",
"action": "defer",
"actor_ref": "account:reviewer",
"decided_at": "2026-08-04T09:10:00Z",
"reason": "No current row.",
"correction": None,
},
]
graph = PipelineGraph(
nodes=[
node(
"records",
"source.inline",
{"source_name": "records", "rows": records},
x=0,
),
node(
"decisions",
"source.inline",
{"source_name": "decisions", "rows": decisions},
x=0,
),
node(
"apply",
"reconcile.decisions",
{
"decision_key_column": "key_hash",
"decision_input_column": "input_hash",
"decision_ref_column": "decision_ref",
"action_column": "action",
"actor_column": "actor_ref",
"decided_at_column": "decided_at",
"reason_column": "reason",
"correction_column": "correction",
"allowed_actions": [
"accept",
"reject",
"correct",
"defer",
],
},
x=300,
),
node("output", "output", {}, x=600),
],
edges=[
GraphEdge(
id="e1",
source="records",
target="apply",
target_port="records",
),
GraphEdge(
id="e2",
source="decisions",
target="apply",
target_port="decisions",
),
GraphEdge(id="e3", source="apply", target="output"),
],
)
result = execute_preview(graph, row_limit=100)
self.assertEqual(
["applied", "stale"],
[row["_decision_state"] for row in result.rows],
)
self.assertEqual("decision:1", result.rows[0]["_decision_ref"])
self.assertEqual(
{"amount": 25},
result.rows[1]["_decision_correction"],
)
apply_diagnostic = next(
item for item in result.node_diagnostics if item.node_id == "apply"
)
self.assertEqual(
[
"1 decision(s) are stale because reconciliation inputs changed.",
"1 decision(s) no longer match a current reconciliation row.",
],
apply_diagnostic.messages,
)
decisions[1] = dict(decisions[0], decision_ref="decision:duplicate")
duplicate_nodes = [
(
graph_node.model_copy(
update={
"config": {**graph_node.config, "rows": decisions}
}
)
if graph_node.id == "decisions"
else graph_node
)
for graph_node in graph.nodes
]
with self.assertRaisesRegex(
PipelineExecutionError,
"more than one current decision",
):
execute_preview(
graph.model_copy(update={"nodes": duplicate_nodes}),
row_limit=100,
)
def test_parameterized_subflow_runs_a_pinned_graph(self) -> None:
nested = {
"schema_version": 1,