diff --git a/host.py b/host.py index fa12513..d4318a5 100644 --- a/host.py +++ b/host.py @@ -486,6 +486,20 @@ class Server: msg = json.loads(raw) except Exception: continue + if not isinstance(msg, dict): + # 非 dict 的合法 JSON(如 ""、123、[]、null): + # 旧代码会走到下面的 msg.get("params") 抛 + # AttributeError/TypeError,被外层 except 吞掉 + # 后读线程退出,此后所有 request() 等到超时。 + # 脏消息只跳过、不当正常请求处理,读循环继续。 + try: + sys.stderr.write( + "host _read_loop: skip non-dict message " + f"({type(msg).__name__}): {str(msg)[:200]}\n") + sys.stderr.flush() + except Exception: + pass + continue if "method" in msg and "id" in msg: self._answer_server_request(msg) continue diff --git a/tests/test_host_bugfix_host.py b/tests/test_host_bugfix_host.py new file mode 100644 index 0000000..9311538 --- /dev/null +++ b/tests/test_host_bugfix_host.py @@ -0,0 +1,84 @@ +"""host.py 覆盖率 bugfix 轨(host 分工)回归测试。 + +只覆盖本分工的三项:_read_loop 非 dict 消息、配置/init 的 +open() 关闭、daemon 循环的 --state 透传。不碰其它轨。 +""" +import contextlib +import io +import json +import queue +import sys +import threading +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +import host + + +def _make_read_loop_server(lines, pending_id=7): + """不拉起真实 app-server 的 Server:只给 _read_loop 供料。 + + proc.stdout 直接用字节行列表;_read_loop 对它做 + `for raw in self.proc.stdout`,与真实 PIPE 的逐行迭代 + 语义一致。脏消息后仍能送达正常响应,即证明读循环 + 没有在脏消息处提前退出(旧代码会在第一条非 dict 处 + 抛 AttributeError/TypeError,被外层 except 吞掉后退出)。 + """ + srv = host.Server.__new__(host.Server) + srv.approval = "deny" + srv.pending_lock = threading.Lock() + srv.pending = {pending_id: queue.Queue()} + srv.events_lock = threading.Lock() + srv.events = [] + srv.turn_queues = {} + srv.trace_path = None + srv._trace_lines = 0 + + class FakeProc: + def __init__(self, stdout_lines): + self.stdout = stdout_lines + + srv.proc = FakeProc(lines) + return srv + + +class ReadLoopNonDictTest(unittest.TestCase): + def test_non_dict_json_then_normal_response(self): + lines = [ + b'""\n', # 合法 JSON,值是 str(旧代码致死) + b'"hello"\n', # 合法 JSON,值是 str + b'123\n', # 合法 JSON,值是 int + b'[1,2]\n', # 合法 JSON,值是 list + b'null\n', # 合法 JSON,值是 None + b'true\n', # 合法 JSON,值是 bool + b'not-json{\n', # 畸形 JSON:旧代码本已 continue + b'{"jsonrpc": "2.0", "id": 7, ' + b'"result": {"ok": true}}\n', + ] + srv = _make_read_loop_server(lines) + err = io.StringIO() + with contextlib.redirect_stderr(err): + srv._read_loop() + resp = srv.pending[7].get(timeout=1) + self.assertEqual(resp["result"], {"ok": True}) + # 脏消息没有被当成通知塞进 events,也没有被当成请求处理。 + self.assertEqual(srv.events, []) + self.assertIn("skip non-dict message", err.getvalue()) + + def test_non_dict_json_then_normal_notification(self): + lines = [ + b'""\n', + b'{"jsonrpc": "2.0", "method": "thread/started", ' + b'"params": {"threadId": "t1"}}\n', + ] + srv = _make_read_loop_server(lines) + srv._read_loop() + self.assertEqual(len(srv.events), 1) + self.assertEqual(srv.events[0]["method"], "thread/started") + + +if __name__ == "__main__": + unittest.main()