From 9880da63fd279001e4df90f5f0b5ece469128731 Mon Sep 17 00:00:00 2001 From: ogt Date: Sat, 11 Jul 2026 19:16:04 +0800 Subject: [PATCH] fix(agent): isolate closure receipt id binds --- .../awooop_ansible_check_mode_service.py | 15 ++++++++---- .../tests/test_ansible_verified_closure.py | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 6eda82042..59144aa09 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -2194,12 +2194,13 @@ async def _read_verified_apply_closure_prerequisites( LEFT JOIN automation_operation_log candidate ON candidate.op_id = check_mode.parent_op_id AND candidate.operation_type = 'ansible_candidate_matched' - WHERE apply.op_id = CAST(:apply_op_id AS uuid) + WHERE apply.op_id = CAST(:apply_op_id_uuid AS uuid) AND apply.operation_type = 'ansible_apply_executed' LIMIT 1 """), { "apply_op_id": apply_op_id, + "apply_op_id_uuid": apply_op_id, "incident_id": claim.incident_id, "project_id": project_id, "automation_run_id": automation_run_id, @@ -2298,7 +2299,7 @@ async def _read_incident_closure_readback( ) AS resolved_lifecycle FROM incidents incident JOIN automation_operation_log apply - ON apply.op_id = CAST(:apply_op_id AS uuid) + ON apply.op_id = CAST(:apply_op_id_uuid AS uuid) WHERE incident.incident_id = :incident_id AND incident.project_id = :project_id LIMIT 1 @@ -2307,6 +2308,7 @@ async def _read_incident_closure_readback( "incident_id": claim.incident_id, "project_id": project_id, "apply_op_id": apply_op_id, + "apply_op_id_uuid": apply_op_id, "automation_run_id": automation_run_id, }, ) @@ -2434,7 +2436,7 @@ async def _commit_verified_incident_closure( WITH target_apply AS ( SELECT apply.op_id FROM automation_operation_log apply - WHERE apply.op_id = CAST(:apply_op_id AS uuid) + WHERE apply.op_id = CAST(:apply_op_id_uuid AS uuid) AND apply.operation_type = 'ansible_apply_executed' AND apply.status = 'success' AND coalesce( @@ -2596,6 +2598,7 @@ async def _commit_verified_incident_closure( """), { "apply_op_id": apply_op_id, + "apply_op_id_uuid": apply_op_id, "incident_id": claim.incident_id, "project_id": project_id, "automation_run_id": automation_run_id, @@ -4581,13 +4584,13 @@ async def _verify_retry_terminal_projection_readback( ) AS projection_verified FROM incidents incident JOIN automation_operation_log apply - ON apply.op_id = CAST(:apply_op_id AS uuid) + ON apply.op_id = CAST(:apply_op_id_uuid AS uuid) AND apply.operation_type = 'ansible_apply_executed' AND apply.status = 'failed' AND apply.input ->> 'automation_run_id' = :automation_run_id JOIN automation_operation_log replay - ON replay.op_id = CAST(:replay_op_id AS uuid) + ON replay.op_id = CAST(:replay_op_id_uuid AS uuid) AND replay.parent_op_id = apply.op_id AND replay.operation_type = 'ansible_execution_skipped' AND replay.status IN ('success', 'failed') @@ -4608,7 +4611,9 @@ async def _verify_retry_terminal_projection_readback( { "automation_run_id": automation_run_id, "apply_op_id": apply_op_id, + "apply_op_id_uuid": apply_op_id, "replay_op_id": replay_op_id, + "replay_op_id_uuid": replay_op_id, "terminal_type": terminal_type, "incident_id": claim.incident_id, "project_id": project_id, diff --git a/apps/api/tests/test_ansible_verified_closure.py b/apps/api/tests/test_ansible_verified_closure.py index 3308af252..eb506ccef 100644 --- a/apps/api/tests/test_ansible_verified_closure.py +++ b/apps/api/tests/test_ansible_verified_closure.py @@ -288,6 +288,29 @@ async def test_atomic_closure_rolls_back_when_bundle_readback_is_missing( assert "UPDATE incidents incident" in atomic_statement assert "WITH target_apply AS" in atomic_statement assert "WHEN upper(incident.status::text) = 'CLOSED'" in atomic_statement + assert db.parameters[1]["apply_op_id_uuid"] == db.parameters[1]["apply_op_id"] + + +def test_uuid_lookup_and_json_text_receipt_binds_are_type_isolated() -> None: + mixed_apply_id_queries = ( + service._read_verified_apply_closure_prerequisites, + service._read_incident_closure_readback, + service._commit_verified_incident_closure, + service._verify_retry_terminal_projection_readback, + ) + + for query_owner in mixed_apply_id_queries: + source = inspect.getsource(query_owner) + assert "CAST(:apply_op_id_uuid AS uuid)" in source + assert "CAST(:apply_op_id AS uuid)" not in source + assert '"apply_op_id_uuid": apply_op_id' in source + + retry_source = inspect.getsource( + service._verify_retry_terminal_projection_readback + ) + assert "CAST(:replay_op_id_uuid AS uuid)" in retry_source + assert "CAST(:replay_op_id AS uuid)" not in retry_source + assert '"replay_op_id_uuid": replay_op_id' in retry_source def test_terminal_candidate_contract_requires_verified_four_node_chain() -> None: