From 69e55718123c99d4aa7275b1150daf3fe8a92bd7 Mon Sep 17 00:00:00 2001 From: ogt Date: Sat, 11 Jul 2026 11:14:02 +0800 Subject: [PATCH] fix(agent): queue bounded host pressure handoff --- .../src/services/repair_candidate_service.py | 16 +++++- .../tests/test_repair_candidate_service.py | 49 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/repair_candidate_service.py b/apps/api/src/services/repair_candidate_service.py index 65221c4e8..10cb6c194 100644 --- a/apps/api/src/services/repair_candidate_service.py +++ b/apps/api/src/services/repair_candidate_service.py @@ -1225,9 +1225,21 @@ class RepairCandidateService: } and repair_template.startswith("agent99-mode ") ) + rollback_template = str( + template.get("rollback_command_template") or "" + ).strip() + safe_host_pressure_readonly_route = bool( + route == "host_pressure_readonly_after_owner_review" + and repair_template.startswith( + "/home/wooo/scripts/host-sustained-load-controller.py " + ) + and "systemctl restart" not in repair_template.lower() + and rollback_template.startswith("no host mutation") + ) if ( "playbook_command_not_safely_routable" in set(blockers) and not safe_agent99_route + and not safe_host_pressure_readonly_route ): return False if not coverage_gap.get("mcp_evidence_ready"): @@ -1241,7 +1253,9 @@ class RepairCandidateService: return False if repair_template.startswith("service-specific repair"): return False - return route.endswith("_after_owner_review") + return safe_host_pressure_readonly_route or route.endswith( + "_after_owner_review" + ) def _build_playbook_draft_template( self, diff --git a/apps/api/tests/test_repair_candidate_service.py b/apps/api/tests/test_repair_candidate_service.py index 9bfb7c25c..c853ece6a 100644 --- a/apps/api/tests/test_repair_candidate_service.py +++ b/apps/api/tests/test_repair_candidate_service.py @@ -559,6 +559,55 @@ def test_host_pressure_controller_is_never_drafted_as_systemd_restart() -> None: assert "systemctl is-active" not in " ".join(template["verifier_plan_template"]) +@pytest.mark.asyncio +async def test_host_pressure_readonly_draft_queues_dedicated_ansible_candidate() -> None: + incident = _incident() + incident.signals[0].alert_name = "Host110SustainedModeratePressureLivefire" + incident.signals[0].labels = { + "component": "host-pressure-controller-livefire-source-sha", + "host": "110", + } + incident.affected_services = ["host-pressure-controller-livefire-source-sha"] + recorded: list[dict] = [] + + async def fake_recorder(**kwargs): + recorded.append(kwargs) + return True + + service = RepairCandidateService( + incident_service=FakeIncidentService(), + investigator=FakeInvestigator(_evidence(incident.incident_id)), + playbook_repository=FakePlaybookRepository(_generic_fallback_playbook()), + auto_repair_service=FakeAutoRepairService(), + ansible_audit_recorder=fake_recorder, + ) + + result = await service.build_from_incident( + incident=incident, + alertname="Host110SustainedModeratePressureLivefire", + target_resource="host-pressure-controller-livefire-source-sha", + namespace="", + message="controlled host pressure live-fire", + fallback_action="NO_ACTION - REPAIR_CANDIDATE_MISSING", + matched_playbook_id="PB-GENERIC-FALLBACK", + severity="medium", + ) + + assert result.draft_ready_for_owner_review is True + assert result.metadata["repair_candidate_owner_review_required"] is False + handoff = result.metadata["controlled_executor_handoff"] + assert handoff["status"] == "ansible_candidate_matched_queued" + assert handoff["check_mode_queued"] is True + assert handoff["candidate_count"] >= 1 + assert recorded + + audit_input = recorded[0]["proposal_data"] + assert audit_input["route_id"] == "host_pressure_readonly_after_owner_review" + assert audit_input["action"].startswith( + "/home/wooo/scripts/host-sustained-load-controller.py" + ) + + @pytest.mark.asyncio async def test_postgres_slow_query_gap_prefills_database_owner_review_not_restart() -> None: incident = _incident()