From 25d48b7aa7c5e2027bdbec6a8beeef3db4f28c73 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Wed, 8 Jul 2026 10:41:15 +0200 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=E5=86=BB=E7=BB=93=20TOOLBOX=20?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E4=B8=BA=E5=8F=AA=E8=AF=BB=20Mappin?= =?UTF-8?q?gProxyType=EF=BC=88CR-H7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/skills/tests/test_toolbox_registry.py | 9 +++++++++ packages/skills/ww_skills/toolbox_registry.py | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/skills/tests/test_toolbox_registry.py b/packages/skills/tests/test_toolbox_registry.py index c05126b..e3a9409 100644 --- a/packages/skills/tests/test_toolbox_registry.py +++ b/packages/skills/tests/test_toolbox_registry.py @@ -7,6 +7,7 @@ from __future__ import annotations +import pytest from ww_agents import SPECS from ww_skills import TOOLBOX, get_tool @@ -105,3 +106,11 @@ def test_get_tool_resolves_known_and_unknown() -> None: assert get_tool("brainstorm") is TOOLBOX["brainstorm"] assert get_tool("worldbuilding") is TOOLBOX["worldbuilding"] assert get_tool("does-not-exist") is None + + +def test_toolbox_registry_is_read_only() -> None: + # CR-H7:注册表是单一真相源,运行时变异(注入/删除)须 fail-fast(冻结为 MappingProxyType)。 + with pytest.raises(TypeError): + TOOLBOX["injected"] = TOOLBOX["brainstorm"] # type: ignore[index] + with pytest.raises(TypeError): + del TOOLBOX["brainstorm"] # type: ignore[attr-defined] diff --git a/packages/skills/ww_skills/toolbox_registry.py b/packages/skills/ww_skills/toolbox_registry.py index 534a441..05a07c0 100644 --- a/packages/skills/ww_skills/toolbox_registry.py +++ b/packages/skills/ww_skills/toolbox_registry.py @@ -16,6 +16,10 @@ from __future__ import annotations +from collections.abc import Mapping +from types import MappingProxyType +from typing import Final + from ww_agents import SPECS from ww_agents.schemas import ( BlurbResult, @@ -67,7 +71,7 @@ _SOURCE_TEXT_FIELD = InputField( # 创作工具箱注册表:key → 描述符。15 条(legacy 3 + 新 8 + Scope B 4:续写/扩写/降AI/拆书)。 -TOOLBOX: dict[str, GeneratorTool] = { +_TOOLBOX_BY_KEY: dict[str, GeneratorTool] = { # ---- legacy(spec=None,前端走 legacy_route 跳现有页面)---- "worldbuilding": GeneratorTool( key="worldbuilding", @@ -243,6 +247,10 @@ TOOLBOX: dict[str, GeneratorTool] = { ), } +# MappingProxyType:只读视图,运行时 `TOOLBOX[x]=...` / `del TOOLBOX[x]` 抛 TypeError(单一 +# 真相源不可被污染;`Final` 仅静态检查、挡不住运行时变异,故须冻结成只读 Mapping)。 +TOOLBOX: Final[Mapping[str, GeneratorTool]] = MappingProxyType(_TOOLBOX_BY_KEY) + def get_tool(key: str) -> GeneratorTool | None: """按 key 取生成器描述符;未知 key → None(端点据此返 404)。"""