feat(ack): reclaim verified worker terminals via script after round end

This commit is contained in:
2026-08-23 23:14:29 +08:00
parent 5a906f24fa
commit 1c52a3996c
5 changed files with 468 additions and 22 deletions
+238
View File
@@ -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()