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"]