@furongjun1999/dsh-memory 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +552 -465
  2. package/docs/README.md +1 -0
  3. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  4. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  5. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  6. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  7. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  8. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
  9. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
  10. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v1.md +156 -0
  11. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v10.md +210 -0
  12. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
  13. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
  14. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
  15. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -0
  16. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v15.md +213 -0
  17. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v16.md +214 -0
  18. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v17.md +199 -0
  19. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v2.md +156 -0
  20. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v3.md +152 -0
  21. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v4.md +128 -0
  22. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v5.md +114 -0
  23. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v6.md +192 -0
  24. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v7.md +187 -0
  25. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v8.md +207 -0
  26. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v9.md +203 -0
  27. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  28. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +19 -3
  29. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  30. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  31. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  32. package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  33. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  34. package/lib/bridge.d.ts +9 -0
  35. package/lib/bridge.js +35 -0
  36. package/lib/index.js +7 -1
  37. package/lib/lib/roleplay_web.js +530 -443
  38. package/lib/lib/token_store.d.ts +7 -1
  39. package/lib/lib/token_store.js +12 -3
  40. package/md_cg/audit.py +12 -1
  41. package/md_cg/backfill.py +16 -15
  42. package/md_cg/backfill_bucket_zh.py +35 -0
  43. package/md_cg/bench_e2e_judge.py +532 -0
  44. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  45. package/md_cg/bench_e2e_qa.py +256 -0
  46. package/md_cg/branches.py +18 -2
  47. package/md_cg/ccgc.py +3 -2
  48. package/md_cg/chain.py +19 -4
  49. package/md_cg/consolidate.py +7 -6
  50. package/md_cg/crosscheck.py +4 -3
  51. package/md_cg/crypto.py +439 -437
  52. package/md_cg/datapath.py +395 -335
  53. package/md_cg/evidence.py +582 -580
  54. package/md_cg/export.py +3 -1
  55. package/md_cg/forgetting.py +2 -2
  56. package/md_cg/fsutil.py +48 -0
  57. package/md_cg/hotcache.py +255 -238
  58. package/md_cg/interop.py +161 -22
  59. package/md_cg/judgment_manifest.py +177 -0
  60. package/md_cg/links.py +655 -622
  61. package/md_cg/mcp_server.py +280 -29
  62. package/md_cg/mdcg.py +616 -125
  63. package/md_cg/mdcos.py +430 -66
  64. package/md_cg/mreview/govern.py +5 -4
  65. package/md_cg/postings.py +4 -2
  66. package/md_cg/readcache.py +76 -18
  67. package/md_cg/reconcile.py +228 -0
  68. package/md_cg/review_cli.py +170 -0
  69. package/md_cg/routing.py +28 -0
  70. package/md_cg/run_tests.py +211 -0
  71. package/md_cg/scrub.py +862 -852
  72. package/md_cg/security.py +385 -275
  73. package/md_cg/selfreport.py +3 -2
  74. package/md_cg/signer.py +565 -562
  75. package/md_cg/sources.py +3 -2
  76. package/md_cg/stg.py +6 -0
  77. package/md_cg/sustain.py +1168 -1138
  78. package/md_cg/test_access_hints.py +147 -0
  79. package/md_cg/test_branch_discard_tombstone.py +136 -0
  80. package/md_cg/test_branches.py +259 -249
  81. package/md_cg/test_chain_read_isolate.py +168 -0
  82. package/md_cg/test_datapath_device_name.py +203 -0
  83. package/md_cg/test_emit_negtail_cache.py +156 -0
  84. package/md_cg/test_en_pipeline.py +186 -166
  85. package/md_cg/test_govern_directread.py +421 -0
  86. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  87. package/md_cg/test_identity_attribution.py +228 -147
  88. package/md_cg/test_index_durability.py +238 -224
  89. package/md_cg/test_interop.py +4 -2
  90. package/md_cg/test_interop_judgment.py +228 -0
  91. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  92. package/md_cg/test_links_concurrent_write.py +188 -0
  93. package/md_cg/test_merge_upsert.py +168 -0
  94. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  95. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  96. package/md_cg/test_n131_merge_gate.py +205 -0
  97. package/md_cg/test_p1x_ref_root.py +160 -0
  98. package/md_cg/test_p27_docindex.py +774 -765
  99. package/md_cg/test_p2_mcp.py +3 -0
  100. package/md_cg/test_p32_backfill.py +304 -298
  101. package/md_cg/test_p39_verify_flow.py +90 -50
  102. package/md_cg/test_p47_session_view.py +60 -25
  103. package/md_cg/test_propose_tail_index.py +157 -0
  104. package/md_cg/test_read_scope_b27.py +277 -0
  105. package/md_cg/test_readcache_default_on.py +168 -0
  106. package/md_cg/test_readcache_precise_inval.py +270 -0
  107. package/md_cg/test_readcache_prodpath.py +55 -7
  108. package/md_cg/test_reconcile_v0.py +294 -0
  109. package/md_cg/test_retr_s1.py +6 -2
  110. package/md_cg/test_retr_s1b.py +67 -0
  111. package/md_cg/test_retr_s7.py +8 -0
  112. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  113. package/md_cg/test_retr_score_once.py +208 -0
  114. package/md_cg/test_review_onepass.py +170 -0
  115. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  116. package/md_cg/test_security_audit.py +155 -0
  117. package/md_cg/test_security_audit_b26.py +161 -0
  118. package/md_cg/test_security_audit_v21.py +250 -0
  119. package/md_cg/test_semantic_canonical.py +255 -241
  120. package/md_cg/test_session_isolation.py +168 -0
  121. package/md_cg/test_snapshot_autoclose.py +187 -0
  122. package/md_cg/test_tail_watermark_race.py +208 -0
  123. package/md_cg/test_tenant_env_override_warn.py +139 -0
  124. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  125. package/md_cg/test_v14_fixes.py +415 -397
  126. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  127. package/md_cg/theory.py +276 -273
  128. package/md_cg/tokens.py +734 -677
  129. package/md_cg/units.py +3 -2
  130. package/md_cg/vision_evidence.py +4 -3
  131. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  132. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  133. package/md_cg/writepipe.py +554 -550
  134. package/package.json +6 -2
  135. package/src/bridge.ts +434 -401
  136. package/src/index.ts +526 -518
  137. package/src/lib/roleplay_web.ts +1019 -932
  138. package/src/lib/token_store.ts +202 -192
