fix(host): _read_loop 跳过非 dict JSON 消息,不再静默死亡

收到合法 JSON 但值不是 dict(如 ""、123、[]、null)时,
旧代码走到 msg.get('params') 抛 AttributeError/TypeError,
被外层 except 吞掉后读线程退出,此后所有 request() 等到超时。
现校验后记 stderr 日志并跳过,读循环继续;脏消息不进
events、不当正常请求处理。畸形 JSON 本已由 json.loads 的
except continue 处理,本次未改动该路径。
回归:tests/test_host_bugfix_host.py ReadLoopNonDictTest(2 项)
This commit is contained in:
muse authored and host committed 2026-11-03 02:49:25 +00:00
1 parent b9466e556b
commit e768959c93
2 files changed
+98

No files matched your search

+84
View File
@@ -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()