"""Coverage batch v4-D5: host.py leftover branches (wait-turn first slice, run-turn interrupt raise, daemon-stop inner break, daemon subcommand failure, interrupt poll paths, goal verify warning). Stubbing discipline: the function under test is never stubbed. Patched collaborators are: time.monotonic / time.sleep / os.kill / subprocess.run / interrupt_turn / _wait_turn / dispatch / run_goal_loop / json.load (sinks, clocks and predicates). Every assertion reads a real return value or real stdout. """ import contextlib import io import json import os import queue import signal import sys import tempfile import threading import time import unittest from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT)) import host from host import Server, _wait_turn def _server(): srv = object.__new__(Server) srv.proc = None srv.codex_bin = "codex" srv.codex_home = None srv.approval = "deny" srv.model_provider = None srv.progress_interval = 30 srv.send_lock = threading.Lock() srv.next_id = 1 srv.pending = {} srv.pending_lock = threading.Lock() srv.turn_queues = {} srv.events = [] srv.events_lock = threading.Lock() srv.restart_count = 0 srv.trace_path = None srv._trace_lines = 0 return srv class _FakeServer: def __init__(self, *args, **kwargs): pass def initialize(self): pass def set_trace_file(self, *args, **kwargs): pass def close(self): pass def resume_thread(self, thread_id): pass def interrupt_turn(self, thread_id, turn_id): return {"error": "x"} class WaitTurnTestCase(unittest.TestCase): def test_first_slice_timeout(self): # 154: elapsed already past timeout before the first slice. q = queue.Queue() with mock.patch.object( host.time, "monotonic", side_effect=[100.0, 100.5]): self.assertEqual(_wait_turn(q, 0.1), ("timeout", None)) class RunTurnInterruptRaiseTestCase(unittest.TestCase): def test_interrupt_turn_raises(self): # 777-780 srv = _server() srv.request = mock.Mock(return_value={ "result": {"turn": {"id": "turn1"}}}) srv.interrupt_turn = mock.Mock( side_effect=RuntimeError("interrupt down")) with mock.patch.object(host, "_wait_turn", return_value=("interrupted-requested", None)): out = srv.run_turn("th1", "do work") self.assertEqual(out["status"], "interrupted") class MainLeftoverTestCase(unittest.TestCase): def _state_cfg(self, tmp, tasks): state_path = os.path.join(tmp, "host-state.json") cfg_path = os.path.join(tmp, "host-config.json") with open(state_path, "w", encoding="utf-8") as f: json.dump({"tasks": tasks, "automations": []}, f) with open(cfg_path, "w", encoding="utf-8") as f: json.dump({}, f) return state_path, cfg_path def _run_main(self, argv, patches=()): with contextlib.ExitStack() as stack: for p in patches: stack.enter_context(p) buf = io.StringIO() with contextlib.redirect_stdout(buf): with mock.patch.object(sys, "argv", argv): host.main() return buf.getvalue() def test_daemon_stop_inner_break(self): # 3407-3408: SIGTERM succeeds, the liveness probe then raises. # (A real sleep child would stay a zombie until reaped, so the # probe would succeed and the break would never fire; the # kill calls are therefore faked for this branch.) def _fake_kill(pid, sig): if sig == 0: raise ProcessLookupError("gone") with tempfile.TemporaryDirectory() as tmp: state_path, cfg_path = self._state_cfg(tmp, {}) pidfile = os.path.join(tmp, "host-daemon.pid") with open(pidfile, "w", encoding="utf-8") as f: f.write("12345") argv = ["host.py", "--state", state_path, "--config", cfg_path, "daemon-stop"] out = self._run_main( argv, patches=[mock.patch("os.kill", side_effect=_fake_kill)]) self.assertIn("daemon 已停", out) self.assertFalse(os.path.exists(pidfile)) def test_daemon_subcommand_failure(self): # 3504-3505 with tempfile.TemporaryDirectory() as tmp: state_path, cfg_path = self._state_cfg(tmp, {}) argv = ["host.py", "--state", state_path, "--config", cfg_path, "daemon", "--foreground", "--interval", "300"] old_term = signal.getsignal(signal.SIGTERM) old_int = signal.getsignal(signal.SIGINT) def sender(): time.sleep(0.5) try: os.kill(os.getpid(), signal.SIGTERM) except Exception: pass sender_thread = threading.Thread(target=sender, daemon=True) try: sender_thread.start() out = self._run_main(argv, patches=[ mock.patch("subprocess.run", side_effect=RuntimeError("child down")), ]) finally: sender_thread.join(timeout=5) signal.signal(signal.SIGTERM, old_term) signal.signal(signal.SIGINT, old_int) self.assertIn("daemon subcommand", out) def test_interrupt_poll_responded(self): # 3963-3964 with tempfile.TemporaryDirectory() as tmp: state_path, cfg_path = self._state_cfg(tmp, { "t1": {"name": "t1", "thread_id": "th1", "status": "interrupted", "turns": [{"result": {"turn_id": "turn9"}}]}}) argv = ["host.py", "--state", state_path, "--config", cfg_path, "interrupt", "--task", "t1"] out = self._run_main(argv, patches=[ mock.patch.object(host, "Server", lambda **kw: _FakeServer()), mock.patch("time.sleep", lambda *a, **k: None), ]) self.assertIn("属主已响应", out) def test_interrupt_poll_read_failure(self): # 3965-3967 (+ else branch): state reads fail during polling. with tempfile.TemporaryDirectory() as tmp: state_path, cfg_path = self._state_cfg(tmp, { "t1": {"name": "t1", "thread_id": "th1", "status": "running", "turns": [{"result": {"turn_id": "turn9"}}]}}) argv = ["host.py", "--state", state_path, "--config", cfg_path, "interrupt", "--task", "t1"] real_load = json.load calls = {"n": 0} def _load(fp, *a, **k): calls["n"] += 1 if calls["n"] <= 2: return real_load(fp, *a, **k) raise RuntimeError("poll read down") out = self._run_main(argv, patches=[ mock.patch.object(host, "Server", lambda **kw: _FakeServer()), mock.patch("time.sleep", lambda *a, **k: None), mock.patch("json.load", side_effect=_load), ]) self.assertIn("10 秒内未响应", out) def test_goal_verify_argv_warning(self): # 4025 with tempfile.TemporaryDirectory() as tmp: state_path, cfg_path = self._state_cfg(tmp, {}) argv = ["host.py", "--state", state_path, "--config", cfg_path, "goal", "--name", "g", "--task", "t", "--repo", "/tmp", "--verify", "echo hi", "--verify-argv", "echo", "hi"] out = self._run_main(argv, patches=[ mock.patch.object(host, "Server", lambda **kw: _FakeServer()), mock.patch.object(host, "dispatch", return_value={"name": "g"}), mock.patch.object(host, "run_goal_loop", return_value=None), ]) self.assertIn("verify_argv 已指定", out) if __name__ == "__main__": unittest.main()