test: 钉住 deep_merge 的 None 替换与列表替换语义 + 只读约定注释

- characterization:显式 None 整体替换且兄弟键保留默认(Task 9 回退契约依赖);列表整体替换不与默认拼接
- DEFAULT_CONFIG 与 TenantConfig.data/Site.brand 补只读约定注释
- platform 包依赖章程改明确:defaults/tenant_config 为叶子模块供 engine 反向消费,registry 组装层可 import app/engine

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 16:59:31 +08:00
parent 89339868db
commit 8a16a2959b
4 changed files with 27 additions and 4 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
"""平台层(多租户):出厂默认、配置合并、租户/站点模型、注册表等。 """平台层(多租户):出厂默认、配置合并、租户/站点模型、注册表等。
Phase 1 逐步迁入;本包不得反向依赖 app/engine 之外的假设。""" Phase 1 逐步迁入。依赖章程:defaults 与 tenant_config 保持叶子模块(不依赖
app/engine),供 engine 侧反向消费;registry 等组装层可以 import app/engine。"""
+2 -1
View File
@@ -1,5 +1,6 @@
"""出厂默认配置。结构 = spec §5.1 的 9 个配置组。 """出厂默认配置。结构 = spec §5.1 的 9 个配置组。
本阶段 brand/wall 的默认值由 Task 4 从 jc 代码提取填充。""" 本阶段 brand/wall 的默认值由 Task 4 从 jc 代码提取填充。
只读约定:消费者不得修改本 dict,需要定制走 deep_merge 拷贝。"""
import copy import copy
DEFAULT_CONFIG: dict = { DEFAULT_CONFIG: dict = {
+4 -2
View File
@@ -14,7 +14,8 @@ class Site:
slug: str slug: str
name: str name: str
domains: tuple[str, ...] domains: tuple[str, ...]
brand: dict # 站点门面覆盖层 ⊕ 租户默认 brand(已深合并) brand: dict # 站点门面覆盖层 ⊕ 租户默认 brand(已深合并)。
# 只读约定:构造后不得修改
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -22,7 +23,8 @@ class TenantConfig:
slug: str slug: str
name: str name: str
status: str # active | disabled status: str # active | disabled
data: dict # deep_merge(DEFAULT_CONFIG, snapshot) data: dict # deep_merge(DEFAULT_CONFIG, snapshot)。
# 只读约定:构造后不得修改
sites: tuple[Site, ...] = () sites: tuple[Site, ...] = ()
@classmethod @classmethod
+19
View File
@@ -66,3 +66,22 @@ def test_tenant_without_sites():
assert tenant.status == "active" assert tenant.status == "active"
assert tenant.data["ai"]["model"] == "glm-4-plus" # 覆盖生效 assert tenant.data["ai"]["model"] == "glm-4-plus" # 覆盖生效
assert tenant.data["push"]["enabled"] is False # 缺省补齐 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"]