from __future__ import annotations import contextlib import io import json import sys import unittest from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] ACK_SCRIPTS = REPO_ROOT / "skills" / "ack" / "scripts" sys.path.insert(0, str(ACK_SCRIPTS)) import reclaim_workers # noqa: E402 def task(task_id: str, status: str, *, incidents: list[dict] | None = None) -> dict: dispatch = {"environmentIncidents": incidents or []} return {"id": task_id, "status": status, "dispatch": dispatch} def receipt(handle: str, task_id: str) -> dict: return { "binding": {"handle": handle}, "createdFor": {"taskId": task_id}, } def board(receipts: list[dict], tasks: list[dict]) -> dict: return {"workerReceipts": receipts, "tasks": tasks} class DecideTests(unittest.TestCase): def test_all_verified_means_close(self) -> None: entry = { "taskIds": ["T-1"], "tasks": [task("T-1", "verified")], } decision, reason, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "close") self.assertIn("verified", reason) def test_blocked_means_retain(self) -> None: entry = { "taskIds": ["T-1"], "tasks": [task("T-1", "blocked")], } decision, reason, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "retain") self.assertIn("blocked", reason) def test_leftover_means_retain(self) -> None: entry = { "taskIds": ["T-1"], "tasks": [task("T-1", "leftover")], } decision, _, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "retain") def test_unresolved_incident_means_retain_even_when_verified(self) -> None: entry = { "taskIds": ["T-1"], "tasks": [ task( "T-1", "verified", incidents=[ { "id": "T-1-ENV-1", "status": "open", } ], ) ], } decision, reason, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "retain") self.assertIn("T-1-ENV-1", reason) def test_resolved_incident_does_not_block_close(self) -> None: entry = { "taskIds": ["T-1"], "tasks": [ task( "T-1", "verified", incidents=[{"id": "T-1-ENV-1", "status": "resolved"}], ) ], } decision, _, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "close") def test_unknown_task_reference_means_retain(self) -> None: entry = {"taskIds": ["T-MISSING"], "tasks": [None]} decision, reason, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "retain") self.assertIn("unknown-task", reason) def test_mixed_statuses_means_retain(self) -> None: entry = { "taskIds": ["T-1", "T-2"], "tasks": [task("T-1", "verified"), task("T-2", "retesting")], } decision, reason, _ = reclaim_workers.decide("term_1", entry) self.assertEqual(decision, "retain") self.assertIn("retesting", reason) class CollectTests(unittest.TestCase): def test_handle_maps_to_unique_task_ids(self) -> None: handles = reclaim_workers.collect_handle_tasks( board( [receipt("term_1", "T-1"), receipt("term_1", "T-1"), receipt("term_2", "T-2")], [task("T-1", "verified"), task("T-2", "blocked")], ) ) self.assertEqual(sorted(handles), ["term_1", "term_2"]) self.assertEqual(handles["term_1"]["taskIds"], ["T-1"]) def test_receipt_without_binding_is_ignored(self) -> None: handles = reclaim_workers.collect_handle_tasks( {"workerReceipts": [{"binding": {}}], "tasks": []} ) self.assertEqual(handles, {}) class MainTests(unittest.TestCase): def test_dry_run_emits_decisions_without_closing(self) -> None: board_data = board([receipt("term_1", "T-1")], [task("T-1", "verified")]) with mock.patch.object( reclaim_workers, "load_authoritative_board", return_value=(Path("/repo"), board_data) ): buffer = io.StringIO() with contextlib.redirect_stdout(buffer): exit_code = reclaim_workers.main( ["--project-root", "/repo"] ) payload = json.loads(buffer.getvalue()) self.assertEqual(exit_code, 0) self.assertEqual(payload["mode"], "dry-run") self.assertEqual(payload["handles"][0]["decision"], "close") self.assertIsNone(payload["handles"][0]["closed"]) def test_apply_closes_verified_and_keeps_blocked(self) -> None: board_data = board( [receipt("term_1", "T-1"), receipt("term_2", "T-2")], [task("T-1", "verified"), task("T-2", "blocked")], ) with ( mock.patch.object( reclaim_workers, "load_authoritative_board", return_value=(Path("/repo"), board_data) ), mock.patch.object( reclaim_workers, "resolve_executable", return_value=Path("/trusted/orca") ), mock.patch.object(reclaim_workers, "close_terminal") as close, ): buffer = io.StringIO() with contextlib.redirect_stdout(buffer): exit_code = reclaim_workers.main(["--project-root", "/repo", "--apply"]) payload = json.loads(buffer.getvalue()) self.assertEqual(exit_code, 0) close.assert_called_once() by_handle = {item["handle"]: item for item in payload["handles"]} self.assertEqual(by_handle["term_1"]["decision"], "close") self.assertTrue(by_handle["term_1"]["closed"]) self.assertEqual(by_handle["term_2"]["decision"], "retain") self.assertIsNone(by_handle["term_2"]["closed"]) def test_apply_close_failure_is_uncertain_and_reported(self) -> None: board_data = board([receipt("term_1", "T-1")], [task("T-1", "verified")]) with ( mock.patch.object( reclaim_workers, "load_authoritative_board", return_value=(Path("/repo"), board_data) ), mock.patch.object( reclaim_workers, "resolve_executable", return_value=Path("/trusted/orca") ), mock.patch.object( reclaim_workers, "close_terminal", side_effect=reclaim_workers.LaunchError("close failed"), ), ): buffer = io.StringIO() with contextlib.redirect_stdout(buffer): exit_code = reclaim_workers.main(["--project-root", "/repo", "--apply"]) payload = json.loads(buffer.getvalue()) self.assertEqual(exit_code, 0) item = payload["handles"][0] self.assertEqual(item["decision"], "uncertain") self.assertIsNone(item["closed"]) self.assertIn("close failed", item["reason"]) def test_missing_board_returns_error_exit(self) -> None: with mock.patch.object( reclaim_workers, "load_authoritative_board", side_effect=reclaim_workers.LaunchError("board missing"), ): buffer = io.StringIO() with contextlib.redirect_stdout(buffer): exit_code = reclaim_workers.main(["--project-root", "/repo"]) payload = json.loads(buffer.getvalue()) self.assertEqual(exit_code, 2) self.assertIn("error", payload) class CloseTerminalTests(unittest.TestCase): def test_close_verifies_show_close_and_live_list(self) -> None: responses = [ {"ok": True, "result": {"terminal": {"handle": "term_1"}}}, {"ok": True, "result": {"handle": "term_1"}}, {"ok": True, "result": {"terminals": [{"handle": "term_2"}]}}, ] with mock.patch.object( reclaim_workers, "run_json", side_effect=responses ): reclaim_workers.close_terminal(Path("/trusted/orca"), "term_1") def test_close_fails_when_handle_still_live(self) -> None: responses = [ {"ok": True, "result": {"terminal": {"handle": "term_1"}}}, {"ok": True, "result": {"handle": "term_1"}}, {"ok": True, "result": {"terminals": [{"handle": "term_1"}]}}, ] with ( mock.patch.object(reclaim_workers, "run_json", side_effect=responses), self.assertRaises(reclaim_workers.LaunchError), ): reclaim_workers.close_terminal(Path("/trusted/orca"), "term_1") if __name__ == "__main__": unittest.main()