@furongjun1999/dsh-memory 0.5.0 → 0.6.0
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.
- package/README.md +572 -465
- package/docs/README.md +1 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
- package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
- package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
- 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
- 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
- package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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_v18.md +183 -0
- 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_v19.md +207 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
- package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
- 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
- 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
- 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
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/index.js +7 -1
- package/lib/init.d.ts +88 -0
- package/lib/init.js +287 -0
- package/lib/lib/roleplay_web.js +530 -443
- package/lib/lib/token_store.d.ts +7 -1
- package/lib/lib/token_store.js +12 -3
- package/md_cg/audit.py +12 -1
- package/md_cg/backfill.py +16 -15
- package/md_cg/backfill_bucket_zh.py +35 -0
- package/md_cg/bench_e2e_judge.py +532 -0
- package/md_cg/bench_e2e_locomo_qa.py +368 -0
- package/md_cg/bench_e2e_qa.py +256 -0
- package/md_cg/branches.py +18 -2
- package/md_cg/ccgc.py +3 -2
- package/md_cg/chain.py +19 -4
- package/md_cg/consolidate.py +7 -6
- package/md_cg/crosscheck.py +4 -3
- package/md_cg/crypto.py +439 -437
- package/md_cg/datapath.py +395 -335
- package/md_cg/evidence.py +8 -3
- package/md_cg/export.py +3 -1
- package/md_cg/forgetting.py +2 -2
- package/md_cg/fsutil.py +48 -0
- package/md_cg/hotcache.py +255 -238
- package/md_cg/interop.py +161 -22
- package/md_cg/judgment_manifest.py +177 -0
- package/md_cg/links.py +655 -622
- package/md_cg/mcp_server.py +282 -31
- package/md_cg/mdcg.py +702 -126
- package/md_cg/mdcos.py +454 -68
- package/md_cg/mreview/govern.py +5 -4
- package/md_cg/postings.py +4 -2
- package/md_cg/readcache.py +76 -18
- package/md_cg/reconcile.py +228 -0
- package/md_cg/review_cli.py +215 -0
- package/md_cg/routing.py +28 -0
- package/md_cg/run_tests.py +211 -0
- package/md_cg/scrub.py +862 -852
- package/md_cg/security.py +385 -275
- package/md_cg/selfreport.py +3 -2
- package/md_cg/signer.py +565 -562
- package/md_cg/sources.py +3 -2
- package/md_cg/stg.py +6 -0
- package/md_cg/sustain.py +1168 -1138
- package/md_cg/test_access_hints.py +147 -0
- package/md_cg/test_branch_discard_tombstone.py +136 -0
- package/md_cg/test_branches.py +259 -249
- package/md_cg/test_chain_read_isolate.py +168 -0
- package/md_cg/test_datapath_device_name.py +203 -0
- package/md_cg/test_emit_negtail_cache.py +156 -0
- package/md_cg/test_en_pipeline.py +186 -166
- package/md_cg/test_govern_directread.py +421 -0
- package/md_cg/test_i32_hotcache_env_key.py +218 -0
- package/md_cg/test_identity_attribution.py +228 -147
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_index_durability.py +238 -224
- package/md_cg/test_interop.py +4 -2
- package/md_cg/test_interop_judgment.py +228 -0
- package/md_cg/test_issue39_utf8_stdio.py +273 -0
- package/md_cg/test_links_concurrent_write.py +188 -0
- package/md_cg/test_merge_upsert.py +168 -0
- package/md_cg/test_n123_derive_expiry_chain.py +205 -0
- package/md_cg/test_n130_verify_falsified_protect.py +185 -0
- package/md_cg/test_n131_merge_gate.py +205 -0
- package/md_cg/test_none_id_write_guard.py +165 -0
- package/md_cg/test_p1x_ref_root.py +160 -0
- package/md_cg/test_p27_docindex.py +774 -765
- package/md_cg/test_p2_mcp.py +3 -0
- package/md_cg/test_p32_backfill.py +304 -298
- package/md_cg/test_p39_verify_flow.py +90 -50
- package/md_cg/test_p47_session_view.py +60 -25
- package/md_cg/test_propose_tail_index.py +157 -0
- package/md_cg/test_read_scope_b27.py +277 -0
- package/md_cg/test_readcache_default_on.py +168 -0
- package/md_cg/test_readcache_precise_inval.py +270 -0
- package/md_cg/test_readcache_prodpath.py +55 -7
- package/md_cg/test_reconcile_v0.py +294 -0
- package/md_cg/test_retr_s1.py +6 -2
- package/md_cg/test_retr_s1b.py +67 -0
- package/md_cg/test_retr_s7.py +8 -0
- package/md_cg/test_retr_s9_entity_ctx.py +181 -175
- package/md_cg/test_retr_score_once.py +208 -0
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_onepass.py +170 -0
- package/md_cg/test_rrf_graph_seed_cache.py +154 -0
- package/md_cg/test_security_audit.py +155 -0
- package/md_cg/test_security_audit_b26.py +161 -0
- package/md_cg/test_security_audit_v21.py +250 -0
- package/md_cg/test_semantic_canonical.py +255 -241
- package/md_cg/test_session_isolation.py +168 -0
- package/md_cg/test_snapshot_autoclose.py +187 -0
- package/md_cg/test_tail_watermark_race.py +208 -0
- package/md_cg/test_tenant_env_override_warn.py +139 -0
- package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
- package/md_cg/test_v14_fixes.py +415 -397
- package/md_cg/test_verify_dirty_reconcile.py +157 -0
- package/md_cg/theory.py +276 -273
- package/md_cg/tokens.py +734 -677
- package/md_cg/units.py +3 -2
- package/md_cg/vision_evidence.py +4 -3
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
- package/md_cg/writepipe.py +554 -550
- package/package.json +11 -2
- package/src/bridge.ts +434 -401
- package/src/cli.ts +65 -0
- package/src/index.ts +526 -518
- package/src/init.ts +347 -0
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_interop_judgment —— 互验执行链判据强度守卫(issue #37 六项)
|
|
3
|
+
|
|
4
|
+
J1 valid=单一放行位(断言 AND 套件)——套件全挂时必须 False
|
|
5
|
+
J2 A2 标注 self-reported;伪造 subject_fp 不影响 A3(承重墙输入不含自报值)
|
|
6
|
+
J3 failure_reason 枚举四象限结构化
|
|
7
|
+
J4 interop_gate 机械消费方三态(pass 放行 / fail 拒绝 / 缺产物 fail-closed)
|
|
8
|
+
J5 脱敏黑名单扩面(UNC/unix/~/$HOME/密钥形态/Bearer/文本形态)+ 形状白名单
|
|
9
|
+
J6 freeze 显式 out_dir 警告(不被 runner 识别)
|
|
10
|
+
|
|
11
|
+
运行:python -X utf8 -m md_cg.test_interop_judgment
|
|
12
|
+
"""
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
|
|
20
|
+
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
+
sys.path.insert(0, HERE)
|
|
22
|
+
|
|
23
|
+
from md_cg.interop import ( # noqa: E402
|
|
24
|
+
InteropSanityError, assert_a2, assert_a3, freeze,
|
|
25
|
+
make_verdict, sanity_check_verdict, shape_check_verdict)
|
|
26
|
+
|
|
27
|
+
_spec = importlib.util.spec_from_file_location(
|
|
28
|
+
"verify_runner", os.path.join(HERE, "hive", "verify_runner.py"))
|
|
29
|
+
vr = importlib.util.module_from_spec(_spec)
|
|
30
|
+
_spec.loader.exec_module(vr)
|
|
31
|
+
|
|
32
|
+
passed = failed = 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check(name, cond, detail=""):
|
|
36
|
+
global passed, failed
|
|
37
|
+
if cond:
|
|
38
|
+
passed += 1
|
|
39
|
+
print(" [PASS] " + name)
|
|
40
|
+
else:
|
|
41
|
+
failed += 1
|
|
42
|
+
print(" [FAIL] " + name + " " + str(detail)[:200])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mk(a_ok, suite_ok):
|
|
46
|
+
return vr._compose_semantics(a_ok, suite_ok)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
print("[1] J1/J3:verdict 语义四象限")
|
|
51
|
+
v, valid, a_ok, fr = _mk(True, True)
|
|
52
|
+
check("全过 → pass/valid=True/reason=None",
|
|
53
|
+
v == "pass" and valid and a_ok and fr is None)
|
|
54
|
+
v, valid, a_ok, fr = _mk(True, False)
|
|
55
|
+
check("断言过+套件挂 → fail/valid=False/assertions_ok=True/reason=suite_failed"
|
|
56
|
+
"(#37 实测形态:套件全挂时 valid 必须不再是 true)",
|
|
57
|
+
v == "fail" and valid is False and a_ok is True
|
|
58
|
+
and fr == "suite_failed")
|
|
59
|
+
v, valid, a_ok, fr = _mk(False, True)
|
|
60
|
+
check("断言挂+套件过 → reason=assertions_failed",
|
|
61
|
+
v == "fail" and valid is False and a_ok is False
|
|
62
|
+
and fr == "assertions_failed")
|
|
63
|
+
v, valid, a_ok, fr = _mk(False, False)
|
|
64
|
+
check("双挂 → reason=both", v == "fail" and fr == "both")
|
|
65
|
+
|
|
66
|
+
print("[2] J2:A2 self-reported 分层 + A3 不受伪造 subject_fp 影响")
|
|
67
|
+
a2 = assert_a2("a" * 64, "deadbeef" * 8)
|
|
68
|
+
check("伪造 subject_fp 可过 A2(弱断言如实)", a2["ok"] is True)
|
|
69
|
+
check("A2 输出标注 subject_fp_source=self-reported(#27 分层复用)",
|
|
70
|
+
a2.get("subject_fp_source") == "self-reported")
|
|
71
|
+
frozen = {"digest": "f" * 64}
|
|
72
|
+
a3 = assert_a3(frozen, "e" * 64)
|
|
73
|
+
check("A3 输入不含 subject_fp(伪造自报值过不了 A3——承重墙)",
|
|
74
|
+
"subject" not in json.dumps(a3) and a3["ok"] is False)
|
|
75
|
+
|
|
76
|
+
print("[3] J5:脱敏黑名单扩面(issue 列举形态全部拦截)")
|
|
77
|
+
leaks = [
|
|
78
|
+
("UNC 路径", "\\\\server\\share\\secret.txt"),
|
|
79
|
+
("macOS /Volumes", "/Volumes/disk/proj/x.md"),
|
|
80
|
+
("unix /etc", "配置在 /etc/passwd 附近"),
|
|
81
|
+
("家目录 ~", "见 ~/secret/keys.txt"),
|
|
82
|
+
("$HOME", "文件在 $HOME/.aws 下"),
|
|
83
|
+
# 假 token 字面量须自带占位符标记(fake/example 等):形状正则
|
|
84
|
+
# (md_cg/interop.py 的 `ghp_[A-Za-z0-9]{20,}`)照旧命中 → 本用例的
|
|
85
|
+
# 拦截断言不变;同时 scripts/check_publish_artifact.py 的 R1 会把它
|
|
86
|
+
# 记为 NOTE 而非 FAIL(否则出货门禁对「测试夹具里的假值」恒红——
|
|
87
|
+
# 红着等于没门禁)。见 FAKE_TOKEN_MARKERS。
|
|
88
|
+
("ghp_ token", "ghp_dummyABCDEFGHIJKLMNOPQRSTUVWXYZ1234"),
|
|
89
|
+
("AIza key", "AIzaSyABCDEFGHIJKLMNOPQRSTUVWXYZ1234567"),
|
|
90
|
+
("AKIA key", "AKIAIOSFODNN7EXAMPLE"),
|
|
91
|
+
("Bearer", "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig"),
|
|
92
|
+
("文本形态 user_prompt", "note: user_prompt: tell me a secret"),
|
|
93
|
+
]
|
|
94
|
+
base = {"iter_id": "t", "verifier_instance": "v", "verifier_fingerprint":
|
|
95
|
+
"a" * 64, "subject_instance": "m", "subject_fingerprint":
|
|
96
|
+
"b" * 64, "suite_origin": "w", "frozen_at": "x", "verdict":
|
|
97
|
+
"fail", "passed": 0, "failed": 0, "details": [], "valid": False,
|
|
98
|
+
"assertions_ok": False, "failure_reason": "both"}
|
|
99
|
+
for label, payload in leaks:
|
|
100
|
+
try:
|
|
101
|
+
v2 = dict(base, iter_id=payload[:40])
|
|
102
|
+
v2["details"] = [{"note": payload}]
|
|
103
|
+
sanity_check_verdict(v2)
|
|
104
|
+
check(f"拦截 {label}", False, payload)
|
|
105
|
+
except InteropSanityError:
|
|
106
|
+
check(f"拦截 {label}", True)
|
|
107
|
+
try:
|
|
108
|
+
sanity_check_verdict(dict(base))
|
|
109
|
+
check("正常 verdict 过脱敏门禁(无误伤)", True)
|
|
110
|
+
except InteropSanityError as e:
|
|
111
|
+
check("正常 verdict 过脱敏门禁(无误伤)", False, str(e))
|
|
112
|
+
|
|
113
|
+
print("[4] J5:形状白名单(越权字段/坏形状/坏枚举)")
|
|
114
|
+
for label, mut in [
|
|
115
|
+
("越权顶层字段", {"extra_key": "x"}),
|
|
116
|
+
("指纹非十六进制", {"verifier_fingerprint": "zzz!*"}),
|
|
117
|
+
("verdict 枚举外", {"verdict": "PASS"}),
|
|
118
|
+
("failure_reason 枚举外", {"failure_reason": "whatever"}),
|
|
119
|
+
("计数非整数", {"passed": "3"}),
|
|
120
|
+
("计数负数", {"failed": -1})]:
|
|
121
|
+
try:
|
|
122
|
+
shape_check_verdict(dict(base, **mut))
|
|
123
|
+
check(f"形状拦截 {label}", False)
|
|
124
|
+
except InteropSanityError:
|
|
125
|
+
check(f"形状拦截 {label}", True)
|
|
126
|
+
try:
|
|
127
|
+
shape_check_verdict(dict(base, verdict="fail", failure_reason=None,
|
|
128
|
+
passed=1, failed=2))
|
|
129
|
+
check("合法形状通过白名单", True)
|
|
130
|
+
except InteropSanityError as e:
|
|
131
|
+
check("合法形状通过白名单", False, str(e))
|
|
132
|
+
|
|
133
|
+
print("[5] J6:freeze 显式 out_dir 警告")
|
|
134
|
+
import contextlib
|
|
135
|
+
with tempfile.TemporaryDirectory() as td:
|
|
136
|
+
buf = io_err = None
|
|
137
|
+
try:
|
|
138
|
+
import io as _io
|
|
139
|
+
buf = _io.StringIO()
|
|
140
|
+
with contextlib.redirect_stderr(buf):
|
|
141
|
+
freeze("zz-guard-j6", out_dir=td)
|
|
142
|
+
finally:
|
|
143
|
+
pass
|
|
144
|
+
check("显式 out_dir → stderr 警告(不被 runner 识别)",
|
|
145
|
+
buf is not None and "不被" in (buf.getvalue() or ""))
|
|
146
|
+
std = os.path.join(HERE, "hive", "interop", "zz-guard-j6")
|
|
147
|
+
import shutil
|
|
148
|
+
shutil.rmtree(std, ignore_errors=True)
|
|
149
|
+
|
|
150
|
+
print("[6] J4:interop_gate 机械消费方三态")
|
|
151
|
+
gate = os.path.join(HERE, "scripts", "interop_gate.py")
|
|
152
|
+
with tempfile.TemporaryDirectory() as td:
|
|
153
|
+
vp = os.path.join(td, "verdict.json")
|
|
154
|
+
ok_v = dict(base, verdict="pass", valid=True, assertions_ok=True,
|
|
155
|
+
failure_reason=None, passed=9, failed=0)
|
|
156
|
+
json.dump(ok_v, open(vp, "w", encoding="utf-8"), ensure_ascii=False)
|
|
157
|
+
r = subprocess.run([sys.executable, gate, "--path", vp],
|
|
158
|
+
capture_output=True, text=True, encoding="utf-8")
|
|
159
|
+
check("pass+valid → exit 0(放行)", r.returncode == 0, r.stdout)
|
|
160
|
+
json.dump(dict(base), open(vp, "w", encoding="utf-8"),
|
|
161
|
+
ensure_ascii=False)
|
|
162
|
+
r = subprocess.run([sys.executable, gate, "--path", vp],
|
|
163
|
+
capture_output=True, text=True, encoding="utf-8")
|
|
164
|
+
check("fail/valid=False → exit 1(拒绝合并)", r.returncode == 1)
|
|
165
|
+
os.remove(vp)
|
|
166
|
+
r = subprocess.run([sys.executable, gate, "--path", vp],
|
|
167
|
+
capture_output=True, text=True, encoding="utf-8")
|
|
168
|
+
check("无 verdict → exit 2(未互验 fail-closed)", r.returncode == 2)
|
|
169
|
+
json.dump({"verdict": "pass"}, open(vp, "w", encoding="utf-8"))
|
|
170
|
+
r = subprocess.run([sys.executable, gate, "--path", vp],
|
|
171
|
+
capture_output=True, text=True, encoding="utf-8")
|
|
172
|
+
check("旧版产物缺契约字段 → exit 3(拒绝猜测)", r.returncode == 3)
|
|
173
|
+
|
|
174
|
+
print("[7] make_verdict 全链(新字段经双门禁落盘)")
|
|
175
|
+
v3 = make_verdict(iter_id="t", verifier_instance="v",
|
|
176
|
+
verifier_fingerprint="a" * 64, subject_instance="m",
|
|
177
|
+
subject_fingerprint="b" * 64,
|
|
178
|
+
suite_origin="w", frozen_at="x", verdict="fail",
|
|
179
|
+
passed=0, failed=1,
|
|
180
|
+
details=[assert_a2("a" * 64, "b" * 64)])
|
|
181
|
+
v3.update({"valid": False, "assertions_ok": False,
|
|
182
|
+
"failure_reason": "both"})
|
|
183
|
+
try:
|
|
184
|
+
sanity_check_verdict(v3)
|
|
185
|
+
shape_check_verdict(v3)
|
|
186
|
+
check("含 self-reported 标注的完整产物过双门禁", True)
|
|
187
|
+
except InteropSanityError as e:
|
|
188
|
+
check("含 self-reported 标注的完整产物过双门禁", False, str(e))
|
|
189
|
+
|
|
190
|
+
print("[8] _parse_counts 与 run_tests SUMMARY 实际形态契约(v6 N28)")
|
|
191
|
+
# 契约断裂实形:run_tests.py:216 打印 `===== SUMMARY N/M 通过,Z 跳过…`,
|
|
192
|
+
# 而 _parse_counts 旧第三正则只认 `通过 X / 失败 Y`(run_tests 从未打印)
|
|
193
|
+
# → python 侧计数恒 (0,0),verdict.json passed/failed 系统性失真
|
|
194
|
+
# (判决靠 exit code 仍对,留痕假零)。
|
|
195
|
+
p188, f188 = vr._parse_counts(
|
|
196
|
+
"===== SUMMARY 188/189 通过,1 跳过(依赖缺失/平台不符) =====")
|
|
197
|
+
check("实跑 SUMMARY 形态可解析(188/189 → passed=188,非 0)",
|
|
198
|
+
p188 == 188, p188)
|
|
199
|
+
check("failed 由 runnable-passed 推出(→ 1,非 0)", f188 == 1, f188)
|
|
200
|
+
# 生产者现场取证:真跑 run_tests(scripts 组 -k 过滤到单测),SUMMARY
|
|
201
|
+
# 行必须能被消费端解析——两侧契约同一守卫钉死,任一侧漂移必红。
|
|
202
|
+
r = subprocess.run(
|
|
203
|
+
[sys.executable, "-X", "utf8",
|
|
204
|
+
os.path.join(HERE, "scripts", "run_tests.py"), "scripts",
|
|
205
|
+
"-k", "judgment"],
|
|
206
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
207
|
+
cwd=HERE, timeout=600)
|
|
208
|
+
summ = [ln for ln in (r.stdout or "").splitlines() if "SUMMARY" in ln]
|
|
209
|
+
check("run_tests 实跑产出 SUMMARY 行(生产者在场)", bool(summ),
|
|
210
|
+
(r.stdout or "")[-200:])
|
|
211
|
+
if summ:
|
|
212
|
+
pp, ff = vr._parse_counts(r.stdout)
|
|
213
|
+
check("消费端能解析生产端实跑输出(非 (0,0) 假读)",
|
|
214
|
+
(pp, ff) != (0, 0), (pp, ff, summ[-1]))
|
|
215
|
+
# cargo 形态不回归;跳过单列不计失败(裸 clone SKIP 面不虚增 failed)
|
|
216
|
+
pc, fc = vr._parse_counts("test result: ok. 3 passed; 1 failed; 0 ignored")
|
|
217
|
+
check("cargo 形态照旧(3 passed; 1 failed → (3,1))", (pc, fc) == (3, 1),
|
|
218
|
+
(pc, fc))
|
|
219
|
+
ps, fs = vr._parse_counts("===== SUMMARY 187/187 通过,2 跳过(依赖缺失) =====")
|
|
220
|
+
check("跳过不计入 failed(187/187+2 跳过 → (187,0))", (ps, fs) == (187, 0),
|
|
221
|
+
(ps, fs))
|
|
222
|
+
|
|
223
|
+
print(f"\ninterop_judgment: {passed} 通过 / {failed} 失败")
|
|
224
|
+
return 0 if not failed else 1
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
sys.exit(main())
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_issue39_utf8_stdio.py · Windows 控制台代码页下 stdio MCP 中文必炸的守卫(issue #39)
|
|
3
|
+
|
|
4
|
+
现场(外部用户实测,issue #39):Windows 控制台代码页(GBK/CP936)下,md_cg 的
|
|
5
|
+
stdio MCP server 写中文必失败——宿主按 UTF-8 发来的中文参数被按 GBK 解码:
|
|
6
|
+
· ``gbk:strict`` → 读侧直接 ``UnicodeDecodeError``(本守卫实弹注入的形态,已实证
|
|
7
|
+
「中文标题:编码守卫」的 UTF-8 字节序列按 GBK 严格解码必非法);
|
|
8
|
+
· ``surrogateescape`` → 解出代理对 ``\\udcXX``,``_j()``(ensure_ascii=False)
|
|
9
|
+
按 UTF-8 编码时抛 ``UnicodeEncodeError: surrogates not allowed``。
|
|
10
|
+
两种形态整条请求同亡;写英文正常,用户难自查。桥层 0.4.10 已在
|
|
11
|
+
``src/lib/mdcg_client.ts`` 的 ``mdcgChildEnv()`` 注入 ``PYTHONUTF8=1``,但只覆盖
|
|
12
|
+
DSH 桥路径——Claude Code / code CLI 直接 mcp.json 接入不经桥,仍踩。
|
|
13
|
+
|
|
14
|
+
修复(本守卫的红绿两态锚点):``md_cg/mcp_server.py`` 与
|
|
15
|
+
``hive/hive_mcp/mcp_server.py``(同族全修)各加模块级 ``_force_utf8_stdio()``,
|
|
16
|
+
并在 ``main()`` **首行**调用——进程内把 stdio 三流 reconfigure 成
|
|
17
|
+
``utf-8``/``errors="replace"``,是对桥层 PYTHONUTF8=1 的纵深补位;``replace``
|
|
18
|
+
保证坏字节最多丢字符、不炸整条请求。
|
|
19
|
+
|
|
20
|
+
实弹形态:subprocess spawn ``[sys.executable, "-m", "md_cg.mcp_server"]``
|
|
21
|
+
(cwd=仓根,**不带** ``-X utf8``),env 显式设 ``PYTHONIOENCODING=gbk:strict``
|
|
22
|
+
模拟 CP936 控制台;按行分隔 JSON-RPC(initialize → notifications/initialized →
|
|
23
|
+
tools/call cg {op:"write", content:中文})。断言:响应行是合法 JSON、write 响应
|
|
24
|
+
不含 ``UnicodeEncodeError`` 且返回 ``ok``/``moved_to`` 字段。hive 面同款实弹
|
|
25
|
+
(中文 hive_spawn;HIVE_EXE 指向不存在的探针路径——``_ensure_serve`` 在 isfile
|
|
26
|
+
即返回,绝不拉起真 serve)。修复前此面红(进程在读侧崩、响应行缺失),修复后绿。
|
|
27
|
+
|
|
28
|
+
隔离纪律:MDCG_ROOT/MDCG_STATE_ROOT/MDCG_DATA_ROOT/MDCG_AUX_ROOT/
|
|
29
|
+
MDCG_TENANT_REGISTRY/HIVE_JOBS_DIR/HIVE_CONFIG 全部指向 tempfile.mkdtemp 临时
|
|
30
|
+
目录(登记表指向不存在的临时路径=空表回落),身份走既有豁免面
|
|
31
|
+
``MDCG_LEGACY_ENV_AUTH=1``(env 直连 designer,零令牌、不回落真实 ~/.mdcg),
|
|
32
|
+
MDCG_SUSTAIN=0 关常驻循环;finally rmtree——绝不触真实令牌库与真实数据目录。
|
|
33
|
+
|
|
34
|
+
自检 floor:实弹断言低于 LIVE_FLOOR 视为失败——防止「spawn 面失效 → 假绿」。
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import shutil
|
|
41
|
+
import subprocess
|
|
42
|
+
import sys
|
|
43
|
+
import tempfile
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
47
|
+
except AttributeError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
51
|
+
LIVE_FLOOR = 2 # 实弹断言下限(md_cg 面 ≥1 + hive 面 ≥1)
|
|
52
|
+
PROBE = "中文标题:编码守卫" # UTF-8 字节按 GBK strict 解码必非法(红态机制,见头注)
|
|
53
|
+
|
|
54
|
+
PASS, FAIL, LIVE = 0, 0, 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check(name, cond, detail="", live=False):
|
|
58
|
+
global PASS, FAIL, LIVE
|
|
59
|
+
if live:
|
|
60
|
+
LIVE += 1
|
|
61
|
+
if cond:
|
|
62
|
+
PASS += 1
|
|
63
|
+
print(f" [ok] {name}")
|
|
64
|
+
else:
|
|
65
|
+
FAIL += 1
|
|
66
|
+
print(f" [FAIL] {name} {detail}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# 模拟 CP936 控制台前,先把父进程可能携带的「身份/路径/编码」注入面清干净——
|
|
70
|
+
# 守卫子进程只吃临时目录,绝不吃本机真实 ~/.mdcg / 真实 MDCG_ROOT。
|
|
71
|
+
_DIRTY_KEYS = (
|
|
72
|
+
"MDCG_TOKEN", "MDCG_CLEARANCE", "MDCG_TENANT", "MDCG_TENANT_REGISTRY",
|
|
73
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_LEGACY_ENV_AUTH",
|
|
74
|
+
"MDCG_LEGACY_ENV_ADMIN", "MDCG_SESSION", "DSH_SESSION_ID",
|
|
75
|
+
"MDCG_HARNESS", "MDCG_UNIT", "MDCG_ROOT", "MDCG_STATE_ROOT",
|
|
76
|
+
"MDCG_DATA_ROOT", "MDCG_AUX_ROOT", "MDCG_SUSTAIN", "MDCG_SUSTAIN_NAME",
|
|
77
|
+
"MDCG_MCP_SURFACE", "MDCG_TOOL_FACE", "MDCG_ACTOR",
|
|
78
|
+
"MDCG_VERIFIER_MODULES", "MDCG_HIVE_JOBS",
|
|
79
|
+
"PYTHONUTF8", "PYTHONIOENCODING", "PYTHONLEGACYWINDOWSSTDIO",
|
|
80
|
+
"HIVE_JOBS_DIR", "HIVE_CONFIG", "HIVE_EXE", "HIVE_API_KEY",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _base_env(tmp: str) -> dict:
|
|
85
|
+
env = {k: v for k, v in os.environ.items() if k not in _DIRTY_KEYS}
|
|
86
|
+
env["PYTHONIOENCODING"] = "gbk:strict" # 模拟 CP936 控制台(无 -X utf8)
|
|
87
|
+
env["MDCG_SUSTAIN"] = "0" # 关常驻循环(线程面归零)
|
|
88
|
+
env["MDCG_ACTOR"] = "issue39-guard"
|
|
89
|
+
# 身份走既有豁免面:env 直连 designer(二次开关齐全),零令牌、不回落真实凭据面
|
|
90
|
+
env["MDCG_LEGACY_ENV_AUTH"] = "1"
|
|
91
|
+
env["MDCG_CAN_ADMIN"] = "1"
|
|
92
|
+
env["MDCG_LEGACY_ENV_ADMIN"] = "1"
|
|
93
|
+
_ = tmp
|
|
94
|
+
return env
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _mdcg_env(tmp: str) -> dict:
|
|
98
|
+
env = _base_env(tmp)
|
|
99
|
+
env["MDCG_ROOT"] = os.path.join(tmp, "cgroot")
|
|
100
|
+
env["MDCG_TENANT_REGISTRY"] = os.path.join(tmp, "_tenants_absent.json")
|
|
101
|
+
env["MDCG_STATE_ROOT"] = os.path.join(tmp, "state")
|
|
102
|
+
env["MDCG_DATA_ROOT"] = os.path.join(tmp, "data")
|
|
103
|
+
# 子目录名不可取 "aux":AUX 是 Windows 保留设备名,ntpath.abspath 经
|
|
104
|
+
# GetFullPathNameW 会把末段 aux 解析成设备路径 \\.\aux(实测),导致
|
|
105
|
+
# aux_root() 覆盖键静默失联——取名 auxroot 避开。
|
|
106
|
+
env["MDCG_AUX_ROOT"] = os.path.join(tmp, "auxroot")
|
|
107
|
+
return env
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _hive_env(tmp: str) -> dict:
|
|
111
|
+
env = _base_env(tmp)
|
|
112
|
+
env["HIVE_JOBS_DIR"] = os.path.join(tmp, "hivejobs")
|
|
113
|
+
# 指向不存在的配置与可执行文件:_ensure_serve 在 isfile 即返回,绝不拉起真 serve
|
|
114
|
+
env["HIVE_CONFIG"] = os.path.join(tmp, "hive_config_absent.json")
|
|
115
|
+
env["HIVE_EXE"] = os.path.join(tmp, "no_such_hive.exe")
|
|
116
|
+
return env
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _rpc(rid, method, params=None):
|
|
120
|
+
msg = {"jsonrpc": "2.0", "id": rid, "method": method}
|
|
121
|
+
if params is not None:
|
|
122
|
+
msg["params"] = params
|
|
123
|
+
return json.dumps(msg, ensure_ascii=False)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _talk(cmd_args, env, lines, timeout=180):
|
|
127
|
+
"""spawn server → 写 JSON-RPC 行 → 收 (returncode, stdout_text, stderr_text)。"""
|
|
128
|
+
proc = subprocess.Popen(
|
|
129
|
+
cmd_args, cwd=ROOT, env=env,
|
|
130
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
131
|
+
text=True, encoding="utf-8", errors="replace")
|
|
132
|
+
try:
|
|
133
|
+
out, err = proc.communicate("\n".join(lines) + "\n", timeout=timeout)
|
|
134
|
+
except subprocess.TimeoutExpired:
|
|
135
|
+
proc.kill()
|
|
136
|
+
out, err = proc.communicate()
|
|
137
|
+
return proc.returncode, out or "", err or ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _resp_lines(out: str):
|
|
141
|
+
"""stdout 逐行解析成 (raw_line, obj_or_None)。"""
|
|
142
|
+
got = []
|
|
143
|
+
for ln in out.splitlines():
|
|
144
|
+
ln = ln.strip()
|
|
145
|
+
if not ln:
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
got.append((ln, json.loads(ln)))
|
|
149
|
+
except ValueError:
|
|
150
|
+
got.append((ln, None))
|
|
151
|
+
return got
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _find_resp(resps, rid):
|
|
155
|
+
for _raw, obj in resps:
|
|
156
|
+
if isinstance(obj, dict) and obj.get("id") == rid:
|
|
157
|
+
return obj
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _main_body_has_call(src: str) -> bool:
|
|
162
|
+
"""源断言:main() 函数体首段含 _force_utf8_stdio() 调用(定义与调用都在)。"""
|
|
163
|
+
if "def _force_utf8_stdio()" not in src:
|
|
164
|
+
return False
|
|
165
|
+
idx = src.find("def main")
|
|
166
|
+
if idx < 0:
|
|
167
|
+
return False
|
|
168
|
+
window = src[idx:idx + 400]
|
|
169
|
+
return "_force_utf8_stdio()" in window
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_issue39_")
|
|
174
|
+
try:
|
|
175
|
+
# ---- 1. md_cg 面实弹:GBK 代码页下写中文 ----
|
|
176
|
+
print("[1] md_cg stdio 实弹(PYTHONIOENCODING=gbk:strict,无 -X utf8)")
|
|
177
|
+
rc, out, err = _talk(
|
|
178
|
+
[sys.executable, "-m", "md_cg.mcp_server"], _mdcg_env(tmp), [
|
|
179
|
+
_rpc(1, "initialize", {}),
|
|
180
|
+
_rpc(None, "notifications/initialized"),
|
|
181
|
+
_rpc(3, "tools/call", {"name": "cg", "arguments": {
|
|
182
|
+
"op": "write", "content": PROBE}}),
|
|
183
|
+
])
|
|
184
|
+
resps = _resp_lines(out)
|
|
185
|
+
init = _find_resp(resps, 1)
|
|
186
|
+
check("1a initialize 握手在 GBK 控制台下存活(合法 JSON 响应)",
|
|
187
|
+
isinstance(init, dict)
|
|
188
|
+
and (init.get("result") or {}).get("serverInfo", {}).get(
|
|
189
|
+
"name") == "mdcg-mcp",
|
|
190
|
+
f"rc={rc} lines={len(resps)} err_tail={err[-200:]!r}", live=True)
|
|
191
|
+
wr = _find_resp(resps, 3)
|
|
192
|
+
wr_ok = False
|
|
193
|
+
if isinstance(wr, dict):
|
|
194
|
+
try:
|
|
195
|
+
payload = json.loads(
|
|
196
|
+
(wr.get("result") or {}).get("content", [{}])[0].get(
|
|
197
|
+
"text", "{}"))
|
|
198
|
+
wr_ok = ("ok" in payload) or ("moved_to" in payload)
|
|
199
|
+
except (ValueError, AttributeError, IndexError):
|
|
200
|
+
wr_ok = False
|
|
201
|
+
check("1b 中文 write 响应合法且返回 ok/moved_to 字段(红态:进程在读侧崩,本条缺失)",
|
|
202
|
+
wr_ok, f"wr={str(wr)[:200]}", live=True)
|
|
203
|
+
check("1c 全程无 UnicodeEncodeError(stdout/stderr 不见代理对编码炸点)",
|
|
204
|
+
"UnicodeEncodeError" not in out and "UnicodeEncodeError" not in err,
|
|
205
|
+
f"out_tail={out[-160:]!r} err_tail={err[-160:]!r}", live=True)
|
|
206
|
+
check("1d 进程正常下线(EOF 后 returncode=0,非中途崩死)",
|
|
207
|
+
rc == 0, f"rc={rc} err_tail={err[-200:]!r}", live=True)
|
|
208
|
+
|
|
209
|
+
# ---- 2. hive 面实弹:同族 server 同款代码页 ----
|
|
210
|
+
print("[2] hive stdio 实弹(同款 PYTHONIOENCODING=gbk:strict)")
|
|
211
|
+
rc2, out2, err2 = _talk(
|
|
212
|
+
[sys.executable, "-m", "hive.hive_mcp.mcp_server"],
|
|
213
|
+
_hive_env(tmp), [
|
|
214
|
+
_rpc(1, "initialize", {}),
|
|
215
|
+
_rpc(None, "notifications/initialized"),
|
|
216
|
+
_rpc(3, "tools/call", {"name": "hive_spawn", "arguments": {
|
|
217
|
+
"model": "probe-model", "user_prompt": PROBE}}),
|
|
218
|
+
])
|
|
219
|
+
resps2 = _resp_lines(out2)
|
|
220
|
+
init2 = _find_resp(resps2, 1)
|
|
221
|
+
check("2a initialize 握手存活(合法 JSON 响应,serverInfo=hive-mcp)",
|
|
222
|
+
isinstance(init2, dict)
|
|
223
|
+
and (init2.get("result") or {}).get("serverInfo", {}).get(
|
|
224
|
+
"name") == "hive-mcp",
|
|
225
|
+
f"rc={rc2} lines={len(resps2)} err_tail={err2[-200:]!r}", live=True)
|
|
226
|
+
sp = _find_resp(resps2, 3)
|
|
227
|
+
sp_ok = False
|
|
228
|
+
if isinstance(sp, dict):
|
|
229
|
+
try:
|
|
230
|
+
payload = json.loads(
|
|
231
|
+
(sp.get("result") or {}).get("content", [{}])[0].get(
|
|
232
|
+
"text", "{}"))
|
|
233
|
+
sp_ok = payload.get("ok") is True and bool(payload.get("job_id"))
|
|
234
|
+
except (ValueError, AttributeError, IndexError):
|
|
235
|
+
sp_ok = False
|
|
236
|
+
check("2b 中文 hive_spawn 返回 ok=true+job_id(探针 exe 不存在,绝不拉真 serve)",
|
|
237
|
+
sp_ok, f"resp={str(sp)[:200]}", live=True)
|
|
238
|
+
check("2c hive 面无 UnicodeEncodeError",
|
|
239
|
+
"UnicodeEncodeError" not in out2
|
|
240
|
+
and "UnicodeEncodeError" not in err2,
|
|
241
|
+
f"err_tail={err2[-160:]!r}", live=True)
|
|
242
|
+
# 探针诚实边界:任务必须落在守卫自己的临时池,真实 jobs 池零触碰
|
|
243
|
+
jobs_dir = os.path.join(tmp, "hivejobs")
|
|
244
|
+
probe_jobs = [n for n in (os.listdir(jobs_dir) if os.path.isdir(jobs_dir)
|
|
245
|
+
else []) if n.startswith("h")]
|
|
246
|
+
check("2d 任务落在临时 jobs 池(隔离面自证)",
|
|
247
|
+
len(probe_jobs) == 1 if sp_ok else len(probe_jobs) == 0,
|
|
248
|
+
f"probe_jobs={probe_jobs}")
|
|
249
|
+
|
|
250
|
+
# ---- 3. 源断言:两 server 的 main() 首段强制 UTF-8 ----
|
|
251
|
+
print("[3] 源断言(main() 首段 _force_utf8_stdio())")
|
|
252
|
+
for rel in ("md_cg/mcp_server.py", "hive/hive_mcp/mcp_server.py"):
|
|
253
|
+
with open(os.path.join(ROOT, rel), encoding="utf-8",
|
|
254
|
+
errors="replace") as fh:
|
|
255
|
+
src = fh.read()
|
|
256
|
+
check(f"3·{rel}: 定义存在且 main() 首段调用 _force_utf8_stdio()",
|
|
257
|
+
_main_body_has_call(src),
|
|
258
|
+
"缺定义或 main() 首段未调用(必须早于一切 stderr 中文写与 stdin 读取)")
|
|
259
|
+
|
|
260
|
+
# ---- 4. 守卫自检 floor:实弹断言不足即假绿 ----
|
|
261
|
+
print("[4] 守卫自检")
|
|
262
|
+
check(f"4a 实弹断言数 ≥ {LIVE_FLOOR}(防 spawn/扫描面失效假绿)",
|
|
263
|
+
LIVE >= LIVE_FLOOR, f"LIVE={LIVE}")
|
|
264
|
+
finally:
|
|
265
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
266
|
+
|
|
267
|
+
print(f"\n=== issue39 utf8 stdio tests: {PASS} passed, {FAIL} failed "
|
|
268
|
+
f"(live={LIVE}) ===")
|
|
269
|
+
sys.exit(1 if FAIL else 0)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
sys.exit(main())
|