"""_read_loop 脏消息回归测试(残留项 1:params 非 dict 等)。 不启动 app-server:Server 实例用 object.__new__ 绕开 __init__ (__init__ 会拉起 codex 子进程),直接同步跑 _read_loop 消费 伪造的 stdout 行。改前行为:真值非 dict 的 params 在 params.get 处抛 AttributeError,被外层 except 吞掉、读循环退出,脏消息后 的正常消息再也送达不了;不可哈希的 id/threadId 同理在 pending.get / turn_queues.get 处抛 TypeError 杀死读循环。 范围说明:顶层非 dict 消息("method" in msg 等路径)是上一轨 #266 的范围,不在本文件覆盖;本文件只覆盖顶层为 dict 的情形。 """ import contextlib import io import json import queue import sys import threading import unittest from pathlib import Path try: import host from host import Server except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import host from host import Server class FakeProc: def __init__(self, lines): self.stdout = lines self.stdin = io.BytesIO() def _normal_notification(): return {"method": "item/completed", "params": {"threadId": "th-1", "item": {"type": "agentMessage", "id": "m1", "text": "done"}}} class ReadLoopParamsTestCase(unittest.TestCase): def _run(self, lines): srv = object.__new__(Server) srv.proc = FakeProc([json.dumps(m) for m in lines]) srv.pending = {} srv.pending_lock = threading.Lock() srv.turn_queues = {} srv.events = [] srv.events_lock = threading.Lock() srv.trace_path = None srv._trace_lines = 0 err = io.StringIO() with contextlib.redirect_stderr(err): srv._read_loop() return srv, err.getvalue() def test_non_dict_params_skipped_and_loop_continues(self): # 真值非 dict 的 params 各型:脏消息不进 events,读循环 # 继续、脏消息后的正常通知仍送达。 for dirty_params in ("foo", 123, [1, 2], True, 1.5): with self.subTest(params=dirty_params): dirty = {"method": "item/completed", "params": dirty_params} srv, err = self._run([dirty, _normal_notification()]) self.assertEqual(srv.events, [_normal_notification()]) self.assertIn("non-dict params", err) def test_null_missing_empty_params_preserved(self): # None/缺失/空 dict 沿用旧的 `or {}` 语义:不属致死脏消息, # 事件仍照常追加(改前改后行为一致,不得以“跳过”丢通知)。 for dirty in ({"method": "item/completed", "params": None}, {"method": "item/completed"}, {"method": "item/completed", "params": {}}): with self.subTest(dirty=dirty): srv, _err = self._run([dirty, _normal_notification()]) self.assertEqual(srv.events, [dirty, _normal_notification()]) def test_unhashable_thread_id_keeps_event_and_loop_continues(self): # threadId / thread.id 不可哈希时:事件本身仍保留(params # 是合法 dict),只跳过 turn 路由,读循环继续。 cases = [ {"method": "turn/completed", "params": {"threadId": ["not", "a", "str"]}}, {"method": "turn/completed", "params": {"thread": {"id": {"x": 1}}}}, ] for dirty in cases: with self.subTest(dirty=dirty): srv, err = self._run([dirty, _normal_notification()]) self.assertEqual( srv.events, [dirty, _normal_notification()]) self.assertIn("unhashable thread_id", err) def test_valid_turn_completed_still_routed(self): # 正常语义不动:str threadId 的 turn/completed 仍进 turn 队列。 srv = object.__new__(Server) turn_q = queue.Queue() done = {"method": "turn/completed", "params": {"threadId": "th-1"}} srv.proc = FakeProc([json.dumps(done)]) srv.pending = {} srv.pending_lock = threading.Lock() srv.turn_queues = {"th-1": turn_q} srv.events = [] srv.events_lock = threading.Lock() srv.trace_path = None srv._trace_lines = 0 srv._read_loop() self.assertEqual(srv.events, [done]) self.assertEqual(turn_q.get_nowait(), done) def test_unhashable_response_id_skipped_and_pending_delivered(self): # 响应 id 不可哈希时:该条跳过,之后 id 正常的响应仍送达 # pending 队列(pending key 全是 int,list/dict id 本就 # 不可能命中)。 for bad_id in (["unhashable"], {"x": 1}): with self.subTest(bad_id=bad_id): srv = object.__new__(Server) pend_q = queue.Queue() resp = {"id": 7, "result": {"ok": True}} srv.proc = FakeProc([json.dumps({"id": bad_id, "result": {}}), json.dumps(resp)]) srv.pending = {7: pend_q} srv.pending_lock = threading.Lock() srv.turn_queues = {} srv.events = [] srv.events_lock = threading.Lock() srv.trace_path = None srv._trace_lines = 0 err = io.StringIO() with contextlib.redirect_stderr(err): srv._read_loop() self.assertEqual(pend_q.get_nowait(), resp) self.assertEqual(srv.events, []) self.assertIn("unhashable id", err.getvalue()) if __name__ == "__main__": unittest.main()