# ============================================================ # Explore 与 Runtime 数据边界黄金测试(plan.md §5.1 / 矩阵 55 行) # 覆盖:主干只读视图写保护(fail closed)、统一沙盒通道隔离、 # Explore 工具无法取得主干写能力(越权与污染测试)。 # ============================================================ from __future__ import annotations import json from server.aps_domain.explore_boundary import readonly_view, run_explore from server.state.seed import seed_world def _world(): w = seed_world() w["salesOrders"].append({"id": "so-1", "orderNo": "SO-1"}) return w def test_readonly_view_blocks_top_level_write(): """主干只读视图:尝试写顶层键必须抛 PermissionError(fail closed)。""" world = _world() view = readonly_view(world) for op in ( lambda v: v.__setitem__("salesOrders", []), lambda v: v.__delitem__("salesOrders"), lambda v: v.setdefault("x", 1), lambda v: v.update({"y": 2}), lambda v: v.clear(), ): try: op(view) raise AssertionError("应抛 PermissionError") except PermissionError: pass # 主干未被污染 assert world["salesOrders"], "只读视图不应能改动主干" def test_readonly_view_blocks_nested_write(): """嵌套容器:列表 append / dict set 同样被拦截(递归只读视图)。""" world = _world() view = readonly_view(world) orders = view["salesOrders"] # 应返回只读列表 n_before = len(world["salesOrders"]) try: orders.append({"id": "hack"}) raise AssertionError("嵌套写应抛 PermissionError") except PermissionError: pass assert len(world["salesOrders"]) == n_before, "嵌套写不应影响主干" def test_run_explore_sandbox_isolates_world(): """统一通道:fn 拿到的沙盒可写,但主干完全不改变(深拷贝隔离)。""" world = _world() before = json.dumps(world, ensure_ascii=False, sort_keys=True) before_count = len(world["salesOrders"]) def explorer(sandbox): sandbox["salesOrders"].append({"id": "sandbox-only"}) sandbox["newKey"] = "explore" return len(sandbox["salesOrders"]) result = run_explore(world, explorer) assert result == len(world["salesOrders"]) + 1 # 沙盒内多 1 条 assert len(world["salesOrders"]) == before_count # 主干不变 assert "newKey" not in world assert json.dumps(world, ensure_ascii=False, sort_keys=True) == before def test_run_explore_nested_mutation_isolated(): """嵌套变异(列表内 dict 修改)在沙盒内生效、主干不变。""" world = _world() def explorer(sandbox): sandbox["salesOrders"][0]["orderNo"] = "CHANGED" sandbox["salesOrders"][0]["extra"] = True return sandbox["salesOrders"][0]["orderNo"] result = run_explore(world, explorer) assert result == "CHANGED" assert any(o["orderNo"] == "SO-1" for o in world["salesOrders"]), "主干 orderNo 不变" assert all("extra" not in o for o in world["salesOrders"]), "主干无额外字段" def test_explore_tool_cannot_gain_main_write(): """越权测试:Explore 工具即使拿到 view 也无法绕过只读边界写主干。""" world = _world() view = readonly_view(world) try: view["salesOrders"] = [] # 工具尝试直接替换主干表 raise AssertionError("应抛 PermissionError") except PermissionError: pass assert world["salesOrders"], "主干写能力必须被切断"