@@ -0,0 +1,139 @@
1
+ # -*- coding: utf-8 -*-
2
+ """租户绑定 env 覆盖告警守卫(v8 N62 / v9 / 第15轮三次成立,止血验收)。
3
+
4
+ 攻击面:控制 MCP 进程 env 的部署面(宿主配置 / 启动器 / 同机多会话)——
5
+ tenantA 签发的 designer(secret, can_admin) 令牌在 MDCG_TENANT=tenantB 下经
6
+ mcp_server._build_principal → verify_token(tok, tenant=env) 静默以 tenantB
7
+ 身份运行(tenant=tenant or rec.get("tenant") 形参优先),全程零告警。
8
+
9
+ 止血口径(本轮):仅加 stderr 告警,零闸变——冲突时 Principal 判定结果与
10
+ 修复前完全一致(形参/env 值仍优先),但必须开口。对照 MDCG_CLEARANCE 先例
11
+ (mcp_server._build_principal 密级冲突告警)。clearance_cap 夹紧接线与租户
12
+ 绑定强校验不在本项范围(另行 deferred)。
13
+
14
+ 运行:python -m md_cg.test_tenant_env_override_warn
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import contextlib
19
+ import io
20
+ import os
21
+ import shutil
22
+ import sys
23
+ import tempfile
24
+
25
+ from . import tokens
26
+
27
+ PASS = FAIL = 0
28
+ FAILS = []
29
+ MARK = "租户绑定被入参覆盖" # 告警稳定锚点(tokens.verify_token)
30
+
31
+
32
+ def check(name, cond, detail=""):
33
+ global PASS, FAIL
34
+ if cond:
35
+ PASS += 1
36
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
37
+ else:
38
+ FAIL += 1
39
+ FAILS.append(name)
40
+ print(f" [FAIL] {name} · {detail}")
41
+
42
+
43
+ def _verify_capture(token, tenant, path):
44
+ """verify_token 并捕获 stderr,返回 (principal, stderr_text)。"""
45
+ buf = io.StringIO()
46
+ with contextlib.redirect_stderr(buf):
47
+ p = tokens.verify_token(token, tenant=tenant, path=path)
48
+ return p, buf.getvalue()
49
+
50
+
51
+ def main():
52
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
53
+ root = tempfile.mkdtemp(prefix="mdcg_tenant_warn_")
54
+ tf = os.path.join(root, "_tokens.json")
55
+ try:
56
+ # ---------------- ① 攻击复现(单元级) ----------------
57
+ print("\n[1] 攻击复现:tenantA 令牌被 tenantB 形参覆盖必须告警")
58
+ d = tokens.issue("designer", actor="dummy-designer", tenant="tenantA",
59
+ path=tf)
60
+ p, err = _verify_capture(d["token"], "tenantB", tf)
61
+ check("冲突覆盖发 stderr 告警", MARK in err,
62
+ (err.strip().splitlines() or ["(零 stderr 输出——静默覆盖成立)"])[-1][:90])
63
+ check("告警含两侧租户名", "tenantA" in err and "tenantB" in err,
64
+ err.strip()[:100])
65
+
66
+ # ---------------- ② 零闸变(止血不改判定) ----------------
67
+ print("\n[2] 零闸变(判停结果与修复前一致,只加告警)")
68
+ check("形参/env 租户仍优先生效", p.tenant == "tenantB",
69
+ f"principal.tenant={p.tenant}")
70
+ check("权限面不受影响(designer/can_admin 原样)",
71
+ p.role == "designer" and p.can_admin is True,
72
+ f"role={p.role} admin={p.can_admin}")
73
+
74
+ # ---------------- ③ 无冲突路径零误伤 ----------------
75
+ print("\n[3] 无冲突路径零误伤")
76
+ p0, err0 = _verify_capture(d["token"], None, tf)
77
+ check("tenant=None:零告警且回退令牌租户",
78
+ MARK not in err0 and p0.tenant == "tenantA",
79
+ f"tenant={p0.tenant}")
80
+ p1, err1 = _verify_capture(d["token"], "", tf)
81
+ check("tenant=''(env 未设/空串):零告警且回退令牌租户",
82
+ MARK not in err1 and p1.tenant == "tenantA",
83
+ f"tenant={p1.tenant}")
84
+ p2, err2 = _verify_capture(d["token"], "tenantA", tf)
85
+ check("tenant 同值:零告警", MARK not in err2 and p2.tenant == "tenantA",
86
+ f"tenant={p2.tenant}")
87
+
88
+ # ---------------- ④ MCP 进程级入口(_build_principal 直调先例 b26) ----------------
89
+ print("\n[4] MCP 进程级:MDCG_TOKEN(tenantA) + MDCG_TENANT=tenantB")
90
+ from .mcp_server import _build_principal
91
+ saved = {k: os.environ.get(k) for k in
92
+ ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_TOKEN_FILE", "MDCG_ROOT",
93
+ "MDCG_LEGACY_ENV_AUTH", "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE",
94
+ "MDCG_CLEARANCE", "MDCG_ACTOR")}
95
+ try:
96
+ os.environ["MDCG_TOKEN"] = d["token"]
97
+ os.environ["MDCG_TOKEN_FILE"] = tf
98
+ os.environ["MDCG_ROOT"] = os.path.join(root, "mcp")
99
+ os.environ["MDCG_TENANT"] = "tenantB"
100
+ for k in ("MDCG_LEGACY_ENV_AUTH", "MDCG_CAN_ADMIN",
101
+ "MDCG_CAN_WRITE", "MDCG_CLEARANCE", "MDCG_ACTOR"):
102
+ os.environ.pop(k, None)
103
+ buf = io.StringIO()
104
+ with contextlib.redirect_stderr(buf):
105
+ mp, merr = _build_principal()
106
+ mout = buf.getvalue()
107
+ check("MCP 入口构建成功(攻击原样可达)", merr is None and mp is not None,
108
+ f"err={merr}")
109
+ check("MCP 入口租户被覆盖为 tenantB(攻击面成立)",
110
+ getattr(mp, "tenant", None) == "tenantB",
111
+ f"tenant={getattr(mp, 'tenant', None)}")
112
+ check("MCP 入口 stderr 含覆盖告警", MARK in mout,
113
+ (mout.strip().splitlines() or ["(零 stderr)"])[-1][:90])
114
+ # 无冲突对照:MDCG_TENANT 与令牌租户一致 → 零告警
115
+ os.environ["MDCG_TENANT"] = "tenantA"
116
+ buf2 = io.StringIO()
117
+ with contextlib.redirect_stderr(buf2):
118
+ mp2, merr2 = _build_principal()
119
+ check("MDCG_TENANT 同值:零告警",
120
+ merr2 is None and MARK not in buf2.getvalue()
121
+ and getattr(mp2, "tenant", None) == "tenantA",
122
+ buf2.getvalue().strip()[:90])
123
+ finally:
124
+ for k, v in saved.items():
125
+ if v is None:
126
+ os.environ.pop(k, None)
127
+ else:
128
+ os.environ[k] = v
129
+ finally:
130
+ shutil.rmtree(root, ignore_errors=True)
131
+
132
+ print(f"\n通过 {PASS} / 失败 {FAIL}")
133
+ if FAILS:
134
+ print("失败项:" + ",".join(FAILS))
135
+ return 0 if FAIL == 0 else 1
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
@@ -0,0 +1,151 @@
1
+ # -*- coding: utf-8 -*-
2
+ """租户登记表损坏静默回落守卫(N124:v15 留档②+引擎域/检索域复测三度成立)。
3
+
4
+ 缺陷形态:mcp_server._resolve_root 的 except 分支(原 :3551-3552 裸 except
5
+ 静默置 t_root=None)+ security.TenantRegistry._load 吞 ValueError/OSError
6
+ 返回空表(security.py:274-283,合法 JSON 但顶层非对象时甚至不进 except)——
7
+ 登记表损坏/不可读时租户根物理隔离绑定**无声消失**:写入本应落到 acme_root
8
+ 的节点落到 MDCG_ROOT(跨租户可见),且「登记根×显式 MDCG_ROOT 冲突
9
+ fail-closed」检测(:3565-3567)连带失效,全程零痕迹。
10
+
11
+ 攻击面:能影响 MDCG_TENANT_REGISTRY 指向文件(损坏/截断/替换为目录/权限
12
+ 回收)的本地攻击面 + MDCG_ROOT 已设。
13
+
14
+ 止血口径(本轮,对照 MDCG_CLEARANCE 覆盖告警先例):仅加 stderr 告警,零
15
+ 闸变——损坏时 _resolve_root 返回值与修复前完全一致(回落 MDCG_ROOT),但
16
+ 必须开口;完好表下「租户已登记」「租户未登记(文档化零变更路径)」「冲突
17
+ fail-closed」三路径零误伤。
18
+
19
+ 红项 = R1-R3(坏 JSON / 登记表为目录 / JSON 顶层非对象 → 零 stderr);
20
+ G 项 = 完好表三路径零误伤 + fail-closed 防线仍在 + 返回值零闸变。
21
+ 运行:python -m md_cg.test_tenant_registry_corrupt_warn
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import contextlib
26
+ import io
27
+ import json
28
+ import os
29
+ import shutil
30
+ import sys
31
+ import tempfile
32
+
33
+ PASS = FAIL = 0
34
+ FAILS = []
35
+ MARK = "租户登记表" # 告警稳定锚点(mcp_server._resolve_root)
36
+
37
+
38
+ def check(name, cond, detail=""):
39
+ global PASS, FAIL
40
+ if cond:
41
+ PASS += 1
42
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
43
+ else:
44
+ FAIL += 1
45
+ FAILS.append(name)
46
+ print(f" [FAIL] {name} · {detail}")
47
+
48
+
49
+ def _resolve_capture(env):
50
+ """_resolve_root 并捕获 stderr,返回 ((root, err), stderr_text)。"""
51
+ from .mcp_server import _resolve_root
52
+ buf = io.StringIO()
53
+ with contextlib.redirect_stderr(buf):
54
+ out = _resolve_root(env)
55
+ return out, buf.getvalue()
56
+
57
+
58
+ def main():
59
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
60
+ root = tempfile.mkdtemp(prefix="mdcg_tenant_reg_")
61
+ try:
62
+ fallback = os.path.join(root, "fallback_root")
63
+ reg_root = os.path.join(root, "acme_root")
64
+ env = {"MDCG_ROOT": fallback, "MDCG_TENANT": "acme",
65
+ "MDCG_TENANT_REGISTRY": os.path.join(root, "_tenants.json")}
66
+
67
+ # ---------------- ① 攻击复现:登记表损坏必须开口 ----------------
68
+ print("\n[1] 攻击复现:登记表损坏 → 静默回落 MDCG_ROOT")
69
+ with open(env["MDCG_TENANT_REGISTRY"], "w", encoding="utf-8") as f:
70
+ f.write("{corrupted!!!") # 坏 JSON(v15 复测原样)
71
+ (r1, e1), err1 = _resolve_capture(env)
72
+ check("R1a 坏 JSON:stderr 含登记表告警", MARK in err1,
73
+ (err1.strip().splitlines() or ["(零 stderr——静默回落成立)"])[-1][:90])
74
+ check("R1b 零闸变:返回值仍回落 MDCG_ROOT",
75
+ r1 == fallback and e1 is None, f"root={r1}")
76
+ check("R1c 告警含租户名与回退根", "acme" in err1 and fallback in err1,
77
+ err1.strip()[:100])
78
+
79
+ # 登记表为目录(OSError 路径)
80
+ os.remove(env["MDCG_TENANT_REGISTRY"])
81
+ os.mkdir(env["MDCG_TENANT_REGISTRY"])
82
+ (r2, e2), err2 = _resolve_capture(env)
83
+ check("R2 登记表为目录:stderr 含告警且零闸变",
84
+ MARK in err2 and r2 == fallback and e2 is None,
85
+ (err2.strip().splitlines() or ["(零 stderr)"])[-1][:90])
86
+ os.rmdir(env["MDCG_TENANT_REGISTRY"])
87
+
88
+ # JSON 顶层非对象(合法 JSON 但 _load 不进 except 分支的路径)
89
+ with open(env["MDCG_TENANT_REGISTRY"], "w", encoding="utf-8") as f:
90
+ f.write('["not","a","table"]')
91
+ (r3, e3), err3 = _resolve_capture(env)
92
+ check("R3 JSON 顶层非对象:stderr 含告警且零闸变",
93
+ MARK in err3 and r3 == fallback and e3 is None,
94
+ (err3.strip().splitlines() or ["(零 stderr)"])[-1][:90])
95
+
96
+ # ---------------- ② 完好表三路径零误伤 ----------------
97
+ print("\n[2] 完好登记表:已登记/未登记/冲突 三路径零误伤")
98
+ os.remove(env["MDCG_TENANT_REGISTRY"])
99
+ from .security import TenantRegistry
100
+ TenantRegistry(env["MDCG_TENANT_REGISTRY"]).register("acme", reg_root)
101
+ # 已登记 happy path:不设 MDCG_ROOT(设了且不同值即冲突 fail-closed)
102
+ env_nr = {k: v for k, v in env.items() if k != "MDCG_ROOT"}
103
+ (r4, e4), err4 = _resolve_capture(env_nr)
104
+ check("G1 已登记:返回登记根且零告警",
105
+ r4 and os.path.abspath(r4) == os.path.abspath(reg_root)
106
+ and e4 is None and MARK not in err4, f"root={r4}")
107
+
108
+ env5 = dict(env, MDCG_TENANT="nobody")
109
+ (r5, e5), err5 = _resolve_capture(env5)
110
+ check("G2 未登记租户:文档化回落 MDCG_ROOT 且零告警",
111
+ r5 == fallback and e5 is None and MARK not in err5, f"root={r5}")
112
+
113
+ env6 = dict(env)
114
+ (r6, e6), err6 = _resolve_capture(dict(env6, MDCG_TENANT_REGISTRY=""))
115
+ # 空串 registry 路径 → 缺省 ~/.mdcg/_tenants.json(不在断言面,仅保不崩)
116
+ check("G3 缺省登记表路径不误伤告警锚",
117
+ isinstance(err6, str), f"root={r6}")
118
+
119
+ # 冲突 fail-closed 防线(损坏时连带失效的那道)必须仍在
120
+ (r7, e7), err7 = _resolve_capture(dict(env, MDCG_ROOT=reg_root + "_x"))
121
+ check("G4 登记根×显式根冲突仍 fail-closed",
122
+ r7 is None and e7 and "fail-closed" in e7, f"err={e7}")
123
+
124
+ # ---------------- ③ TenantRegistry.load_error 单元面 ----------------
125
+ print("\n[3] TenantRegistry.load_error 单元面")
126
+ bad = os.path.join(root, "bad.json")
127
+ with open(bad, "w", encoding="utf-8") as f:
128
+ f.write("{corrupted!!!")
129
+ t1 = TenantRegistry(bad)
130
+ check("G5 坏 JSON:load_error 非空", bool(t1.load_error),
131
+ f"load_error={t1.load_error}")
132
+ with open(bad, "w", encoding="utf-8") as f:
133
+ json.dump({"schema": 1, "tenants": {
134
+ "acme": {"root": reg_root}}}, f, ensure_ascii=False)
135
+ t2 = TenantRegistry(bad)
136
+ check("G6 完好表:load_error 为空", not t2.load_error,
137
+ f"load_error={t2.load_error}")
138
+ t3 = TenantRegistry(os.path.join(root, "absent.json"))
139
+ check("G7 缺文件:load_error 为空(正常首启不算损坏)",
140
+ not t3.load_error, f"load_error={t3.load_error}")
141
+ finally:
142
+ shutil.rmtree(root, ignore_errors=True)
143
+
144
+ print(f"\n通过 {PASS} / 失败 {FAIL}")
145
+ if FAILS:
146
+ print("失败项:" + ",".join(FAILS))
147
+ return 0 if FAIL == 0 else 1
148
+
149
+
150
+ if __name__ == "__main__":
151
+ sys.exit(main())