diff --git a/tests/unit/test_structured.py b/tests/unit/test_structured.py index 889da54..2b4df28 100644 --- a/tests/unit/test_structured.py +++ b/tests/unit/test_structured.py @@ -177,3 +177,39 @@ class TestNativeOverlayFirstAttempt: resp = await _mw()(ChatRequest(messages=_MSGS, structured=Verdict), terminal) with pytest.raises(dataclasses.FrozenInstanceError): resp.structured_data = None + + +class TestSamplingSnapshotInvariant: + """地基不变式: `sampling` 跨洋葱层恒定,`overlay` 会被结构化注入(issue #4)。 + + 缓存 key(决策 C)与三个遥测入口(决策 D)都建立在这条之上,而它此前只靠 + "dataclasses.replace 恰好保留未提及字段"的约定成立,无任何机械执法。 + 这个测试是那份执法——它红了就意味着两个决策同时失效。 + """ + + async def test_sampling_survives_feedback_ladder_while_overlay_diverges(self): + caller_sampling = {"temperature": 0, "seed": 42} + # 先坏后好,强制走一次带反馈重问(重问会 replace messages) + terminal = ScriptedTerminal(["not json at all", '{"answer": 1, "reason": "r"}']) + mw = _mw(strategy=NativeSchemaStrategy(), max_retries=1) + await mw( + ChatRequest(messages=_MSGS, structured=Verdict, sampling=caller_sampling), + terminal, + ) + assert len(terminal.requests) == 2 # 确实重问过 + for seen in terminal.requests: + # ① 跨层恒定: 每次尝试看到的 sampling 与调用方传入的逐字相同 + assert seen.sampling == caller_sampling + # ② 确实分叉: 同一时刻 overlay 已被注入 response_format + assert seen.overlay["response_format"]["type"] == "json_schema" + assert "response_format" not in seen.sampling + + async def test_middleware_does_not_mutate_caller_mapping(self): + """决策 E 的第二条约束: 中间件只能 replace 派生,不得就地改这两个 dict。""" + caller_sampling = {"seed": 7} + terminal = ScriptedTerminal(['{"answer": 1, "reason": "r"}']) + await _mw(strategy=NativeSchemaStrategy())( + ChatRequest(messages=_MSGS, structured=Verdict, sampling=caller_sampling), + terminal, + ) + assert caller_sampling == {"seed": 7} # 调用方的对象未被污染