"""探索清单形状校验纯函数:顺序、身份、类型与来源绑定的确定性拒绝;离线不连库。""" from dataclasses import replace import pytest from muse.上下文.接口 import 上下文错误, 探索条目, 校验清单, 记录探索 from muse.任务运行.接口 import 工具来源, 工具结果 范围 = { "work_id": "work-a", "as_of": 2, "source_ids": ["doc-1", "plan-1", "fact-lin"], "documents": [ { "chapter_id": "ch-1", "branch_id": "main", "revision": 1, "document_id": "doc-1", "document_hash": "hash-doc-1", "position": 1, } ], "directory_revision": 3, "authorized_by": "author-a", "run_purpose": "production", "content_purpose": "generation", "target_plan": { "chapter_id": "ch-3", "plan_id": "plan-1", "revision": 1, "content_hash": "hash-plan-1", }, "facts": {"system_revision": 5}, } 读取结果 = 工具结果( 内容={"chapter_id": "ch-1", "text": "林深停在码头"}, 来源=( 工具来源("doc-1", "1", "hash-doc-1", "visible-text-codepoints/v1:production:generation"), ), ) def 条目(调用ID: str = "call-1", 工具名: str = "read_history_document", 参数: dict | None = None): """用真实读取结果形状构建顺序为 1 的清单条目;覆盖字段由用例自行 replace。""" return 记录探索( (), 调用ID, 工具名, 参数 if 参数 is not None else {"chapter_id": "ch-1", "revision": 1}, 读取结果, )[0] def 顺序清单(*调用: tuple[str, str, dict]) -> tuple[探索条目, ...]: """按调用次序经记录探索构建顺序连续的合法清单。""" 清单: tuple[探索条目, ...] = () for 调用ID, 工具名, 参数 in 调用: 清单 = 记录探索(清单, 调用ID, 工具名, 参数, 读取结果) return 清单 @pytest.mark.case_id( "NC-ctx-replay-manifest-order", environment="离线纯函数", given="顺序不连续的探索清单", when="校验清单", then="CONTEXT_INPUT_INVALID 拒绝,顺序必须连续可核对", contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_顺序断裂拒绝__b7e0b1() -> None: 跳号 = (条目("call-1"), replace(条目("call-2"), 顺序=3)) with pytest.raises(上下文错误) as 异常: 校验清单(跳号, 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" assert "顺序" in 异常.value.说明 倒序 = (replace(条目("call-1"), 顺序=2), replace(条目("call-2"), 顺序=1)) with pytest.raises(上下文错误) as 异常: 校验清单(倒序, 范围) assert "顺序" in 异常.value.说明 @pytest.mark.case_id( "NC-ctx-replay-manifest-duplicate", environment="离线纯函数", given="调用ID重复的探索清单", when="校验清单", then="明确拒绝,不猜测去重", contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_重复调用ID拒绝__b7e0b2() -> None: 重复 = (条目("call-1"), replace(条目("call-1"), 顺序=2)) with pytest.raises(上下文错误) as 异常: 校验清单(重复, 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" assert "call-1" in 异常.value.说明 and "重复" in 异常.value.说明 @pytest.mark.case_id( "NC-ctx-replay-manifest-types", environment="离线纯函数", given="顺序/版本等字段为布尔或类型混淆的清单", when="校验清单", then="明确拒绝,不用宽松类型顶替", contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_布尔与类型混淆拒绝__b7e0b3() -> None: # 顺序是 bool 而不是 int:True 不能冒充 1 with pytest.raises(上下文错误) as 异常: 校验清单((replace(条目("call-1"), 顺序=True),), 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" # 参数里的版本是 bool:JSON Schema 的 integer 不接受布尔 with pytest.raises(上下文错误) as 异常: 校验清单((条目("call-1", 参数={"chapter_id": "ch-1", "revision": True}),), 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" # 条目本身不是探索条目对象:字典形状不能冒充 with pytest.raises(上下文错误) as 异常: 校验清单(({"call_id": "call-1", "tool": "read_history_document"},), 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" @pytest.mark.case_id( "NC-ctx-replay-manifest-binding", environment="离线纯函数", given="缺来源绑定或消费哈希非法的条目", when="校验清单", then="分别明确拒绝", contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_缺来源绑定与非法消费哈希拒绝__b7e0b4() -> None: 缺绑定 = (replace(条目("call-1"), 来源引用=()),) with pytest.raises(上下文错误) as 异常: 校验清单(缺绑定, 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" assert "来源绑定" in 异常.value.说明 非法哈希 = (replace(条目("call-1"), 消费哈希="不是十六进制"),) with pytest.raises(上下文错误) as 异常: 校验清单(非法哈希, 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID" assert "消费哈希" in 异常.value.说明 @pytest.mark.case_id( "NC-ctx-replay-manifest-valid", environment="离线纯函数", given="合法清单与空清单", when="校验清单", then="合法按序通过;空清单拒绝(探索必须非空)", contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_合法清单按序通过且空清单拒绝__b7e0b5() -> None: 清单 = 顺序清单( ("call-1", "query_work_plan", {}), ("call-2", "query_facts_as_of", {}), ("call-3", "read_history_document", {"chapter_id": "ch-1", "revision": 1}), ("call-4", "read_history_document", {"chapter_id": "ch-1", "revision": 1, "end": 6}), ) assert [项.顺序 for 项 in 清单] == [1, 2, 3, 4] 校验清单(清单, 范围) # 不抛异常即通过 with pytest.raises(上下文错误) as 异常: 校验清单((), 范围) assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"