- 压缩崩根因(venv 2.0.2 代码级证据,4 次实证 'important_discoveries' required):reply 每轮推理前 compress_context(_agent.py:609)→ generate_structured_output 用 SummarySchema json-schema(五字段全 required+maxLength,_config.py:11-53)强制 M3 tool_choice 产摘要 → dict 路 jsonschema.validate (model/_base.py:566-567)对缺字段/超长抛 ValidationError → 非 retryable 直接 raise(_base.py:426-428) → _compress_context_impl 非 overflow 支 raise e from None(_agent.py:461)→ 穿透 reply;ChatService.run 吞异常不发终结事件(app/_service/_chat.py:166-176)→ driver 600s 慢失败 - ctx_compress_patch:fail-open 包 Agent.compress_context(压缩=旁路优化件,失败只跳过不杀主链;连续 2 次 断路防每轮白烧压缩计费调用;钉 2.0.2 不改 venv 本体,纪律同 m3_stream_patch);Service 与 CLI 两处 apply - collector on_reply 增崩溃支:先 flush 收口采集,再 yield 合成 ReplyEndEvent(消费侧先 publish 到 bus → driver 立刻收到回合终结按盘面快速失败),原异常原样重抛不掩盖;正常路零行为变化 - 测试 9 用例:成功透传/失败吞掉/2 次断路/成功清零/按实例隔离/apply 幂等;崩溃合成终结+异常上抛+崩溃路 flush/正常路不追加/合成兜底不遮原异常 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""test_ctx_compress_patch.py — 上下文压缩 fail-open 补丁单测(T5 压缩崩封口)。
|
|
|
|
守的不变量:
|
|
· 压缩成功:透传返回值、连续失败计数清零。
|
|
· 压缩抛异常(实证形态 jsonschema ValidationError 'important_discoveries' required):吞掉不上抛
|
|
(生成主链继续),计连续失败。
|
|
· 连续失败 ≥2 次:本 agent 实例断路禁用压缩(不再调原函数——压缩调用计费,防每轮白烧)。
|
|
· apply_ctx_compress_patch:幂等(重复 apply 不套第二层);patch 后真 Agent.compress_context 带补丁标记。
|
|
|
|
跑:cheap-worker/.venv/bin/python -m pytest cheap-worker/tests/test_ctx_compress_patch.py -v
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
|
import _bootstrap # noqa: E402,F401
|
|
import ctx_compress_patch as P # noqa: E402
|
|
|
|
|
|
class _FakeAgent:
|
|
"""普通对象即可(补丁把失败状态挂实例属性,与真 Agent 同为普通类)。"""
|
|
|
|
|
|
def _run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
# ───────────────────────── 包装器行为 ─────────────────────────
|
|
|
|
def test_success_passthrough_and_reset():
|
|
calls = []
|
|
|
|
async def ok_orig(self, context_config=None):
|
|
calls.append(context_config)
|
|
return "COMPRESSED"
|
|
|
|
wrapped = P.make_failopen_compress(ok_orig)
|
|
a = _FakeAgent()
|
|
assert _run(wrapped(a, context_config="CFG")) == "COMPRESSED"
|
|
assert calls == ["CFG"]
|
|
assert getattr(a, P._STATE_ATTR)["fails"] == 0
|
|
|
|
|
|
def test_failure_swallowed_not_raised():
|
|
"""实证崩因形态:结构化输出校验失败 → fail-open 吞掉,生成主链不被杀。"""
|
|
async def bad_orig(self, context_config=None):
|
|
raise ValueError("'important_discoveries' is a required property")
|
|
|
|
wrapped = P.make_failopen_compress(bad_orig)
|
|
a = _FakeAgent()
|
|
assert _run(wrapped(a)) is None # 不抛
|
|
st = getattr(a, P._STATE_ATTR)
|
|
assert st["fails"] == 1 and st["disabled"] is False
|
|
|
|
|
|
def test_two_consecutive_failures_open_circuit():
|
|
"""连续 2 次失败 → 断路:第 3 次起不再调原函数(压缩调用计费,断路防白烧)。"""
|
|
call_count = {"n": 0}
|
|
|
|
async def bad_orig(self, context_config=None):
|
|
call_count["n"] += 1
|
|
raise RuntimeError("compress boom")
|
|
|
|
wrapped = P.make_failopen_compress(bad_orig)
|
|
a = _FakeAgent()
|
|
_run(wrapped(a))
|
|
_run(wrapped(a))
|
|
st = getattr(a, P._STATE_ATTR)
|
|
assert st["disabled"] is True and st["fails"] == P.FAIL_DISABLE_THRESHOLD
|
|
_run(wrapped(a)) # 断路后
|
|
assert call_count["n"] == 2, "断路后不得再调原压缩(不再烧压缩调用)"
|
|
|
|
|
|
def test_success_resets_consecutive_counter():
|
|
"""失败→成功→失败:成功清零,断路器只数「连续」失败(单次抖动不致禁用)。"""
|
|
behavior = ["fail", "ok", "fail"]
|
|
|
|
async def flaky_orig(self, context_config=None):
|
|
b = behavior.pop(0)
|
|
if b == "fail":
|
|
raise RuntimeError("boom")
|
|
return "OK"
|
|
|
|
wrapped = P.make_failopen_compress(flaky_orig)
|
|
a = _FakeAgent()
|
|
_run(wrapped(a))
|
|
assert getattr(a, P._STATE_ATTR)["fails"] == 1
|
|
_run(wrapped(a))
|
|
assert getattr(a, P._STATE_ATTR)["fails"] == 0
|
|
_run(wrapped(a))
|
|
st = getattr(a, P._STATE_ATTR)
|
|
assert st["fails"] == 1 and st["disabled"] is False
|
|
|
|
|
|
def test_per_agent_isolation():
|
|
"""断路状态按 agent 实例隔离:一个实例断路不连累另一个。"""
|
|
async def bad_orig(self, context_config=None):
|
|
raise RuntimeError("boom")
|
|
|
|
wrapped = P.make_failopen_compress(bad_orig)
|
|
a1, a2 = _FakeAgent(), _FakeAgent()
|
|
_run(wrapped(a1))
|
|
_run(wrapped(a1))
|
|
assert getattr(a1, P._STATE_ATTR)["disabled"] is True
|
|
_run(wrapped(a2))
|
|
assert getattr(a2, P._STATE_ATTR)["disabled"] is False
|
|
|
|
|
|
# ───────────────────────── apply 到真 Agent(venv 2.0.2)─────────────────────────
|
|
|
|
def test_apply_patches_real_agent_and_idempotent():
|
|
from worker import config # noqa: F401 先走项目 import 链(代理旁路)再摸 agentscope
|
|
from agentscope.agent import Agent
|
|
|
|
first = P.apply_ctx_compress_patch()
|
|
assert getattr(Agent.compress_context, P._PATCH_FLAG, False) is True, "patch 后方法应带补丁标记"
|
|
second = P.apply_ctx_compress_patch()
|
|
assert second is False, "重复 apply 必须幂等(不套第二层)"
|
|
# first 可能为 False(同进程其他测试/模块已 apply 过,如 import cheap_studio),幂等语义下均合法。
|
|
assert first in (True, False)
|
|
assert hasattr(Agent.compress_context, "_ctx_compress_orig"), "应留原方法引用供诊断"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
|
_failed = 0
|
|
for _fn in _fns:
|
|
try:
|
|
_fn()
|
|
print(f" PASS {_fn.__name__}")
|
|
except Exception as e: # noqa: BLE001
|
|
_failed += 1
|
|
print(f" FAIL {_fn.__name__}: {type(e).__name__}: {e}")
|
|
print(f"\n{len(_fns) - _failed}/{len(_fns)} passed")
|
|
sys.exit(1 if _failed else 0)
|