2026-10-12 16:44:49 +08:00
|
|
|
|
"""出厂默认与快照深合并(E4 配置演进)——Task 3 TDD 用例。
|
|
|
|
|
|
|
|
|
|
|
|
覆盖:
|
|
|
|
|
|
- deep_merge 三性质:缺省补齐 / 不动入参 / 兄弟键保留
|
|
|
|
|
|
- TenantConfig.from_snapshot 的站点模型:覆盖生效、未覆盖项继承租户级 brand、
|
|
|
|
|
|
无 sites 时空元组
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.platform.defaults import DEFAULT_CONFIG, deep_merge
|
|
|
|
|
|
from app.platform.tenant_config import Site, TenantConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- deep_merge 三性质 ----------
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_keys_filled_from_defaults():
|
|
|
|
|
|
snapshot = {"brand": {"title": "X"}} # 快照缺一大堆键
|
|
|
|
|
|
merged = deep_merge(DEFAULT_CONFIG, snapshot)
|
|
|
|
|
|
assert merged["brand"]["title"] == "X" # 覆盖生效
|
|
|
|
|
|
assert "color" in merged["brand"] # 缺省补齐
|
|
|
|
|
|
assert "wall" in merged and "push" in merged # 顶层组补齐
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_snapshot_not_mutated():
|
|
|
|
|
|
snapshot = {"brand": {"title": "X"}}
|
|
|
|
|
|
deep_merge(DEFAULT_CONFIG, snapshot)
|
|
|
|
|
|
assert snapshot == {"brand": {"title": "X"}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_nested_override_keeps_sibling():
|
|
|
|
|
|
snapshot = {"email": {"sender_name": "【T】"}}
|
|
|
|
|
|
merged = deep_merge(DEFAULT_CONFIG, snapshot)
|
|
|
|
|
|
assert merged["email"]["sender_name"] == "【T】"
|
|
|
|
|
|
assert "notify_emails" in merged["email"] # 兄弟键保留默认
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- TenantConfig / Site 站点模型 ----------
|
|
|
|
|
|
|
|
|
|
|
|
def test_site_overrides_and_inherits_tenant_brand():
|
|
|
|
|
|
"""站点覆盖项生效;未覆盖项继承租户级 brand(而非出厂默认)。"""
|
|
|
|
|
|
tenant = TenantConfig.from_snapshot(
|
|
|
|
|
|
slug="t1",
|
|
|
|
|
|
name="Tenant 1",
|
|
|
|
|
|
snapshot={"brand": {"title": "T Co", "welcome": "hi from tenant"}},
|
|
|
|
|
|
sites=[
|
|
|
|
|
|
{"slug": "s1", "name": "Site 1", "domains": ["a.com"],
|
|
|
|
|
|
"brand": {"color": "#ff0000"}},
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
site = tenant.sites[0]
|
|
|
|
|
|
assert isinstance(site, Site)
|
|
|
|
|
|
assert site.slug == "s1"
|
|
|
|
|
|
assert site.name == "Site 1"
|
|
|
|
|
|
assert site.domains == ("a.com",)
|
|
|
|
|
|
assert site.brand["color"] == "#ff0000" # 站点覆盖生效
|
|
|
|
|
|
assert site.brand["title"] == "T Co" # 继承租户级 brand
|
|
|
|
|
|
assert site.brand["welcome"] == "hi from tenant"
|
|
|
|
|
|
# 站点 brand 是独立副本,不与租户 data 共享可变对象
|
|
|
|
|
|
assert site.brand is not tenant.data["brand"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tenant_without_sites():
|
|
|
|
|
|
"""无 sites 时 sites 为空元组,data 正常深合并。"""
|
|
|
|
|
|
tenant = TenantConfig.from_snapshot(
|
|
|
|
|
|
slug="t2", name="Tenant 2", snapshot={"ai": {"model": "glm-4-plus"}},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert tenant.sites == ()
|
|
|
|
|
|
assert tenant.status == "active"
|
|
|
|
|
|
assert tenant.data["ai"]["model"] == "glm-4-plus" # 覆盖生效
|
|
|
|
|
|
assert tenant.data["push"]["enabled"] is False # 缺省补齐
|
2026-10-12 16:59:31 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- 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"]
|