Files
chongming-core/test/test_stream_worker.py
T
2026-08-23 05:10:27 +08:00

119 lines
3.2 KiB
Python
Raw 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.
"""
Worker 框架流式回复单测(不依赖真实 zenoh 会话)
直接测 runtime.stream_replies_to_query(fake query 仅需 key_expr 与 reply):
1. 异步生成器:多块顺序 reply,payload 为 orjson JSON
2. 同步生成器:同样逐块 reply
3. 生成器中途异常:{"error": ...} 收尾块后停流
4. reply 失败(查询已终止):立即停流且不抛异常
运行: uv run python test_stream_worker.py
"""
import asyncio
import sys
from pathlib import Path
import orjson
REPO_ROOT = Path(__file__).resolve().parent
# 优先使用本地框架 checkout(即使 chongming_worker 已通过 uv 安装也以本地为准)
sys.path.insert(0, str(REPO_ROOT / "utils" / "worker" / "src"))
from chongming_worker.runtime import stream_replies_to_query # noqa: E402
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
class FakeQuery:
"""仅实现 stream_replies_to_query 用到的两个接口(key_expr / reply)"""
def __init__(self, key_expr="worker/example/worker-0/example.stream_chat"):
self.key_expr = key_expr
self.replies = []
self.fail_reply = False
def reply(self, key_expr, payload):
if self.fail_reply:
raise RuntimeError("query terminated")
self.replies.append((key_expr, payload))
def test_async_generator():
q = FakeQuery()
async def gen():
yield {"token": "a"}
yield {"token": "b"}
asyncio.run(stream_replies_to_query(q, gen()))
check("异步生成器逐块回复", len(q.replies) == 2)
check("回复 key_expr 正确", all(k == q.key_expr for k, _ in q.replies))
check(
"payload 为 JSON dict",
[orjson.loads(p) for _, p in q.replies] == [{"token": "a"}, {"token": "b"}],
)
def test_sync_generator():
q = FakeQuery()
def gen():
yield {"n": 1}
yield {"n": 2}
asyncio.run(stream_replies_to_query(q, gen()))
check(
"同步生成器逐块回复",
[orjson.loads(p) for _, p in q.replies] == [{"n": 1}, {"n": 2}],
)
def test_mid_stream_exception():
q = FakeQuery()
async def gen():
yield {"token": "a"}
raise ValueError("boom")
asyncio.run(stream_replies_to_query(q, gen()))
check("中途异常收尾 error 块", len(q.replies) == 2)
check("error 块内容", orjson.loads(q.replies[1][1]) == {"error": "boom"})
def test_reply_failure_stops():
q = FakeQuery()
q.fail_reply = True
yielded = []
async def gen():
yielded.append("yield-1")
yield {"token": "a"}
yielded.append("yield-2") # 首个块 reply 失败后不应被执行到
yield {"token": "b"}
# 不应抛异常,且首个块 reply 失败后立即停流
asyncio.run(stream_replies_to_query(q, gen()))
check("reply 失败不抛异常", True)
check("reply 失败后立即停流", yielded == ["yield-1"])
def run():
test_async_generator()
test_sync_generator()
test_mid_stream_exception()
test_reply_failure_stops()
print(f"\n结果: {len(failures)} 个失败")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(run())