feat(ack): reclaim verified worker terminals via script after round end
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user