Baseline merge head e5ff26a: 28144 stmts, 2807 missing, exact 90.0263%, margin +7. After thickening (full run): 2350 missing, exact 91.6501%, margin +464. Full suite 3516 items: 3509 pass, 1 failure (pre-existing November date failure, untouched), 6 skipped. No production code changed.
503 lines
20 KiB
Python
503 lines
20 KiB
Python
"""Coverage batch v4-C: host.py state/lock/worktree/verify/notify/
|
|
pr/dispatch/goal-loop branches.
|
|
|
|
Stubbing discipline: the function under test is never stubbed.
|
|
Patched collaborators are: socket.gethostname / os.getmtime / os.rmdir /
|
|
os.replace / log_event / save_state / build_verify_audit_report /
|
|
subprocess.run / gh runner / FakeServer (all sinks or data providers),
|
|
plus host.HERE (a path constant) and os.environ git identity vars for
|
|
the real-git worktree tests. Every assertion reads a real return value,
|
|
a real file, or real stdout.
|
|
"""
|
|
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
import host
|
|
from host import (
|
|
_audit_verify,
|
|
_cmd_verify_audit,
|
|
_seed_worktree,
|
|
_state_lock,
|
|
_state_tmp_path,
|
|
build_verify_audit_report,
|
|
copy_tree_excluding_git,
|
|
dispatch,
|
|
log_event,
|
|
make_worktree,
|
|
merge_pr,
|
|
notify_failure,
|
|
parse_pr_checks,
|
|
recover_interrupted,
|
|
restore_state,
|
|
run_gated_verify,
|
|
run_gh,
|
|
run_goal_loop,
|
|
run_health_check,
|
|
run_task_turn,
|
|
run_verify_argv,
|
|
save_state,
|
|
task_diff,
|
|
)
|
|
|
|
GIT_ENV = {
|
|
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
|
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
|
|
"GIT_CONFIG_NOSYSTEM": "1",
|
|
}
|
|
|
|
|
|
def _git(args, cwd, env_extra=None):
|
|
env = dict(os.environ)
|
|
env.update(GIT_ENV)
|
|
if env_extra:
|
|
env.update(env_extra)
|
|
return subprocess.run(["git"] + args, cwd=cwd, capture_output=True,
|
|
text=True, env=env)
|
|
|
|
|
|
def _init_repo(path):
|
|
os.makedirs(path, exist_ok=True)
|
|
_git(["init", "-q"], path)
|
|
with open(os.path.join(path, "f.txt"), "w") as fh:
|
|
fh.write("base\n")
|
|
_git(["add", "-A"], path)
|
|
_git(["commit", "-qm", "init"], path)
|
|
|
|
|
|
class StateLockTestCase(unittest.TestCase):
|
|
def test_state_tmp_path_socket_failure(self):
|
|
with mock.patch.object(socket, "gethostname",
|
|
side_effect=OSError("no hostname")):
|
|
out = _state_tmp_path("/tmp/state.json")
|
|
self.assertIn("unknown", out)
|
|
|
|
def test_state_lock_getmtime_failure_timeout(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
os.mkdir(path + ".lockdir")
|
|
with mock.patch.object(host, "_STATE_LOCK_TIMEOUT", 0), \
|
|
mock.patch("os.path.getmtime",
|
|
side_effect=OSError("gone")):
|
|
with self.assertRaises(RuntimeError):
|
|
with _state_lock(path):
|
|
pass
|
|
|
|
def test_state_lock_stale_rmdir_once_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
lockdir = path + ".lockdir"
|
|
os.mkdir(lockdir)
|
|
old = time.time() - 100000
|
|
os.utime(lockdir, (old, old))
|
|
real_rmdir = os.rmdir
|
|
calls = []
|
|
|
|
def _flaky(p):
|
|
calls.append(p)
|
|
if len(calls) == 1:
|
|
raise OSError("someone else")
|
|
return real_rmdir(p)
|
|
with mock.patch("os.rmdir", side_effect=_flaky):
|
|
with _state_lock(path):
|
|
pass
|
|
self.assertFalse(os.path.exists(lockdir))
|
|
|
|
def test_state_lock_final_rmdir_failure_swallowed(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
with _state_lock(path):
|
|
os.rmdir(path + ".lockdir")
|
|
self.assertFalse(os.path.exists(path + ".lockdir"))
|
|
|
|
|
|
class RestoreRecoverSaveLogTestCase(unittest.TestCase):
|
|
def test_restore_state_replace_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
with open(path, "w") as fh:
|
|
fh.write("{broken")
|
|
with mock.patch("os.replace",
|
|
side_effect=RuntimeError("rename down")):
|
|
ok, msg = restore_state(path)
|
|
self.assertFalse(ok)
|
|
self.assertIn("改名留底失败", msg)
|
|
|
|
def test_restore_state_no_file_no_backups(self):
|
|
# 1047 行(corrupt=None)只在“严格读失败、但复查时文件已
|
|
# 不在”这一竞态下可达:单线程里文件缺失时严格读直接返回
|
|
# 空 state、无需恢复;这里用 exists 的调用序列注入该竞态。
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
with open(path, "w") as fh:
|
|
fh.write("{broken")
|
|
real_exists = os.path.exists
|
|
calls = []
|
|
|
|
def _exists(p):
|
|
calls.append(p)
|
|
if len(calls) == 1:
|
|
return True
|
|
return False
|
|
with mock.patch("os.path.exists", side_effect=_exists):
|
|
ok, msg = restore_state(path)
|
|
self.assertFalse(ok)
|
|
self.assertIn("没有可用的备份", msg)
|
|
|
|
def test_recover_interrupted_log_failure(self):
|
|
state = {"tasks": {"t": {"status": "running",
|
|
"last_turn_at": 0, "created_at": 0}}}
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")):
|
|
out = recover_interrupted(state, "/tmp/state.json")
|
|
self.assertEqual(out, ["t"])
|
|
self.assertEqual(state["tasks"]["t"]["status"], "interrupted")
|
|
|
|
def test_save_state_conflict_log_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
with open(path, "w") as fh:
|
|
json.dump({"version": 5, "tasks": {}}, fh)
|
|
os.mkdir(os.path.join(d, "host-run.log"))
|
|
state = {"tasks": {}, "_loaded_version": 1}
|
|
save_state(path, state)
|
|
with open(path) as fh:
|
|
saved = json.load(fh)
|
|
self.assertEqual(saved["version"], 6)
|
|
|
|
def test_save_state_last_writer_socket_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
with mock.patch.object(socket, "gethostname",
|
|
side_effect=OSError("no hostname")):
|
|
save_state(path, {"tasks": {}})
|
|
with open(path) as fh:
|
|
saved = json.load(fh)
|
|
self.assertEqual(saved["last_writer"], str(os.getpid()))
|
|
|
|
def test_log_event_failure_swallowed(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
blocker = os.path.join(d, "blocker")
|
|
with open(blocker, "w") as fh:
|
|
fh.write("x")
|
|
log_event(os.path.join(blocker, "state.json"), "hi")
|
|
|
|
def test_health_check_pid_alive_and_close_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
path = os.path.join(d, "state.json")
|
|
with open(path, "w") as fh:
|
|
json.dump({"version": 1, "tasks": {}}, fh)
|
|
with open(os.path.join(d, "host-daemon.pid"), "w") as fh:
|
|
fh.write(str(os.getpid()))
|
|
server = mock.Mock()
|
|
server.list_threads.return_value = {"data": []}
|
|
server.close.side_effect = RuntimeError("close down")
|
|
out = run_health_check(path, {}, sys.executable, d, "a",
|
|
server_factory=lambda: server)
|
|
self.assertTrue(out["checks"]["daemon_alive"])
|
|
self.assertEqual(out["checks"]["daemon_pid"], os.getpid())
|
|
self.assertTrue(out["checks"]["server_ok"])
|
|
|
|
|
|
class WorktreeDiffTestCase(unittest.TestCase):
|
|
def test_make_worktree_existing(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with mock.patch.object(host, "HERE", d):
|
|
base = os.path.join(d, "work", "host-worktrees")
|
|
os.makedirs(os.path.join(base, "t1"))
|
|
path, branch = make_worktree("/tmp/repo", "t1")
|
|
self.assertEqual(path, os.path.join(base, "t1"))
|
|
self.assertIsNone(branch)
|
|
|
|
def test_make_worktree_branch_fallback(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
repo = os.path.join(d, "repo")
|
|
_init_repo(repo)
|
|
_git(["branch", "host/t1"], repo)
|
|
with mock.patch.object(host, "HERE", d):
|
|
path, branch = make_worktree(repo, "t1")
|
|
self.assertEqual(branch, "host/t1")
|
|
self.assertTrue(os.path.isdir(path))
|
|
|
|
def test_make_worktree_both_fail(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
repo = os.path.join(d, "repo")
|
|
os.makedirs(repo)
|
|
_git(["init", "-q"], repo)
|
|
with mock.patch.object(host, "HERE", d):
|
|
with self.assertRaises(RuntimeError):
|
|
make_worktree(repo, "t1")
|
|
|
|
def test_make_worktree_copy_non_git(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
repo = os.path.join(d, "repo")
|
|
os.makedirs(repo)
|
|
with open(os.path.join(repo, "a.txt"), "w") as fh:
|
|
fh.write("hi")
|
|
with mock.patch.object(host, "HERE", d):
|
|
path, branch = make_worktree(repo, "t1")
|
|
self.assertIsNone(branch)
|
|
with open(os.path.join(path, "a.txt")) as fh:
|
|
self.assertEqual(fh.read(), "hi")
|
|
|
|
def test_task_diff_unreadable(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
repo = os.path.join(d, "repo")
|
|
_init_repo(repo)
|
|
with open(os.path.join(repo, "bin.dat"), "wb") as fh:
|
|
fh.write(b"\xff\xfe\x00abc")
|
|
out = task_diff(repo)
|
|
self.assertEqual(out["untracked_contents"]["bin.dat"],
|
|
"<binary or unreadable>")
|
|
|
|
|
|
class VerifyAuditTestCase(unittest.TestCase):
|
|
def test_audit_verify_log_failure(self):
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")):
|
|
_audit_verify("/tmp/state.json", "true", "argv", 0)
|
|
|
|
def test_run_verify_argv_empty(self):
|
|
self.assertEqual(run_verify_argv([], "/tmp"), (0, ""))
|
|
|
|
def test_run_verify_argv_non_str(self):
|
|
with self.assertRaises(TypeError):
|
|
run_verify_argv(["echo", 123], "/tmp")
|
|
|
|
def test_run_gated_verify_blank(self):
|
|
self.assertEqual(run_gated_verify(" ", "/tmp"), (0, ""))
|
|
|
|
def test_build_report_non_dict_task(self):
|
|
report = build_verify_audit_report(
|
|
{"tasks": {"bad": "str",
|
|
"ok": {"verify_command": None}}})
|
|
self.assertEqual([r["task"] for r in report], [])
|
|
|
|
def test_cmd_verify_audit_notes_and_simple(self):
|
|
rows = [
|
|
{"task": "n", "kind": "shell", "features": ["f"],
|
|
"reason": "", "argv": None, "notes": ["note1"],
|
|
"applied": False},
|
|
{"task": "s", "kind": "simple", "features": [],
|
|
"reason": "", "argv": ["true"], "notes": [],
|
|
"applied": False},
|
|
]
|
|
args = SimpleNamespace(apply=False, json=False, state="st.json")
|
|
buf = io.StringIO()
|
|
with mock.patch.object(host, "build_verify_audit_report",
|
|
return_value=rows), \
|
|
contextlib.redirect_stdout(buf):
|
|
_cmd_verify_audit(args, {"tasks": {}})
|
|
out = buf.getvalue()
|
|
self.assertIn("notes=note1", out)
|
|
self.assertIn("[可转 argv=['true']]", out)
|
|
|
|
def test_cmd_verify_audit_json(self):
|
|
args = SimpleNamespace(apply=False, json=True, state="st.json")
|
|
buf = io.StringIO()
|
|
with mock.patch.object(host, "build_verify_audit_report",
|
|
return_value=[]), \
|
|
contextlib.redirect_stdout(buf):
|
|
_cmd_verify_audit(args, {"tasks": {}})
|
|
parsed = json.loads(buf.getvalue())
|
|
self.assertEqual(parsed["applied"], 0)
|
|
self.assertEqual(parsed["tasks"], [])
|
|
|
|
|
|
class NotifyTestCase(unittest.TestCase):
|
|
def _state_path(self, d):
|
|
return os.path.join(d, "state.json")
|
|
|
|
def test_notify_no_command_log_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")):
|
|
msg = notify_failure(self._state_path(d), {}, "t1",
|
|
{"status": "failed", "text": "boom"})
|
|
self.assertTrue(msg.startswith("FAILED t1"))
|
|
|
|
def test_notify_template_error_log_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")):
|
|
msg = notify_failure(
|
|
self._state_path(d), {"notify_command": "echo {unknown}"},
|
|
"t1", {"status": "failed", "text": "boom"})
|
|
self.assertTrue(msg.startswith("FAILED t1"))
|
|
|
|
def test_notify_rc_log_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")):
|
|
msg = notify_failure(
|
|
self._state_path(d), {"notify_command": "true"},
|
|
"t1", {"status": "failed", "text": "boom"})
|
|
self.assertTrue(msg.startswith("FAILED t1"))
|
|
|
|
def test_notify_timeout_log_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")), \
|
|
mock.patch("subprocess.run",
|
|
side_effect=subprocess.TimeoutExpired(
|
|
"cmd", 30)):
|
|
msg = notify_failure(
|
|
self._state_path(d), {"notify_command": "sleep 60"},
|
|
"t1", {"status": "failed", "text": "boom"})
|
|
self.assertTrue(msg.startswith("FAILED t1"))
|
|
|
|
def test_notify_generic_error_log_failure(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with mock.patch.object(host, "log_event",
|
|
side_effect=RuntimeError("log down")), \
|
|
mock.patch("subprocess.run",
|
|
side_effect=RuntimeError("spawn down")):
|
|
msg = notify_failure(
|
|
self._state_path(d), {"notify_command": "true"},
|
|
"t1", {"status": "failed", "text": "boom"})
|
|
self.assertTrue(msg.startswith("FAILED t1"))
|
|
|
|
|
|
class PrDispatchTurnTestCase(unittest.TestCase):
|
|
def test_parse_pr_checks_branches(self):
|
|
checks, all_pass = parse_pr_checks("foo\t\nrandom words\npass\n")
|
|
self.assertEqual(checks, [{"name": "pass", "state": "pass"}])
|
|
self.assertTrue(all_pass)
|
|
|
|
def test_merge_pr_gh_failure(self):
|
|
def _runner(args):
|
|
if args[1] == "checks":
|
|
return SimpleNamespace(returncode=0, stdout="ci\tpass\n",
|
|
stderr="")
|
|
return SimpleNamespace(returncode=1, stdout="",
|
|
stderr="merge failed")
|
|
task = {"name": "t", "pr_number": 7, "pr_url": "u"}
|
|
ok, msg = merge_pr(task, gh_runner=_runner, repo="/tmp")
|
|
self.assertFalse(ok)
|
|
self.assertIn("gh pr merge 失败", msg)
|
|
|
|
def test_run_gh_real_path_mocked(self):
|
|
sentinel = SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
with mock.patch("subprocess.run", return_value=sentinel) as run:
|
|
out = run_gh(["pr", "checks", "1"], cwd="/tmp")
|
|
self.assertIs(out, sentinel)
|
|
run.assert_called_once()
|
|
|
|
def test_copy_tree_skips_pyc(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
src = os.path.join(d, "src")
|
|
dst = os.path.join(d, "dst")
|
|
os.makedirs(src)
|
|
with open(os.path.join(src, "a.pyc"), "wb") as fh:
|
|
fh.write(b"\x00")
|
|
with open(os.path.join(src, "b.txt"), "w") as fh:
|
|
fh.write("b")
|
|
copy_tree_excluding_git(src, dst)
|
|
self.assertFalse(os.path.exists(os.path.join(dst, "a.pyc")))
|
|
self.assertTrue(os.path.exists(os.path.join(dst, "b.txt")))
|
|
|
|
def test_dispatch_relative_repo(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
rel = os.path.join(d, "relrepo")
|
|
os.makedirs(rel)
|
|
server = SimpleNamespace(
|
|
create_thread=lambda cwd, **kw: "th1")
|
|
state = {"tasks": {}}
|
|
with mock.patch.object(host, "HERE", d):
|
|
task = dispatch(server, state, "t1", "prompt", "relrepo",
|
|
"m", "low", None, None, use_worktree=False)
|
|
self.assertEqual(task["repo"], rel)
|
|
self.assertEqual(task["cwd"], rel)
|
|
|
|
def test_run_task_turn_type_error_reraise(self):
|
|
class _Server:
|
|
def run_turn(self, *args, **kwargs):
|
|
raise TypeError("boom")
|
|
task = {"thread_id": "th", "sandbox": None, "turns": []}
|
|
with self.assertRaises(TypeError):
|
|
run_task_turn(_Server(), {"tasks": {}}, task, "p", "m", "low",
|
|
interrupt_file="/tmp/if")
|
|
|
|
def test_seed_worktree_git_skipped(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
seed = os.path.join(d, "seed")
|
|
work = os.path.join(d, "work")
|
|
os.makedirs(os.path.join(seed, ".git"))
|
|
os.makedirs(work)
|
|
with open(os.path.join(seed, "f.txt"), "w") as fh:
|
|
fh.write("f")
|
|
_seed_worktree(work, seed)
|
|
self.assertTrue(os.path.exists(os.path.join(work, "f.txt")))
|
|
self.assertFalse(os.path.exists(os.path.join(work, ".git")))
|
|
|
|
|
|
class FakeServer:
|
|
def run_turn(self, *args, **kwargs):
|
|
return {"status": "completed", "text": ""}
|
|
|
|
|
|
class GoalLoopTestCase(unittest.TestCase):
|
|
def _setup(self, d):
|
|
state_path = os.path.join(d, "state.json")
|
|
task = {"name": "g", "thread_id": "x", "cwd": d,
|
|
"sandbox": None, "turns": []}
|
|
state = {"tasks": {"g": task}, "automations": []}
|
|
return state_path, task, state
|
|
|
|
def test_goal_verify_oserror_interrupt(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
state_path, task, state = self._setup(d)
|
|
out = run_goal_loop(
|
|
FakeServer(), state, state_path, task, "goal", None, 1,
|
|
"m", "low", "workspace-write", cfg={},
|
|
verify_argv=["/nonexistent/binary-xyz"])
|
|
self.assertTrue(out["interrupted"])
|
|
self.assertEqual(task["status"], "interrupted")
|
|
|
|
def test_goal_save_failure_interrupt(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
state_path, task, state = self._setup(d)
|
|
orig = host.save_state
|
|
calls = []
|
|
|
|
def _flaky(path, st):
|
|
calls.append(1)
|
|
if len(calls) > 1:
|
|
raise RuntimeError("save down")
|
|
return orig(path, st)
|
|
with mock.patch.object(host, "save_state",
|
|
side_effect=_flaky):
|
|
out = run_goal_loop(
|
|
FakeServer(), state, state_path, task, "goal", "true",
|
|
1, "m", "low", "workspace-write", cfg={})
|
|
self.assertTrue(out["interrupted"])
|
|
self.assertEqual(task["status"], "interrupted")
|
|
|
|
def test_goal_steps_write_failure_swallowed(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
state_path, task, state = self._setup(d)
|
|
with open(os.path.join(d, "host-steps"), "w") as fh:
|
|
fh.write("blocker")
|
|
out = run_goal_loop(
|
|
FakeServer(), state, state_path, task, "goal", "true",
|
|
1, "m", "low", "workspace-write", cfg={})
|
|
self.assertTrue(out["ok"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|