"""host.py goal-resume 断点丢失守卫:state 里没有任务时拒绝不炸。 2026-10-29 长任务实弹:断点续跑在 state 里找不到任务时抛 KeyError traceback,worker 把无结论异常退出判成 interrupted, 自动恢复再排队、再 KeyError,12 秒一轮空转到 50 次上限。 守卫后:打印拒绝行、正常返回,state 文件不动、线程不恢复。 """ import io import json import os import sys import tempfile import unittest from contextlib import redirect_stdout from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT)) import host class FakeServer(object): def __init__(self): self.resumed = [] def initialize(self): pass def close(self): pass def resume_thread(self, thread_id): self.resumed.append(thread_id) class GoalResumeMissingTest(unittest.TestCase): def test_missing_task_refuses_without_traceback(self): with tempfile.TemporaryDirectory() as tmp: 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": {"g": {"name": "g", "thread_id": "x", "status": "goal-running"}}, "automations": []}, f) with open(cfg_path, "w", encoding="utf-8") as f: json.dump({}, f) fake = FakeServer() argv = ["host.py", "--state", state_path, "--config", cfg_path, "goal-resume", "--task", "missing-task"] out = io.StringIO() with mock.patch.object(sys, "argv", argv): with mock.patch.object(host, "Server", lambda **kw: fake): with redirect_stdout(out): host.main() self.assertIn("goal-resume 拒绝", out.getvalue()) self.assertIn("missing-task", out.getvalue()) self.assertEqual(fake.resumed, []) with open(state_path, encoding="utf-8") as f: saved = json.load(f) self.assertEqual(list(saved["tasks"].keys()), ["g"])