@@ -229,3 +229,107 @@ async def test_execute_approved_batch_requires_scan_roots(tmp_path: Path, monkey
229229 with pytest .raises (RuntimeError , match = "scan_roots" ):
230230 await execute_approved_batch ({"api_client" : api }, ** payload .model_dump (mode = "json" ))
231231 api .patch_proposal_state .assert_not_awaited ()
232+
233+
234+ async def test_execute_approved_batch_tolerates_post_execution_log_failure (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
235+ """Best-effort: POST execution-log failure does NOT abort the file op (lines 105-108)."""
236+ _patch_settings (monkeypatch , [str (tmp_path )])
237+ api = _make_api_client_mock ()
238+ api .post_execution_log = AsyncMock (side_effect = RuntimeError ("audit log down" ))
239+
240+ orig_paths , proposed_paths = _seed_files (tmp_path , 1 )
241+ proposals = [
242+ ExecuteBatchProposalItem (
243+ proposal_id = uuid .uuid4 (),
244+ file_id = uuid .uuid4 (),
245+ original_path = str (orig_paths [0 ]),
246+ proposed_path = str (proposed_paths [0 ]),
247+ ),
248+ ]
249+ payload = ExecuteApprovedBatchPayload (batch_id = uuid .uuid4 (), agent_id = "a" , proposals = proposals )
250+ result = await execute_approved_batch ({"api_client" : api }, ** payload .model_dump (mode = "json" ))
251+
252+ # File op still ran and proposal still marked executed
253+ assert result ["status" ] == "completed"
254+ assert proposed_paths [0 ].exists ()
255+ assert not orig_paths [0 ].exists ()
256+ assert api .patch_proposal_state .await_args .args [1 ].proposal_state == "executed"
257+
258+
259+ async def test_execute_approved_batch_tolerates_patch_completed_log_failure (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
260+ """Best-effort: PATCH completed log failure does NOT prevent SUCCESS report (lines 140-141)."""
261+ _patch_settings (monkeypatch , [str (tmp_path )])
262+ api = _make_api_client_mock ()
263+ api .patch_execution_log = AsyncMock (side_effect = RuntimeError ("patch died" ))
264+
265+ orig_paths , proposed_paths = _seed_files (tmp_path , 1 )
266+ proposals = [
267+ ExecuteBatchProposalItem (
268+ proposal_id = uuid .uuid4 (),
269+ file_id = uuid .uuid4 (),
270+ original_path = str (orig_paths [0 ]),
271+ proposed_path = str (proposed_paths [0 ]),
272+ ),
273+ ]
274+ payload = ExecuteApprovedBatchPayload (batch_id = uuid .uuid4 (), agent_id = "a" , proposals = proposals )
275+ result = await execute_approved_batch ({"api_client" : api }, ** payload .model_dump (mode = "json" ))
276+
277+ assert result ["status" ] == "completed"
278+ # patch_proposal_state still called with executed state
279+ assert api .patch_proposal_state .await_args .args [1 ].proposal_state == "executed"
280+
281+
282+ async def test_execute_approved_batch_tolerates_patch_failed_log_failure (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
283+ """Best-effort: PATCH failed log failure does NOT prevent FAILURE report (lines 173-174)."""
284+ _patch_settings (monkeypatch , [str (tmp_path )])
285+ api = _make_api_client_mock ()
286+
287+ # First PATCH (sets in_progress is via POST, second PATCH after file-op-failure goes to status=failed)
288+ # Make patch_execution_log raise on EVERY call so the "failed log" branch raises.
289+ api .patch_execution_log = AsyncMock (side_effect = RuntimeError ("patch died" ))
290+
291+ # Force file-op failure via missing source.
292+ missing = tmp_path / "missing.mp3"
293+ proposed = tmp_path / "new" / "missing.mp3"
294+ proposals = [
295+ ExecuteBatchProposalItem (
296+ proposal_id = uuid .uuid4 (),
297+ file_id = uuid .uuid4 (),
298+ original_path = str (missing ),
299+ proposed_path = str (proposed ),
300+ ),
301+ ]
302+ payload = ExecuteApprovedBatchPayload (batch_id = uuid .uuid4 (), agent_id = "a" , proposals = proposals )
303+ result = await execute_approved_batch ({"api_client" : api }, ** payload .model_dump (mode = "json" ))
304+
305+ assert result ["error_count" ] == 1
306+ # Failure still reported via patch_proposal_state(failed)
307+ assert api .patch_proposal_state .await_args .args [1 ].proposal_state == "failed"
308+
309+
310+ async def test_execute_approved_batch_tolerates_failure_report_failure (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
311+ """Last-line defense: even if patch_proposal_state(failed) raises, the batch returns cleanly (lines 189-192)."""
312+ _patch_settings (monkeypatch , [str (tmp_path )])
313+ api = _make_api_client_mock ()
314+ api .patch_proposal_state = AsyncMock (side_effect = RuntimeError ("state-machine API down" ))
315+
316+ # Force file-op failure -- this exercises the failed-PATCH-of-failure path.
317+ missing = tmp_path / "missing.mp3"
318+ proposed = tmp_path / "new" / "missing.mp3"
319+ proposals = [
320+ ExecuteBatchProposalItem (
321+ proposal_id = uuid .uuid4 (),
322+ file_id = uuid .uuid4 (),
323+ original_path = str (missing ),
324+ proposed_path = str (proposed ),
325+ ),
326+ ]
327+ payload = ExecuteApprovedBatchPayload (batch_id = uuid .uuid4 (), agent_id = "a" , proposals = proposals )
328+
329+ # Should NOT raise -- the inner try/except wraps the failure report.
330+ result = await execute_approved_batch ({"api_client" : api }, ** payload .model_dump (mode = "json" ))
331+
332+ assert result ["processed_count" ] == 1
333+ assert result ["error_count" ] == 1
334+ # Handler reached patch_proposal_state (the side_effect fired) before swallowing.
335+ api .patch_proposal_state .assert_awaited_once ()
0 commit comments