From 8a16a2959b2ca815bf9337cce9035ff512b57bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Mon, 12 Oct 2026 16:59:31 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E9=92=89=E4=BD=8F=20deep=5Fmerge=20?= =?UTF-8?q?=E7=9A=84=20None=20=E6=9B=BF=E6=8D=A2=E4=B8=8E=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=9B=BF=E6=8D=A2=E8=AF=AD=E4=B9=89=20+=20=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E7=BA=A6=E5=AE=9A=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - characterization:显式 None 整体替换且兄弟键保留默认(Task 9 回退契约依赖);列表整体替换不与默认拼接 - DEFAULT_CONFIG 与 TenantConfig.data/Site.brand 补只读约定注释 - platform 包依赖章程改明确:defaults/tenant_config 为叶子模块供 engine 反向消费,registry 组装层可 import app/engine Co-Authored-By: Claude Code --- app/platform/__init__.py | 3 ++- app/platform/defaults.py | 3 ++- app/platform/tenant_config.py | 6 ++++-- tests/test_config_merge.py | 19 +++++++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/platform/__init__.py b/app/platform/__init__.py index d43e6f7..d4b8678 100644 --- a/app/platform/__init__.py +++ b/app/platform/__init__.py @@ -1,2 +1,3 @@ """平台层(多租户):出厂默认、配置合并、租户/站点模型、注册表等。 -Phase 1 逐步迁入;本包不得反向依赖 app/engine 之外的假设。""" +Phase 1 逐步迁入。依赖章程:defaults 与 tenant_config 保持叶子模块(不依赖 +app/engine),供 engine 侧反向消费;registry 等组装层可以 import app/engine。""" diff --git a/app/platform/defaults.py b/app/platform/defaults.py index 1de5ce0..d877f0f 100644 --- a/app/platform/defaults.py +++ b/app/platform/defaults.py @@ -1,5 +1,6 @@ """出厂默认配置。结构 = spec §5.1 的 9 个配置组。 -本阶段 brand/wall 的默认值由 Task 4 从 jc 代码提取填充。""" +本阶段 brand/wall 的默认值由 Task 4 从 jc 代码提取填充。 +只读约定:消费者不得修改本 dict,需要定制走 deep_merge 拷贝。""" import copy DEFAULT_CONFIG: dict = { diff --git a/app/platform/tenant_config.py b/app/platform/tenant_config.py index 12a81f4..d052e55 100644 --- a/app/platform/tenant_config.py +++ b/app/platform/tenant_config.py @@ -14,7 +14,8 @@ class Site: slug: str name: str domains: tuple[str, ...] - brand: dict # 站点门面覆盖层 ⊕ 租户默认 brand(已深合并) + brand: dict # 站点门面覆盖层 ⊕ 租户默认 brand(已深合并)。 + # 只读约定:构造后不得修改 @dataclass(frozen=True) @@ -22,7 +23,8 @@ class TenantConfig: slug: str name: str status: str # active | disabled - data: dict # deep_merge(DEFAULT_CONFIG, snapshot) + data: dict # deep_merge(DEFAULT_CONFIG, snapshot)。 + # 只读约定:构造后不得修改 sites: tuple[Site, ...] = () @classmethod diff --git a/tests/test_config_merge.py b/tests/test_config_merge.py index 2f508f5..070fbe6 100644 --- a/tests/test_config_merge.py +++ b/tests/test_config_merge.py @@ -66,3 +66,22 @@ def test_tenant_without_sites(): assert tenant.status == "active" assert tenant.data["ai"]["model"] == "glm-4-plus" # 覆盖生效 assert tenant.data["push"]["enabled"] is False # 缺省补齐 + + +# ---------- characterization:None / 列表整体替换语义 ---------- + +def test_explicit_none_replaces_and_keeps_sibling(): + """显式 None 整体替换该键;兄弟键保留默认。 + + Task 9 的回退契约依赖此语义:快照里显式写 null 表示"清空/未设置", + 不能被解读为"回落默认值"。 + """ + merged = deep_merge(DEFAULT_CONFIG, {"ai": {"api_key": None}}) + assert merged["ai"]["api_key"] is None # 显式 None 生效 + assert merged["ai"]["model"] == "glm-4-flash" # 兄弟键保留默认 + + +def test_list_replaced_wholesale(): + """列表整体替换,不与默认值拼接。""" + merged = deep_merge(DEFAULT_CONFIG, {"email": {"notify_emails": ["x@y.z"]}}) + assert merged["email"]["notify_emails"] == ["x@y.z"]