Files
codex-app-server/tests/test_host_read_loop_params.py
host 24adb568da fix(host): _read_loop 跳过非 dict params 等脏消息,不杀死读循环
顶层为 dict 但 params 为真值非 dict 时,旧代码在 params.get 处抛
AttributeError,被外层 except 吞掉、读线程退出,后续正常消息
再也送达不了。改后校验跳过并记 stderr 日志、脏消息不进 events、
读循环继续;None/缺失/假值沿用旧 or {} 语义。审计同函数其它
get 路径:一并守住不可哈希的响应 id(pending.get)与 threadId /
thread.id(turn_queues.get)的 TypeError 致死路径;params.thread
已有 isinstance 守卫、_append_trace 已有 try 包裹,均不动。
2026-11-03 02:49:26 +00:00

145 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""_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()