97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
# ============================================================
|
||
# 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"], "主干写能力必须被切断"
|