diff --git a/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py b/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py index f23955fac..e89e2d5a2 100644 --- a/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py +++ b/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py @@ -26,6 +26,7 @@ from src.services.awooop_ansible_audit_service import ( build_ansible_decision_audit_payload, preflight_ansible_candidate_generation, record_ansible_decision_audit, + verified_ansible_candidate_terminal_sql, ) from src.services.awooop_ansible_check_mode_service import ( preflight_failed_apply_retry_queue_once, @@ -65,10 +66,12 @@ async def _fetch_missing_candidate_incidents( window_hours: int, scan_limit: int, ) -> list[dict[str, Any]]: + terminal_candidate_sql = verified_ansible_candidate_terminal_sql( + "candidate" + ) async with get_db_context(project_id) as db: await db.execute(text("SET LOCAL statement_timeout = '5000ms'")) - result = await db.execute( - text(""" + query_sql = """ SELECT incidents.incident_id, incidents.project_id, @@ -94,6 +97,8 @@ async def _fetch_missing_candidate_incidents( AS latest_candidate_target_route_generation, latest_candidate.source_occurrence_generation AS latest_candidate_source_occurrence_generation, + latest_candidate.is_terminal + AS latest_candidate_is_terminal, ( SELECT approval.id FROM approval_records approval @@ -134,7 +139,8 @@ async def _fetch_missing_candidate_incidents( candidate.input ->> 'target_route_generation' AS target_route_generation, candidate.input ->> 'source_occurrence_generation' - AS source_occurrence_generation + AS source_occurrence_generation, + ({terminal_candidate_sql}) AS is_terminal FROM automation_operation_log candidate WHERE candidate.operation_type = 'ansible_candidate_matched' AND candidate.input ->> 'executor' = 'ansible' @@ -163,7 +169,11 @@ async def _fetch_missing_candidate_incidents( ) = current_recurrence.source_fingerprint ORDER BY current_recurrence.received_at DESC LIMIT :scan_limit - """), + """.replace( + "{terminal_candidate_sql}", terminal_candidate_sql + ) + result = await db.execute( + text(query_sql), { "project_id": project_id, "window_hours": max(1, window_hours), diff --git a/apps/api/tests/test_ansible_verified_closure.py b/apps/api/tests/test_ansible_verified_closure.py index 966887f36..9770392c3 100644 --- a/apps/api/tests/test_ansible_verified_closure.py +++ b/apps/api/tests/test_ansible_verified_closure.py @@ -618,9 +618,11 @@ def test_terminal_candidate_contract_requires_verified_four_node_chain() -> None assert "repository_readback_verified" in predicate assert "{detail,execution_success}" in predicate assert "{detail,telegram_receipt_acknowledged}" in predicate - assert "ORDER BY latest.created_at DESC, latest.op_id DESC" in ( + assert "ORDER BY candidate.created_at DESC, candidate.op_id DESC" in ( recorder_source ) + assert "target_route_generation" in recorder_source + assert "candidate_lane_lock_key" in recorder_source @pytest.mark.asyncio @@ -1169,8 +1171,13 @@ def test_backfill_preserves_approval_and_replaces_terminal_skipped_candidate() - assert proposal["approval_id"] == "00000000-0000-0000-0000-000000000103" assert "approval_records" in query_source assert "verified_ansible_candidate_terminal_sql" in query_source - assert "AND NOT ({terminal_candidate_sql})" in query_source - assert "ORDER BY latest.created_at DESC, latest.op_id DESC" in query_source + assert "current_recurrence" in query_source + assert "target_route_generation" in query_source + assert "latest_candidate_is_terminal" in query_source + assert "preflight_ansible_candidate_generation" in inspect.getsource( + candidate_job.enqueue_missing_ansible_candidates_once + ) + assert "ORDER BY candidate.created_at DESC, candidate.op_id DESC" in query_source def test_retry_projection_only_repairs_latest_candidate_run() -> None: