239 lines
8.8 KiB
Python
239 lines
8.8 KiB
Python
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()
|