Add version-pinned reconciliation decisions
This commit is contained in:
@@ -30,6 +30,7 @@ class DataflowNodeLibraryTests(unittest.TestCase):
|
||||
"limit",
|
||||
"quality.rules",
|
||||
"reconcile.compare",
|
||||
"reconcile.decisions",
|
||||
"subflow",
|
||||
"output",
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user