@furongjun1999/dsh-memory 0.5.1 → 0.6.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.
- package/README.md +588 -552
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- 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/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -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/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -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//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -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 +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -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_v20.md +283 -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_v21.md +224 -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_v22.md +223 -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_v23.md +293 -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_v24.md +303 -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_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +20 -0
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -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/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.js +24 -2
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.d.ts +88 -0
- package/lib/init.js +302 -0
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evidence.py +585 -582
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4152 -3818
- package/md_cg/mdcg.py +4208 -3462
- package/md_cg/mdcos.py +4491 -4137
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/review_cli.py +49 -4
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1299 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +166 -0
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +516 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- 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_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +8 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/cli.ts +65 -0
- package/src/hooks.ts +229 -16
- package/src/init.ts +361 -0
- package/src/lib/prompt_safety.ts +88 -1
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · B4 守卫:索引分片目录不自愈(长驻进程内 `_index_log/` 被删后
|
|
3
|
+
append 永抛 `FileNotFoundError`,且永不恢复)
|
|
4
|
+
|
|
5
|
+
缺陷(修复前,本守卫红态实抓:`probe_b4.py` 探针 + 本文件 A 组)
|
|
6
|
+
``ShardedLog`` 只在 ``__init__`` 里 ``makedirs(self.dir)``;``append`` 首次
|
|
7
|
+
``open(self.path, "a")`` 前**不复查目录**。生产形态是长驻进程(``MdCGSecure(
|
|
8
|
+
root, principal, autoflush=1)``,``MdCG.flush`` 每批写完整批 ``close()`` 分片
|
|
9
|
+
句柄 ⇒ 下次 append 必经重开),于是:
|
|
10
|
+
|
|
11
|
+
· 进程内存活期间 ``_index_log/`` 被删(外部清理脚本 / 误删 / 备份还原)后,
|
|
12
|
+
此后**每次** append 都在 ``open`` 处抛 ``FileNotFoundError [Errno 2]``;
|
|
13
|
+
· 目录三次都不重建、``close()`` 抛同一异常、``_index.json`` 从未落成;
|
|
14
|
+
· 正文 ``.md`` 已落盘(``mdcg.py`` 先写盘再标脏索引)而索引未更新——
|
|
15
|
+
索引静默落后于盘面,且**无任何失败信号以外的可观测痕迹**。
|
|
16
|
+
|
|
17
|
+
实测(2026-09-30,沙箱临时根,非在役库):
|
|
18
|
+
· 修前:``add#2/#3/#4`` 三连抛 ``FileNotFoundError``,崩点 ``fsutil.py`` 的
|
|
19
|
+
``open``(原 564 行);``read_all`` 返回 ``[]``;``close()`` 抛同异常;
|
|
20
|
+
``_index.json`` 不存在。
|
|
21
|
+
· 不自愈的必要条件是「**本进程已持有 ShardedLog 实例**」:``md_cg/mdcg.py``
|
|
22
|
+
仅在 ``self._log is None`` 时建实例;全新短命进程做同一操作时
|
|
23
|
+
``__init__`` 的 makedirs 生效、目录会被重建(反证,探针 P-4 已给)。
|
|
24
|
+
|
|
25
|
+
修复口径(**单点**:`fsutil.ensure_shard_dir` 是分片目录存在性的唯一实现,
|
|
26
|
+
``ShardedLog.__init__`` 与 ``ShardedLog.append`` 两处调用点都是委托、不做副本):
|
|
27
|
+
① ``append`` 首次打开分片前确保 ``self.dir`` 存在——缺失即重建,**路径照常**
|
|
28
|
+
(仍是 ``self.path``,同一分片名,只补回目录这一层)。
|
|
29
|
+
② **容忍 ≠ 静默**(本仓 N225 已确立的判据):重建成功要可观测——模块级计数
|
|
30
|
+
``SHARD_DIR_REBUILDS`` + 有界样本 + stderr 汇总告警;**首建不记账**
|
|
31
|
+
(``where="init"`` = 建库时目录本就不存在,那是正常路径;计入会淹没真事件)。
|
|
32
|
+
③ 重建**失败**(父目录只读 / 权限不足 / 有文件占着该路径)抛结构化
|
|
33
|
+
``ShardDirError``:可机读 ``code=E_SHARD_DIR_UNREBUILDABLE`` + 可操作
|
|
34
|
+
``hint`` + ``path``,且**仍是 ``OSError`` 子类**(既有 ``except OSError``
|
|
35
|
+
面的语义不漂移);失败**同样记账 + 告警**——异常可能被上层兜底吞掉
|
|
36
|
+
(``MdCG.close`` 的 ``except (OSError, ValueError)``),痕迹不得只存在于
|
|
37
|
+
异常里。
|
|
38
|
+
④ ``read_all`` 既有语义**一字不改**:directory 不是目录 → 返回 ``[]``。
|
|
39
|
+
|
|
40
|
+
守卫构成(A 自愈面 / B 边界与既有语义 / C 重建失败的结构化 / D 生产链 /
|
|
41
|
+
E MCP 出口端到端):
|
|
42
|
+
A 6 条:删目录后 append 不抛 · 目录真被重建 · 删除后的记录真落进分片 ·
|
|
43
|
+
记账可断言(次数 + 样本)· stderr 告警可观测 · **路径照常**(分片名不换)
|
|
44
|
+
B 5 条:read_all 缺目录 → ``[]`` 不抛(要求④)· read_all 路径是文件 → ``[]``
|
|
45
|
+
不抛 · 首建不记账(计数 0 + stderr 干净)· 目录在位时零记账零告警(不产生
|
|
46
|
+
噪声)· clear 缺目录 → 直接返回不抛(既有语义)
|
|
47
|
+
C 9 条(有文件占着分片目录路径 ⇒ 重建必失败):抛 ``ShardDirError`` 而非裸
|
|
48
|
+
``FileNotFoundError`` · 仍是 ``OSError`` 子类 · ``code`` 可机读 · message
|
|
49
|
+
首字段含 ``[code]``(出口串里可机读)· ``hint`` 非空且含目录与处置 ·
|
|
50
|
+
``path`` 属性 · 失败也记账 · 失败也告警 · **打开面同口径**(append 重建失败
|
|
51
|
+
也抛同一结构化错误、``where=append`` 记账)
|
|
52
|
+
D 5 条(长驻 ``MdCG`` 实例,autoflush=1):删 ``_index_log`` 后再 add 不抛 ·
|
|
53
|
+
节点索引可见且盘面 ``.md`` 一致 · 分片日志可重放(索引不落后于盘面)·
|
|
54
|
+
生产链自愈也记账 · 生产链自愈也告警
|
|
55
|
+
E 4 条(`python -X utf8 -m md_cg.mcp_server` stdio 主循环 + 文件占住
|
|
56
|
+
``_index_log``):MCP 出口的 error 串含 ``E_SHARD_DIR_UNREBUILDABLE``(不是
|
|
57
|
+
裸 ``FileNotFoundError``)· 同一应答带可操作 ``hint`` · 错误之后进程仍应答
|
|
58
|
+
且 shutdown 后 rc=0 · 变异轮**注入自证**在场(fail-closed:注入失效不得被
|
|
59
|
+
读成「端到端面无判别力」)
|
|
60
|
+
|
|
61
|
+
``--branch-baseline`` 定点变异自证(与 ``test_n225_nonobject_load`` 同口径):
|
|
62
|
+
逐个关掉一处判据,守卫**必须转红**且红项**恰好**落在该判据负责的组、数量等于
|
|
63
|
+
表内预期;锚点漂移报 ``ANCHOR-MISS`` 并以**退出码 2** fail-closed 停手;基线源
|
|
64
|
+
是**工作区源码**(``inspect.getsource`` 取自当前已加载模块),绝不绑 git HEAD。
|
|
65
|
+
``--drop-fix`` 是「抽掉修复本体」的定点自证:只施加 M1(append 前不复查目录),
|
|
66
|
+
逐个打印转红的断言名与红项数,恢复后重跑并打印绿项数——「红→恢复→绿」三态
|
|
67
|
+
原样可见。
|
|
68
|
+
|
|
69
|
+
**并行编辑边界(如实)**:D/E 组依赖 ``md_cg`` 包内 ``MdCG``/``mcp_server`` 在
|
|
70
|
+
树内**完整可用**;本轮是多分片并行修改同一工作区,若 ``mdcg.py`` 正处于中间态
|
|
71
|
+
(类体被插断 ⇒ ``MdCG`` 缺 ``_write_node``),D/E 会整组红——那是**真红**(树
|
|
72
|
+
确实不可用),不是守卫自身的判别力问题;此时按「HEAD 基线树 + 本处 delta」的
|
|
73
|
+
隔离镜像跑本文件(见 FixReport)。
|
|
74
|
+
|
|
75
|
+
隔离纪律(与 ``test_n225_nonobject_load`` / ``test_n206_stdio_jsonrpc_type``
|
|
76
|
+
同款):env 剔净全部 MDCG_*/HIVE_* 注入面,只注入指向临时目录的
|
|
77
|
+
``MDCG_ROOT``/``MDCG_AUX_ROOT``/``MDCG_STATE_ROOT``/``MDCG_DATA_ROOT``/
|
|
78
|
+
``MDCG_TENANT_REGISTRY``(指向不存在的临时路径=空表回落)/``MDCG_SUSTAIN=0``/
|
|
79
|
+
``MDCG_LEGACY_ENV_AUTH=1``;全部合成库落在 ``tempfile.mkdtemp`` 下,跑完 rmtree。
|
|
80
|
+
**绝不触在役活库、真实令牌库与真实 serve。**
|
|
81
|
+
|
|
82
|
+
运行:
|
|
83
|
+
python -X utf8 -m md_cg.test_b4_shard_dir_selfheal
|
|
84
|
+
python -X utf8 -m md_cg.test_b4_shard_dir_selfheal --branch-baseline
|
|
85
|
+
python -X utf8 -m md_cg.test_b4_shard_dir_selfheal --drop-fix
|
|
86
|
+
"""
|
|
87
|
+
from __future__ import annotations
|
|
88
|
+
|
|
89
|
+
import contextlib
|
|
90
|
+
import inspect
|
|
91
|
+
import io
|
|
92
|
+
import json
|
|
93
|
+
import os
|
|
94
|
+
import shutil
|
|
95
|
+
import subprocess
|
|
96
|
+
import sys
|
|
97
|
+
import tempfile
|
|
98
|
+
import textwrap
|
|
99
|
+
from contextlib import redirect_stderr
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
103
|
+
except AttributeError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
from . import fsutil as F # noqa: E402
|
|
107
|
+
|
|
108
|
+
PASS, FAIL = 0, 0
|
|
109
|
+
FAILS = []
|
|
110
|
+
_CURRENT = ["?"]
|
|
111
|
+
_GROUP_FAILS = {}
|
|
112
|
+
_TMP_ROOTS = []
|
|
113
|
+
_LIVE = []
|
|
114
|
+
|
|
115
|
+
# 六要素正文(写入闸要求的 text 形态;D/E 组的写入用它)
|
|
116
|
+
BODY = ("# 功能名:B4 守卫合成节点\n# 生效条件:守卫运行期间\n# 子功能:占位\n"
|
|
117
|
+
"# 执行:占位\n# 验证方式:test\n# 不适用条件:非守卫运行")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# 生效条件:把 PASS/FAIL/失败名单/分组失败数全部清零并解除当前组名(每次完整跑套件前调用;--branch-baseline 每个变异轮都要一次干净基线)。
|
|
121
|
+
def _reset_counters():
|
|
122
|
+
global PASS, FAIL
|
|
123
|
+
PASS = FAIL = 0
|
|
124
|
+
del FAILS[:]
|
|
125
|
+
_GROUP_FAILS.clear()
|
|
126
|
+
_CURRENT[0] = "?"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# 生效条件:group 为非空字符串时设为当前组名并在 _GROUP_FAILS 里开出该组条目(未开则记 0);重复调用同名组只是切换当前组,不清零已有计数。
|
|
130
|
+
def begin(group: str):
|
|
131
|
+
_CURRENT[0] = group
|
|
132
|
+
_GROUP_FAILS.setdefault(group, 0)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# 生效条件:cond 为真记一条通过并打印;为假把 FAIL 与当前组失败数各 +1、把 name 追加进 FAILS 并打印 detail(detail 为空则只打印名字)。
|
|
136
|
+
def check(name, cond, detail=""):
|
|
137
|
+
global PASS, FAIL
|
|
138
|
+
if cond:
|
|
139
|
+
PASS += 1
|
|
140
|
+
print(" [ok] %s" % name)
|
|
141
|
+
return True
|
|
142
|
+
FAIL += 1
|
|
143
|
+
_GROUP_FAILS[_CURRENT[0]] = _GROUP_FAILS.get(_CURRENT[0], 0) + 1
|
|
144
|
+
FAILS.append(name)
|
|
145
|
+
print(" [FAIL] %s %s" % (name, detail))
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# 生效条件:fn 为无参回调时调用它取条件值——回调抛异常即判红并把异常类型与消息写进 detail(**不中断整组**,保证「变异下红项数 == 该组断言数」这一精确口径成立);fn 正常返回则按返回值真假记账;detail 传 callable 时在同一保护下求值。
|
|
150
|
+
def check_fn(name, fn, detail=""):
|
|
151
|
+
if callable(detail):
|
|
152
|
+
try:
|
|
153
|
+
detail = detail()
|
|
154
|
+
except Exception as exc: # noqa: BLE001
|
|
155
|
+
detail = "detail 求值抛 %s: %s" % (type(exc).__name__, exc)
|
|
156
|
+
try:
|
|
157
|
+
cond = bool(fn())
|
|
158
|
+
extra = detail
|
|
159
|
+
except Exception as exc: # noqa: BLE001 —— 红态正是抛异常
|
|
160
|
+
cond = False
|
|
161
|
+
extra = "回调抛 %s: %s(%s)" % (type(exc).__name__, exc, detail)
|
|
162
|
+
return check(name, cond, extra)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# 生效条件:新建系统临时目录(prefix 前缀)并把路径登记进 _TMP_ROOTS,供本次运行收尾统一 rmtree;返回该路径。
|
|
166
|
+
def _tmpdir(prefix: str) -> str:
|
|
167
|
+
p = tempfile.mkdtemp(prefix=prefix)
|
|
168
|
+
_TMP_ROOTS.append(p)
|
|
169
|
+
return p
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# 生效条件:无入参,对 _TMP_ROOTS 里每个目录 rmtree(ignore_errors=True) 并清空清单、对 _LIVE 里每个认知图实例尝试 close()(异常吞掉,收尾不该再抛);收尾是幂等的。
|
|
173
|
+
def _cleanup():
|
|
174
|
+
for cg in _LIVE:
|
|
175
|
+
try:
|
|
176
|
+
cg.close()
|
|
177
|
+
except Exception: # noqa: BLE001 —— 收尾路径不再抛
|
|
178
|
+
pass
|
|
179
|
+
del _LIVE[:]
|
|
180
|
+
for p in _TMP_ROOTS:
|
|
181
|
+
shutil.rmtree(p, ignore_errors=True)
|
|
182
|
+
del _TMP_ROOTS[:]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------- A/B/C 组 fixture(只碰 fsutil,不依赖 mdcg) ----------------
|
|
186
|
+
|
|
187
|
+
# 生效条件:无入参时在临时目录建一个分片实例写入 a0、close(生产写点每批写完即 close 句柄)→ 记录分片名 → 重置记账 → rmtree 分片目录 → 以 redirect_stderr 捕获告警后追加 a1、close、追加 a2、close;返回上下文 dict(d / base / stats / stderr / err / ids / files / dir_after)。
|
|
188
|
+
def _case_heal() -> dict:
|
|
189
|
+
tmp = _tmpdir("mdcg_b4_heal_")
|
|
190
|
+
d = os.path.join(tmp, "_index_log")
|
|
191
|
+
ctx = {"d": d, "base": None, "stats": None, "stderr": "", "err": None,
|
|
192
|
+
"ids": None, "files": None, "dir_after": None, "err0": None}
|
|
193
|
+
lg = None
|
|
194
|
+
try:
|
|
195
|
+
lg = F.ShardedLog(d)
|
|
196
|
+
lg.append({"id": "a0"})
|
|
197
|
+
lg.close()
|
|
198
|
+
ctx["base"] = os.path.basename(lg.path)
|
|
199
|
+
F.reset_shard_dir_stats()
|
|
200
|
+
shutil.rmtree(d)
|
|
201
|
+
buf = io.StringIO()
|
|
202
|
+
try:
|
|
203
|
+
with redirect_stderr(buf):
|
|
204
|
+
lg.append({"id": "a1"})
|
|
205
|
+
lg.close()
|
|
206
|
+
lg.append({"id": "a2"})
|
|
207
|
+
lg.close()
|
|
208
|
+
except Exception as exc: # noqa: BLE001 —— 修前此处即红
|
|
209
|
+
ctx["err"] = exc
|
|
210
|
+
ctx["stderr"] = buf.getvalue()
|
|
211
|
+
ctx["stats"] = F.shard_dir_stats()
|
|
212
|
+
ctx["dir_after"] = os.path.isdir(d)
|
|
213
|
+
ctx["files"] = sorted(os.listdir(d)) if os.path.isdir(d) else None
|
|
214
|
+
ctx["ids"] = [r.get("id") for r in F.ShardedLog.read_all(d)]
|
|
215
|
+
except Exception as exc: # noqa: BLE001
|
|
216
|
+
ctx["err0"] = exc
|
|
217
|
+
return ctx
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# 生效条件:无入参时跑两段「重建必失败」场景——① 用**文件占住**分片目录路径后构造 ShardedLog(构造面):isdir 为假、makedirs 必抛 FileExistsError;② 先正常构造再 rmdir 目录、用文件占住同路径后 append(打开面):同一条单点判据、同一结构化错误;返回上下文 dict(d / err / stats / stderr / d2 / err_append / stats2 / stderr2 / err0)。
|
|
221
|
+
def _case_fail() -> dict:
|
|
222
|
+
tmp = _tmpdir("mdcg_b4_fail_")
|
|
223
|
+
d = os.path.join(tmp, "_index_log")
|
|
224
|
+
ctx = {"d": d, "err": None, "stats": None, "stderr": "", "d2": None,
|
|
225
|
+
"err_append": None, "stats2": None, "stderr2": "", "err0": None}
|
|
226
|
+
try:
|
|
227
|
+
with open(d, "w", encoding="utf-8") as fh:
|
|
228
|
+
fh.write("squat") # 有文件占着该路径 → 重建必失败
|
|
229
|
+
F.reset_shard_dir_stats()
|
|
230
|
+
buf = io.StringIO()
|
|
231
|
+
try:
|
|
232
|
+
with redirect_stderr(buf):
|
|
233
|
+
F.ShardedLog(d) # 构造面(where="init")
|
|
234
|
+
except Exception as exc: # noqa: BLE001 —— 期望结构化错误
|
|
235
|
+
ctx["err"] = exc
|
|
236
|
+
ctx["stderr"] = buf.getvalue()
|
|
237
|
+
ctx["stats"] = F.shard_dir_stats()
|
|
238
|
+
|
|
239
|
+
d2 = os.path.join(tmp, "_index_log_append")
|
|
240
|
+
ctx["d2"] = d2
|
|
241
|
+
lg = F.ShardedLog(d2) # 先正常建好
|
|
242
|
+
os.rmdir(d2) # 再抽掉目录
|
|
243
|
+
with open(d2, "w", encoding="utf-8") as fh:
|
|
244
|
+
fh.write("squat")
|
|
245
|
+
F.reset_shard_dir_stats()
|
|
246
|
+
buf2 = io.StringIO()
|
|
247
|
+
try:
|
|
248
|
+
with redirect_stderr(buf2):
|
|
249
|
+
lg.append({"id": "c1"}) # 打开面(where="append")
|
|
250
|
+
except Exception as exc: # noqa: BLE001
|
|
251
|
+
ctx["err_append"] = exc
|
|
252
|
+
ctx["stderr2"] = buf2.getvalue()
|
|
253
|
+
ctx["stats2"] = F.shard_dir_stats()
|
|
254
|
+
except Exception as exc: # noqa: BLE001
|
|
255
|
+
ctx["err0"] = exc
|
|
256
|
+
return ctx
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------- D 生产链 fixture(长驻 MdCG 实例) ----------------
|
|
260
|
+
|
|
261
|
+
# 生效条件:无入参时在临时根建 MdCG(root, autoflush=1) 长驻实例并写入 p0(此时分片目录已建成)→ 重置记账 → 以 redirect_stderr 捕获告警后 rmtree(_index_log) 并再写 p1/p2;返回上下文 dict(root / cg / log_dir / stats / stderr / nodes / ids / on_disk / err)。
|
|
262
|
+
def _case_prod() -> dict:
|
|
263
|
+
from .mdcg import MdCG
|
|
264
|
+
tmp = _tmpdir("mdcg_b4_prod_")
|
|
265
|
+
root = os.path.join(tmp, "cgroot")
|
|
266
|
+
ctx = {"root": root, "cg": None, "log_dir": None, "stats": None,
|
|
267
|
+
"stderr": "", "nodes": None, "ids": None, "on_disk": None,
|
|
268
|
+
"err": None}
|
|
269
|
+
try:
|
|
270
|
+
cg = MdCG(root, autoflush=1)
|
|
271
|
+
ctx["cg"] = cg
|
|
272
|
+
_LIVE.append(cg)
|
|
273
|
+
cg.add("b4_p0", BODY, layer="knowledge")
|
|
274
|
+
log_dir = cg.index_log_dir
|
|
275
|
+
ctx["log_dir"] = log_dir
|
|
276
|
+
F.reset_shard_dir_stats()
|
|
277
|
+
buf = io.StringIO()
|
|
278
|
+
with redirect_stderr(buf):
|
|
279
|
+
shutil.rmtree(log_dir)
|
|
280
|
+
cg.add("b4_p1", BODY, layer="knowledge")
|
|
281
|
+
cg.add("b4_p2", BODY, layer="knowledge")
|
|
282
|
+
ctx["stderr"] = buf.getvalue()
|
|
283
|
+
ctx["stats"] = F.shard_dir_stats()
|
|
284
|
+
ctx["nodes"] = sorted(cg.index["nodes"])
|
|
285
|
+
ctx["ids"] = [r.get("id") for r in F.ShardedLog.read_all(log_dir)]
|
|
286
|
+
e = cg.index["nodes"].get("b4_p2") or {}
|
|
287
|
+
ctx["on_disk"] = os.path.exists(os.path.join(root, e.get("path") or ""))
|
|
288
|
+
except Exception as exc: # noqa: BLE001
|
|
289
|
+
ctx["err"] = exc
|
|
290
|
+
return ctx
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------- E stdio 端到端 fixture(MCP 出口) ----------------
|
|
294
|
+
|
|
295
|
+
# 模拟宿主前先把「身份/路径/编码」注入面清干净——子进程只吃临时目录(与 N225/N206 同款)
|
|
296
|
+
_DIRTY_KEYS = (
|
|
297
|
+
"MDCG_TOKEN", "MDCG_TOKEN_FILE", "MDCG_MASTER_KEY", "MDCG_CLEARANCE",
|
|
298
|
+
"MDCG_TENANT", "MDCG_TENANT_REGISTRY",
|
|
299
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_LEGACY_ENV_AUTH",
|
|
300
|
+
"MDCG_LEGACY_ENV_ADMIN", "MDCG_SESSION", "DSH_SESSION_ID",
|
|
301
|
+
"MDCG_HARNESS", "MDCG_UNIT", "MDCG_ROOT", "MDCG_STATE_ROOT",
|
|
302
|
+
"MDCG_DATA_ROOT", "MDCG_AUX_ROOT", "MDCG_SUSTAIN", "MDCG_SUSTAIN_NAME",
|
|
303
|
+
"MDCG_MCP_SURFACE", "MDCG_TOOL_FACE", "MDCG_ACTOR",
|
|
304
|
+
"MDCG_VERIFIER_MODULES", "MDCG_HIVE_JOBS",
|
|
305
|
+
"PYTHONUTF8", "PYTHONIOENCODING", "PYTHONLEGACYWINDOWSSTDIO",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# 生效条件:tmp 为本次隔离根时,返回剔净 _DIRTY_KEYS 后的 os.environ 副本再注入指向 tmp 的覆盖键(MDCG_ROOT / MDCG_TENANT_REGISTRY / MDCG_STATE_ROOT / MDCG_DATA_ROOT / MDCG_AUX_ROOT / MDCG_TOKEN_FILE / MDCG_MASTER_KEY / MDCG_SUSTAIN / MDCG_LEGACY_ENV_AUTH / MDCG_ACTOR)——子进程绝不读真实 ~/.mdcg、真实令牌库与真实密钥面。
|
|
310
|
+
def _env(tmp: str) -> dict:
|
|
311
|
+
env = {k: v for k, v in os.environ.items() if k not in _DIRTY_KEYS}
|
|
312
|
+
env["MDCG_ROOT"] = os.path.join(tmp, "cgroot")
|
|
313
|
+
env["MDCG_TENANT_REGISTRY"] = os.path.join(tmp, "_tenants_absent.json")
|
|
314
|
+
env["MDCG_STATE_ROOT"] = os.path.join(tmp, "state")
|
|
315
|
+
env["MDCG_DATA_ROOT"] = os.path.join(tmp, "data")
|
|
316
|
+
env["MDCG_AUX_ROOT"] = os.path.join(tmp, "auxroot") # 不可取名 "aux"(设备名)
|
|
317
|
+
env["MDCG_TOKEN_FILE"] = os.path.join(tmp, "tokens.json")
|
|
318
|
+
env["MDCG_MASTER_KEY"] = "00" * 32 # 哑主密钥
|
|
319
|
+
env["MDCG_SUSTAIN"] = "0" # 关常驻循环
|
|
320
|
+
env["MDCG_LEGACY_ENV_AUTH"] = "1" # 既有豁免面:零令牌
|
|
321
|
+
env["MDCG_ACTOR"] = "b4-guard"
|
|
322
|
+
return env
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# 当前生效的变异(仅 --branch-baseline/--drop-fix 期间非 None):内存 patch 不跨
|
|
326
|
+
# 进程,E 组走独立子进程 ⇒ 靠 sitecustomize 把同一处定点变异注入子进程,端到端
|
|
327
|
+
# 面才有真正的判别力。注入以 stderr 标记自证,标记缺失即 E 组判红(fail-closed)。
|
|
328
|
+
_MUTATION = [None]
|
|
329
|
+
|
|
330
|
+
_SITECUSTOMIZE_SRC = '''# B4 守卫跨进程变异注入(临时件,随 tmp 目录一并回收)
|
|
331
|
+
import os
|
|
332
|
+
import sys
|
|
333
|
+
import textwrap
|
|
334
|
+
import inspect
|
|
335
|
+
_repo = os.environ.get("B4_REPO") or ""
|
|
336
|
+
if _repo and _repo not in sys.path:
|
|
337
|
+
sys.path.insert(0, _repo)
|
|
338
|
+
try:
|
|
339
|
+
from md_cg import fsutil as _F
|
|
340
|
+
_which = os.environ["B4_MUT_WHICH"]
|
|
341
|
+
_fname = os.environ["B4_MUT_FUNC"]
|
|
342
|
+
_old = os.environ["B4_MUT_OLD"]
|
|
343
|
+
_new = os.environ["B4_MUT_NEW"]
|
|
344
|
+
if _which == "fsutil":
|
|
345
|
+
_holder, _raw = _F.ShardedLog, _F.ShardedLog.__dict__[_fname]
|
|
346
|
+
else: # fsutil_mod:模块级函数
|
|
347
|
+
_holder, _raw = _F, _F.__dict__[_fname]
|
|
348
|
+
_src = inspect.getsource(getattr(_raw, "__func__", _raw))
|
|
349
|
+
_src = textwrap.dedent(_src[_src.index("def %s(" % _fname):])
|
|
350
|
+
# 真全局(vars(_F) 副本会让被测函数的 `global X; X += 1` 写进临时 dict,
|
|
351
|
+
# 父进程断言读到的恒为 0 ⇒ 那是 patch 机制的假红,不是判据红)。
|
|
352
|
+
_ns = _F.__dict__
|
|
353
|
+
exec(compile(_src.replace(_old, _new), "b4_mut.py", "exec"), _ns)
|
|
354
|
+
if _which == "fsutil":
|
|
355
|
+
setattr(_holder, _fname, staticmethod(_ns[_fname]))
|
|
356
|
+
else:
|
|
357
|
+
setattr(_holder, _fname, _ns[_fname])
|
|
358
|
+
sys.stderr.write("[b4-mut] injected %s.%s\\n" % (_which, _fname))
|
|
359
|
+
except Exception as _e:
|
|
360
|
+
sys.stderr.write("[b4-mut] inject FAILED: %r\\n" % (_e,))
|
|
361
|
+
'''
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# 生效条件:lines 为待喂给 stdin 的字符串序列、tmp 为隔离根时,spawn `python -X utf8 -m md_cg.mcp_server`(cwd=包根、env=_env(tmp);变异轮另经 sitecustomize 注入)一次喂完并 communicate;超时则 kill 后取残余输出;随后删掉**本子进程自己**的自报戳;返回 (returncode, stdout, stderr)。
|
|
365
|
+
def _talk(lines, tmp: str, timeout: int = 180):
|
|
366
|
+
env = _env(tmp)
|
|
367
|
+
mut = _MUTATION[0]
|
|
368
|
+
if mut is not None:
|
|
369
|
+
sc_dir = os.path.join(tmp, "_mutpath")
|
|
370
|
+
os.makedirs(sc_dir, exist_ok=True)
|
|
371
|
+
with open(os.path.join(sc_dir, "sitecustomize.py"), "w",
|
|
372
|
+
encoding="utf-8") as f:
|
|
373
|
+
f.write(_SITECUSTOMIZE_SRC)
|
|
374
|
+
env["PYTHONPATH"] = sc_dir + os.pathsep + env.get("PYTHONPATH", "")
|
|
375
|
+
env["B4_REPO"] = os.path.dirname(os.path.dirname(
|
|
376
|
+
os.path.abspath(__file__)))
|
|
377
|
+
env["B4_MUT_WHICH"] = mut["which"]
|
|
378
|
+
env["B4_MUT_FUNC"] = mut["func"]
|
|
379
|
+
env["B4_MUT_OLD"] = mut["old"]
|
|
380
|
+
env["B4_MUT_NEW"] = mut["new"]
|
|
381
|
+
proc = subprocess.Popen(
|
|
382
|
+
[sys.executable, "-X", "utf8", "-m", "md_cg.mcp_server"],
|
|
383
|
+
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
384
|
+
env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
385
|
+
stderr=subprocess.PIPE, text=True, encoding="utf-8", errors="replace")
|
|
386
|
+
try:
|
|
387
|
+
out, err = proc.communicate("\n".join(lines) + "\n", timeout=timeout)
|
|
388
|
+
except subprocess.TimeoutExpired:
|
|
389
|
+
proc.kill()
|
|
390
|
+
out, err = proc.communicate()
|
|
391
|
+
try:
|
|
392
|
+
os.remove(os.path.join(tempfile.gettempdir(), "md_cg_servers",
|
|
393
|
+
"%d.json" % proc.pid))
|
|
394
|
+
except OSError:
|
|
395
|
+
pass
|
|
396
|
+
return proc.returncode, out or "", err or ""
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# 生效条件:rid/method 给定、params 为 None 时省略该键,否则原样写入;返回一行紧凑 JSON-RPC 请求文本。
|
|
400
|
+
def _rpc(rid, method, params=None) -> str:
|
|
401
|
+
msg = {"jsonrpc": "2.0", "id": rid, "method": method}
|
|
402
|
+
if params is not None:
|
|
403
|
+
msg["params"] = params
|
|
404
|
+
return json.dumps(msg, ensure_ascii=False)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# 生效条件:out 为 stdout 文本时逐行 strip、空行跳过,返回 [(原始行, 解析结果或 None)](解析失败记 None)。
|
|
408
|
+
def _resp_lines(out: str):
|
|
409
|
+
got = []
|
|
410
|
+
for ln in out.splitlines():
|
|
411
|
+
ln = ln.strip()
|
|
412
|
+
if not ln:
|
|
413
|
+
continue
|
|
414
|
+
try:
|
|
415
|
+
got.append((ln, json.loads(ln)))
|
|
416
|
+
except ValueError:
|
|
417
|
+
got.append((ln, None))
|
|
418
|
+
return got
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# 生效条件:无入参时在临时根用**文件占住** `_index_log` 路径(重建必失败)→ 起 stdio 主循环喂 [initialize, mdcg_remember(b4_e1), tools/list, shutdown];返回上下文 dict(tmp / rc / out / err / by_id / tool_text / tool_obj / inject_ok / error)。
|
|
422
|
+
def _case_stdio() -> dict:
|
|
423
|
+
tmp = _tmpdir("mdcg_b4_stdio_")
|
|
424
|
+
ctx = {"tmp": tmp, "rc": None, "out": "", "err": "", "by_id": {},
|
|
425
|
+
"tool_text": "", "tool_obj": None, "inject_ok": True,
|
|
426
|
+
"error": None}
|
|
427
|
+
try:
|
|
428
|
+
root = os.path.join(tmp, "cgroot")
|
|
429
|
+
os.makedirs(root, exist_ok=True)
|
|
430
|
+
with open(os.path.join(root, "_index_log"), "w",
|
|
431
|
+
encoding="utf-8") as fh:
|
|
432
|
+
fh.write("squat") # 目录路径被文件占住
|
|
433
|
+
lines = [_rpc(1, "initialize"),
|
|
434
|
+
_rpc(2, "tools/call",
|
|
435
|
+
{"name": "mdcg_remember",
|
|
436
|
+
"arguments": {"node_id": "b4_e1", "content": BODY}}),
|
|
437
|
+
_rpc(3, "tools/list"), _rpc(4, "shutdown")]
|
|
438
|
+
ctx["rc"], ctx["out"], ctx["err"] = _talk(lines, tmp)
|
|
439
|
+
ctx["inject_ok"] = ("[b4-mut] injected" in ctx["err"]
|
|
440
|
+
if _MUTATION[0] is not None else True)
|
|
441
|
+
objs = [o for _raw, o in _resp_lines(ctx["out"])]
|
|
442
|
+
ctx["by_id"] = {o.get("id"): o for o in objs if isinstance(o, dict)}
|
|
443
|
+
call = ctx["by_id"].get(2) or {}
|
|
444
|
+
content = (call.get("result") or {}).get("content") or []
|
|
445
|
+
ctx["tool_text"] = content[0].get("text") if content else ""
|
|
446
|
+
try:
|
|
447
|
+
ctx["tool_obj"] = json.loads(ctx["tool_text"])
|
|
448
|
+
except Exception: # noqa: BLE001 —— 非 JSON 也照实记录
|
|
449
|
+
ctx["tool_obj"] = None
|
|
450
|
+
except Exception as exc: # noqa: BLE001
|
|
451
|
+
ctx["error"] = exc
|
|
452
|
+
return ctx
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ---------------- A 自愈面 ----------------
|
|
456
|
+
|
|
457
|
+
# 生效条件:跑 A 组 6 条断言(append 不抛 / 目录被重建 / 删除后的记录真落进分片 / 记账可断言 / stderr 告警可观测 / 路径照常);ctx 由 _case_heal 产出,任一阶段抛异常时六条各自判红。
|
|
458
|
+
def g_a():
|
|
459
|
+
begin("A")
|
|
460
|
+
ctx = _case_heal()
|
|
461
|
+
check_fn("B4-A1·删目录后 append 不抛(修前为 FileNotFoundError)",
|
|
462
|
+
lambda c=ctx: c["err"] is None and c["err0"] is None,
|
|
463
|
+
"err=%r err0=%r" % (ctx["err"], ctx["err0"]))
|
|
464
|
+
check_fn("B4-A2·分片目录被重建(isdir 为真)",
|
|
465
|
+
lambda c=ctx: c["dir_after"] is True,
|
|
466
|
+
"dir_after=%r" % (ctx["dir_after"],))
|
|
467
|
+
check_fn("B4-A3·删除后的记录真落进分片(read_all 含 a1/a2)",
|
|
468
|
+
lambda c=ctx: c["ids"] == ["a1", "a2"],
|
|
469
|
+
"ids=%r" % (ctx["ids"],))
|
|
470
|
+
check_fn("B4-A4·记账可断言(1 次重建 + 样本 where=append)",
|
|
471
|
+
lambda c=ctx: c["stats"] is not None
|
|
472
|
+
and c["stats"][0] == 1 and c["stats"][2] == 0
|
|
473
|
+
and len(c["stats"][1]) == 1
|
|
474
|
+
and c["stats"][1][0][1] == "append",
|
|
475
|
+
"stats=%r" % (ctx["stats"],))
|
|
476
|
+
check_fn("B4-A5·不静默(stderr 含「缺失」与目录名)",
|
|
477
|
+
lambda c=ctx: "分片目录缺失" in c["stderr"]
|
|
478
|
+
and "_index_log" in c["stderr"],
|
|
479
|
+
"stderr=%r" % (ctx["stderr"][:200],))
|
|
480
|
+
check_fn("B4-A6·路径照常(分片名与删前同名,不换名)",
|
|
481
|
+
lambda c=ctx: c["files"] == [c["base"]],
|
|
482
|
+
"files=%r base=%r" % (ctx["files"], ctx["base"]))
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# ---------------- B 边界与既有语义 ----------------
|
|
486
|
+
|
|
487
|
+
# 生效条件:跑 B 组 5 条断言(read_all 缺目录 → [] / read_all 路径是文件 → [] / 首建不记账 / 目录在位零记账零告警 / clear 缺目录不抛),全部就地构造最小场景、不依赖 mdcg。
|
|
488
|
+
def g_b():
|
|
489
|
+
begin("B")
|
|
490
|
+
tmp = _tmpdir("mdcg_b4_edge_")
|
|
491
|
+
missing = os.path.join(tmp, "_index_log_absent")
|
|
492
|
+
squat = os.path.join(tmp, "_index_log_squat")
|
|
493
|
+
with open(squat, "w", encoding="utf-8") as fh:
|
|
494
|
+
fh.write("squat")
|
|
495
|
+
|
|
496
|
+
got_missing, err_missing = None, None
|
|
497
|
+
try:
|
|
498
|
+
got_missing = F.ShardedLog.read_all(missing)
|
|
499
|
+
except Exception as exc: # noqa: BLE001
|
|
500
|
+
err_missing = exc
|
|
501
|
+
check_fn("B4-B1·read_all 缺目录 → [] 不抛(既有语义一字不改)",
|
|
502
|
+
lambda: err_missing is None and got_missing == [],
|
|
503
|
+
"got=%r err=%r" % (got_missing, err_missing))
|
|
504
|
+
|
|
505
|
+
got_squat, err_squat = None, None
|
|
506
|
+
try:
|
|
507
|
+
got_squat = F.ShardedLog.read_all(squat)
|
|
508
|
+
except Exception as exc: # noqa: BLE001
|
|
509
|
+
err_squat = exc
|
|
510
|
+
check_fn("B4-B2·read_all 路径是文件 → [] 不抛(非目录降级口径)",
|
|
511
|
+
lambda: err_squat is None and got_squat == [],
|
|
512
|
+
"got=%r err=%r" % (got_squat, err_squat))
|
|
513
|
+
|
|
514
|
+
fresh = os.path.join(tmp, "_index_log_fresh")
|
|
515
|
+
F.reset_shard_dir_stats()
|
|
516
|
+
st = None
|
|
517
|
+
buf = io.StringIO()
|
|
518
|
+
with redirect_stderr(buf):
|
|
519
|
+
lg = F.ShardedLog(fresh)
|
|
520
|
+
lg.append({"id": "b_fresh"})
|
|
521
|
+
lg.close()
|
|
522
|
+
st = F.shard_dir_stats()
|
|
523
|
+
check_fn("B4-B3·首建不记账(构造 + 首写:计数 0、stderr 干净)",
|
|
524
|
+
lambda: st == (0, (), 0, ()) and buf.getvalue() == "",
|
|
525
|
+
"stats=%r stderr=%r" % (st, buf.getvalue()[:120]))
|
|
526
|
+
|
|
527
|
+
F.reset_shard_dir_stats()
|
|
528
|
+
st2 = None
|
|
529
|
+
buf2 = io.StringIO()
|
|
530
|
+
with redirect_stderr(buf2):
|
|
531
|
+
lg2 = F.ShardedLog(fresh) # 目录已存在
|
|
532
|
+
lg2.append({"id": "b_exist"})
|
|
533
|
+
lg2.close()
|
|
534
|
+
st2 = F.shard_dir_stats()
|
|
535
|
+
check_fn("B4-B4·目录在位时零记账零告警(不产生噪声)",
|
|
536
|
+
lambda: st2 == (0, (), 0, ()) and buf2.getvalue() == ""
|
|
537
|
+
and os.path.isdir(fresh),
|
|
538
|
+
"stats=%r stderr=%r" % (st2, buf2.getvalue()[:120]))
|
|
539
|
+
|
|
540
|
+
err_clear = None
|
|
541
|
+
try:
|
|
542
|
+
F.ShardedLog.clear(os.path.join(tmp, "_index_log_absent2"))
|
|
543
|
+
except Exception as exc: # noqa: BLE001
|
|
544
|
+
err_clear = exc
|
|
545
|
+
check_fn("B4-B5·clear 缺目录 → 直接返回不抛(既有语义)",
|
|
546
|
+
lambda: err_clear is None, "err=%r" % (err_clear,))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ---------------- C 重建失败的结构化 ----------------
|
|
550
|
+
|
|
551
|
+
# 生效条件:跑 C 组 9 条断言(构造面:抛 ShardDirError 而非裸 FileNotFoundError / 仍是 OSError 子类 / code 可机读 / message 首字段含 [code] / hint 非空且含目录与处置 / path 属性 / 失败也记账 / 失败也告警;打开面:append 走同一条单点判据、同一结构化错误与 where=append 样本);ctx 由 _case_fail 产出。
|
|
552
|
+
def g_c():
|
|
553
|
+
begin("C")
|
|
554
|
+
ctx = _case_fail()
|
|
555
|
+
e = ctx["err"]
|
|
556
|
+
check_fn("B4-C1·抛 ShardDirError(不是裸 FileNotFoundError)",
|
|
557
|
+
lambda: isinstance(e, F.ShardDirError),
|
|
558
|
+
"err=%r err0=%r" % (e, ctx["err0"]))
|
|
559
|
+
check_fn("B4-C2·仍是 OSError 子类(既有 except OSError 面语义不变)",
|
|
560
|
+
lambda: isinstance(e, OSError), "type=%s" % type(e).__name__)
|
|
561
|
+
check_fn("B4-C3·code 可机读(== E_SHARD_DIR_UNREBUILDABLE)",
|
|
562
|
+
lambda: getattr(e, "code", None) == F.SHARD_DIR_ERR_CODE,
|
|
563
|
+
"code=%r" % (getattr(e, "code", None),))
|
|
564
|
+
check_fn("B4-C4·message 首字段含 [code](MCP 出口 error 串里可机读)",
|
|
565
|
+
lambda: ("[%s]" % F.SHARD_DIR_ERR_CODE) in str(e),
|
|
566
|
+
"str=%r" % (str(e)[:160],))
|
|
567
|
+
check_fn("B4-C5·hint 非空且含目录与处置(重开进程即可恢复索引)",
|
|
568
|
+
lambda: bool(getattr(e, "hint", ""))
|
|
569
|
+
and ctx["d"] in e.hint and "重开进程" in e.hint,
|
|
570
|
+
"hint=%r" % (getattr(e, "hint", "")[:160],))
|
|
571
|
+
check_fn("B4-C6·path 属性 == 分片目录(可机读定位)",
|
|
572
|
+
lambda: getattr(e, "path", None) == ctx["d"],
|
|
573
|
+
"path=%r d=%r" % (getattr(e, "path", None), ctx["d"]))
|
|
574
|
+
check_fn("B4-C7·失败也记账(1 次失败 + 样本含 where 与异常类型名)",
|
|
575
|
+
lambda: ctx["stats"] is not None and ctx["stats"][2] == 1
|
|
576
|
+
and len(ctx["stats"][3]) == 1
|
|
577
|
+
and ctx["stats"][3][0][1] == "init"
|
|
578
|
+
and ctx["stats"][3][0][2] == "FileExistsError",
|
|
579
|
+
"stats=%r" % (ctx["stats"],))
|
|
580
|
+
check_fn("B4-C8·失败也告警(stderr 含「重建失败」与 code)",
|
|
581
|
+
lambda: "重建失败" in ctx["stderr"]
|
|
582
|
+
and F.SHARD_DIR_ERR_CODE in ctx["stderr"],
|
|
583
|
+
"stderr=%r" % (ctx["stderr"][:200],))
|
|
584
|
+
check_fn("B4-C9·打开面同口径:append 重建失败也抛结构化 + where=append 记账",
|
|
585
|
+
lambda c=ctx: isinstance(c["err_append"], F.ShardDirError)
|
|
586
|
+
and getattr(c["err_append"], "code", None) == F.SHARD_DIR_ERR_CODE
|
|
587
|
+
and c["stats2"][2] == 1 and c["stats2"][3][0][1] == "append",
|
|
588
|
+
"err=%r stats2=%r" % (ctx["err_append"], ctx["stats2"]))
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ---------------- D 生产链(长驻 MdCG 实例) ----------------
|
|
592
|
+
|
|
593
|
+
# 生效条件:跑 D 组 5 条断言(删 _index_log 后再 add 不抛 / 节点索引可见且盘面 .md 一致 / 分片日志可重放 / 生产链自愈也记账 / 生产链自愈也告警);ctx 由 _case_prod 产出,MdCG 不可用时五条各自判红。
|
|
594
|
+
def g_d():
|
|
595
|
+
begin("D")
|
|
596
|
+
ctx = _case_prod()
|
|
597
|
+
check_fn("B4-D1·生产链:删 _index_log 后再 add 不抛(修前为 FileNotFoundError)",
|
|
598
|
+
lambda c=ctx: c["err"] is None, "err=%r" % (ctx["err"],))
|
|
599
|
+
check_fn("B4-D2·删除后写入的节点索引可见且盘面 .md 存在(索引不落后于盘面)",
|
|
600
|
+
lambda c=ctx: c["nodes"] is not None
|
|
601
|
+
and {"b4_p0", "b4_p1", "b4_p2"} <= set(c["nodes"])
|
|
602
|
+
and c["on_disk"] is True,
|
|
603
|
+
"nodes=%r on_disk=%r" % (ctx["nodes"], ctx["on_disk"]))
|
|
604
|
+
check_fn("B4-D3·分片日志可重放(read_all 含删除后写入的 p1/p2)",
|
|
605
|
+
lambda c=ctx: c["ids"] is not None
|
|
606
|
+
and {"b4_p1", "b4_p2"} <= set(c["ids"]),
|
|
607
|
+
"ids=%r" % (ctx["ids"],))
|
|
608
|
+
check_fn("B4-D4·生产链自愈也记账(1 次重建 + 样本 where=append)",
|
|
609
|
+
lambda c=ctx: c["stats"] is not None and c["stats"][0] == 1
|
|
610
|
+
and c["stats"][2] == 0 and len(c["stats"][1]) == 1
|
|
611
|
+
and c["stats"][1][0][1] == "append",
|
|
612
|
+
"stats=%r" % (ctx["stats"],))
|
|
613
|
+
check_fn("B4-D5·生产链自愈也告警(stderr 含「缺失」与 _index_log)",
|
|
614
|
+
lambda c=ctx: "分片目录缺失" in c["stderr"]
|
|
615
|
+
and "_index_log" in c["stderr"],
|
|
616
|
+
"stderr=%r" % (ctx["stderr"][:200],))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# ---------------- E MCP 出口端到端 ----------------
|
|
620
|
+
|
|
621
|
+
# 生效条件:跑 E 组 4 条断言(出口 error 串含 code / 出口带可操作 hint / 错误之后进程仍应答且 rc=0 / 变异轮注入自证在场);ctx 由 _case_stdio 产出。
|
|
622
|
+
def g_e():
|
|
623
|
+
begin("E")
|
|
624
|
+
ctx = _case_stdio()
|
|
625
|
+
obj = ctx["tool_obj"]
|
|
626
|
+
check_fn("B4-E1·MCP 出口不裸抛:error 串含 E_SHARD_DIR_UNREBUILDABLE",
|
|
627
|
+
lambda c=ctx: isinstance(c["tool_obj"], dict)
|
|
628
|
+
and F.SHARD_DIR_ERR_CODE in (c["tool_obj"].get("error") or "")
|
|
629
|
+
and "ShardDirError" in (c["tool_obj"].get("error") or ""),
|
|
630
|
+
"tool_text=%r" % (ctx["tool_text"][:220],))
|
|
631
|
+
check_fn("B4-E2·MCP 出口带可操作 hint(含「重开进程」处置)",
|
|
632
|
+
lambda: isinstance(obj, dict)
|
|
633
|
+
and "重开进程" in (obj.get("hint") or ""),
|
|
634
|
+
"hint=%r" % ((obj or {}).get("hint") or "")[:160])
|
|
635
|
+
check_fn("B4-E3·错误之后进程仍应答(tools/list 有 id=3 应答)且 rc=0",
|
|
636
|
+
lambda c=ctx: c["rc"] == 0 and 3 in c["by_id"],
|
|
637
|
+
"rc=%r ids=%r stderr=%r" % (ctx["rc"], sorted(ctx["by_id"]),
|
|
638
|
+
ctx["err"][-200:]))
|
|
639
|
+
check_fn("B4-E4·变异轮注入自证在场(fail-closed:注入失效即判红)",
|
|
640
|
+
lambda c=ctx: c["inject_ok"] is True,
|
|
641
|
+
"inject_ok=%r stderr=%r" % (ctx["inject_ok"], ctx["err"][-200:]))
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
_GROUPS = (("A", g_a), ("B", g_b), ("C", g_c), ("D", g_d), ("E", g_e))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# 生效条件:重算计数器后依次跑 A–E 五组(静默与否由 print 决定),返回 (总失败数, {组名: 失败数} 的副本);供正常模式与 --branch-baseline 的变异核验共用。
|
|
648
|
+
def _run_groups():
|
|
649
|
+
_reset_counters()
|
|
650
|
+
for _name, fn in _GROUPS:
|
|
651
|
+
fn()
|
|
652
|
+
return FAIL, dict(_GROUP_FAILS)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# ---------------- 定点变异自证 ----------------
|
|
656
|
+
|
|
657
|
+
# 变异表:逐处关掉一个判据,守卫**必须转红**,且红项**恰好**落在该判据负责的
|
|
658
|
+
# 组、数量等于表内预期(多一项少一项都报红)。表内数字是**实跑读数**(不是
|
|
659
|
+
# 设计意图的估计),每条后面的「外溢面」注释说明该变异为什么牵连到别的组——
|
|
660
|
+
# 红项数不是越小越好,而是必须与真实因果一致。基线源=**工作区源码**
|
|
661
|
+
# (inspect.getsource 取自当前已加载模块),绝不绑 git HEAD。
|
|
662
|
+
_MUTATIONS = (
|
|
663
|
+
# ① 修复本体:append 首开前不复查目录(= 修前行为)。
|
|
664
|
+
# 外溢面(如实登记,不掩盖):
|
|
665
|
+
# · C9(append 面的结构化失败)——自愈调用被抽掉后 append 改由 open 抛裸
|
|
666
|
+
# NotADirectoryError;
|
|
667
|
+
# · D 全组 5 条——生产链 fixture 在第一次 add 就抛,后续读数(nodes /
|
|
668
|
+
# read_all / 记账 / 告警)连带落空(修前实测就是这样:一次抛即整链停)。
|
|
669
|
+
("M1 抽掉修复本体(append 打开前不复查目录)",
|
|
670
|
+
"fsutil", "append",
|
|
671
|
+
'ensure_shard_dir(self.dir, "append")', "pass",
|
|
672
|
+
{"A": 6, "C": 1, "D": 5}),
|
|
673
|
+
# ② 自愈静默:重建成功不记账(容忍 ≠ 静默的计数面)
|
|
674
|
+
("M2 自愈不记账(计数不累加)",
|
|
675
|
+
"fsutil_mod", "note_shard_dir_rebuild",
|
|
676
|
+
"SHARD_DIR_REBUILDS += 1", "pass",
|
|
677
|
+
{"A": 1, "D": 1}),
|
|
678
|
+
# ③ 自愈静默:重建成功不告警(stderr 面)
|
|
679
|
+
("M3 自愈不告警(stderr 静默)",
|
|
680
|
+
"fsutil_mod", "note_shard_dir_rebuild",
|
|
681
|
+
" sys.stderr.write(", " (lambda *a: None)(",
|
|
682
|
+
{"A": 1, "D": 1}),
|
|
683
|
+
# ④ 重建失败不记账不告警(痕迹只剩异常,被兜底吞掉即失明)。
|
|
684
|
+
# 外溢面:C9 同轮转红(打开面复用同一记账函数)。
|
|
685
|
+
("M4 重建失败不记账不告警",
|
|
686
|
+
"fsutil_mod", "ensure_shard_dir",
|
|
687
|
+
"note_shard_dir_heal_failure(directory, where, exc)", "pass",
|
|
688
|
+
{"C": 3}),
|
|
689
|
+
# ⑤ 重建失败降级为裸 FileNotFoundError(③ 的反面:冒到出口)。
|
|
690
|
+
# 外溢面:E1/E2(出口串里 code 与 hint 双失)——这正是本条要钉的死点。
|
|
691
|
+
("M5 重建失败降级为裸 FileNotFoundError",
|
|
692
|
+
"fsutil_mod", "ensure_shard_dir",
|
|
693
|
+
"note_shard_dir_heal_failure(directory, where, exc)",
|
|
694
|
+
"raise FileNotFoundError(directory)",
|
|
695
|
+
{"C": 8, "E": 2}),
|
|
696
|
+
# ⑥ 口径过宽:首建也记账(把正常路径当病态事件,淹没真事件)
|
|
697
|
+
("M6 首建也记账(口径过宽)",
|
|
698
|
+
"fsutil_mod", "note_shard_dir_rebuild",
|
|
699
|
+
' if where == "init":\n return', ' if False:\n return',
|
|
700
|
+
{"B": 1}),
|
|
701
|
+
# ⑦ 改掉 read_all「非目录 → []」语义(要求④ 的反面)。
|
|
702
|
+
# 外溢面:D 全组 + E 全组——read_all 抛异常会打断 MdCG 装载链
|
|
703
|
+
# (`_load_index` 在盘面无快照时经它重放),常驻服务连启动都过不去;
|
|
704
|
+
# 这正是要求④「不许改 read_all 既有语义」的**因果理由**,不是偶然牵连。
|
|
705
|
+
("M7 read_all 非目录不再返回空列表",
|
|
706
|
+
"fsutil", "read_all", " return []", " pass",
|
|
707
|
+
{"B": 2, "D": 5, "E": 3}),
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# 生效条件:which 为 "fsutil" 时从 ShardedLog 类、为 "fsutil_mod" 时从 fsutil 模块取 func_name 的源码文本;**从 `def <name>(` 行起截断再 dedent**(inspect.getsource 对本仓「顶格 `# 生效条件:` 注释 + 类内缩进 def」的排版会把注释一并带来,直接 dedent 会留下缩进 ⇒ IndentationError);函数缺失或无法取源时返回 None。
|
|
712
|
+
def _func_src(which: str, func_name: str):
|
|
713
|
+
try:
|
|
714
|
+
if which == "fsutil":
|
|
715
|
+
raw = F.ShardedLog.__dict__[func_name]
|
|
716
|
+
else:
|
|
717
|
+
raw = F.__dict__[func_name]
|
|
718
|
+
fn = getattr(raw, "__func__", raw)
|
|
719
|
+
src = inspect.getsource(fn)
|
|
720
|
+
return textwrap.dedent(src[src.index("def %s(" % func_name):])
|
|
721
|
+
except Exception: # noqa: BLE001 —— 取源失败按锚点漂移处理
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# 生效条件:which 为 "fsutil"/"fsutil_mod"、old 在 func_name 源码中时,把替换后的源码 exec 进 **fsutil 模块的真实命名空间**(`vars(F)` 而非副本——被测函数里的 `global X; X += 1` 必须落到模块真全局,副本会让计数写在临时 dict 里、守卫读到的恒为 0,那是 patch 机制的假红)并挂回目标(类属性或模块级名字;原为 staticmethod 者仍包 staticmethod),返回还原回调;old 不在源码中返回 None(调用方按 ANCHOR-MISS 处置,退出码 2)。
|
|
726
|
+
def _patch(which: str, func_name: str, old: str, new: str):
|
|
727
|
+
src = _func_src(which, func_name) # 取源必须在 exec 之前(exec 后原件被覆盖)
|
|
728
|
+
if src is None or old not in src:
|
|
729
|
+
return None
|
|
730
|
+
holder = F.ShardedLog if which == "fsutil" else F
|
|
731
|
+
orig = holder.__dict__[func_name] # ← 先存原件:exec 会就地覆盖目标名字
|
|
732
|
+
ns = F.__dict__ # 真全局:global 语句必须落在被测模块命名空间
|
|
733
|
+
exec(compile(src.replace(old, new), "b4_mut.py", "exec"), ns)
|
|
734
|
+
if isinstance(orig, staticmethod):
|
|
735
|
+
setattr(holder, func_name, staticmethod(ns[func_name]))
|
|
736
|
+
else:
|
|
737
|
+
setattr(holder, func_name, ns[func_name])
|
|
738
|
+
|
|
739
|
+
def _restore():
|
|
740
|
+
setattr(holder, func_name, orig)
|
|
741
|
+
if holder is not F and ns.get(func_name) is not orig:
|
|
742
|
+
ns.pop(func_name, None) # 类方法变异在模块面留下的同名残件
|
|
743
|
+
|
|
744
|
+
return _restore
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# 生效条件:无入参时跑定点变异自证——先跑未变异基线(必须零失败),再逐个应用 _MUTATIONS:锚点缺失立即打印 ANCHOR-MISS 并返回 2(fail-closed,不再继续);变异后**逐一打印转红的断言名**与红项数,红项组与数量必须与预期完全一致,否则计入 bad;全部通过返回 0,存在不符返回 1。
|
|
748
|
+
def _branch_baseline() -> int:
|
|
749
|
+
print("!! 定点变异模式:逐处关掉判据,守卫应当转红且**恰好**命中预期组/项数\n")
|
|
750
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
751
|
+
clean_fail, _clean_groups = _run_groups()
|
|
752
|
+
print(" 未变异基线:失败=%d" % clean_fail)
|
|
753
|
+
if clean_fail:
|
|
754
|
+
print(" 基线即失败 → 定点变异自证无意义(先修基线)")
|
|
755
|
+
return 1
|
|
756
|
+
bad = []
|
|
757
|
+
for label, which, fname, old, new, expect in _MUTATIONS:
|
|
758
|
+
restore = _patch(which, fname, old, new)
|
|
759
|
+
if restore is None:
|
|
760
|
+
print(" ANCHOR-MISS %s —— 变异锚点漂移(实现改了却没同步本表;"
|
|
761
|
+
"基线源=%s.%s)" % (label, which, fname))
|
|
762
|
+
return 2 # fail-closed:锚点漂移不静默失效
|
|
763
|
+
_MUTATION[0] = {"which": which, "func": fname, "old": old, "new": new}
|
|
764
|
+
try:
|
|
765
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
766
|
+
got_fail, got_groups = _run_groups()
|
|
767
|
+
red_names = list(FAILS)
|
|
768
|
+
finally:
|
|
769
|
+
_MUTATION[0] = None
|
|
770
|
+
restore()
|
|
771
|
+
_cleanup() # 每轮清干净:临时根与活实例不跨轮累积
|
|
772
|
+
hit = {g: n for g, n in got_groups.items() if n}
|
|
773
|
+
print(" 关掉「%s」→ 红项=%d,命中组=%s" % (label, got_fail, hit))
|
|
774
|
+
for nm in red_names:
|
|
775
|
+
print(" [RED] %s" % nm)
|
|
776
|
+
if hit == expect and got_fail == sum(expect.values()):
|
|
777
|
+
print(" %s(与预期逐位一致)" % "OK")
|
|
778
|
+
else:
|
|
779
|
+
print(" MISMATCH 预期=%s/%d 实得=%s/%d"
|
|
780
|
+
% (expect, sum(expect.values()), hit, got_fail))
|
|
781
|
+
bad.append(label)
|
|
782
|
+
return 0 if not bad else 1
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# 生效条件:无入参时只施加 M1(抽掉修复本体)——打印转红断言名与红项数、还原后重跑并打印绿项数(「红→恢复→绿」三态原地可见);锚点缺失返回 2,红项数与预期不符返回 1,两侧都符合返回 0。
|
|
786
|
+
def _drop_fix() -> int:
|
|
787
|
+
print("!! 抽掉修复本体:append 打开分片前不再复查目录(= 修前行为)\n")
|
|
788
|
+
label, which, fname, old, new, expect = _MUTATIONS[0]
|
|
789
|
+
print(" 变异:%s\n 锚点:%s.%s %r → %r\n" % (label, which, fname, old, new))
|
|
790
|
+
restore = _patch(which, fname, old, new)
|
|
791
|
+
if restore is None:
|
|
792
|
+
print(" ANCHOR-MISS —— 锚点漂移,fail-closed(退出码 2)")
|
|
793
|
+
return 2
|
|
794
|
+
_MUTATION[0] = {"which": which, "func": fname, "old": old, "new": new}
|
|
795
|
+
try:
|
|
796
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
797
|
+
got_fail, got_groups = _run_groups()
|
|
798
|
+
red_names = list(FAILS)
|
|
799
|
+
finally:
|
|
800
|
+
_MUTATION[0] = None
|
|
801
|
+
restore()
|
|
802
|
+
_cleanup()
|
|
803
|
+
hit = {g: n for g, n in got_groups.items() if n}
|
|
804
|
+
print(" 抽掉修复后:红项=%d 项,命中组=%s" % (got_fail, hit))
|
|
805
|
+
for nm in red_names:
|
|
806
|
+
print(" [RED] %s" % nm)
|
|
807
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
808
|
+
back_fail, back_groups = _run_groups()
|
|
809
|
+
back_hit = {g: n for g, n in back_groups.items() if n}
|
|
810
|
+
print("\n 恢复修复后:红项=%d 项,命中组=%s(全绿 ⇒ 红态确由该改动引起)"
|
|
811
|
+
% (back_fail, back_hit))
|
|
812
|
+
ok_red = hit == expect and got_fail == sum(expect.values())
|
|
813
|
+
ok_green = back_fail == 0
|
|
814
|
+
print(" 判定:红态%s ;恢复%s"
|
|
815
|
+
% ("符合预期(%s/%d)" % (expect, sum(expect.values())) if ok_red
|
|
816
|
+
else "**与预期不符**(预期 %s/%d)" % (expect, sum(expect.values())),
|
|
817
|
+
"转绿(0 失败)" if ok_green else "**仍未转绿(%d 失败)**"
|
|
818
|
+
% back_fail))
|
|
819
|
+
return 0 if (ok_red and ok_green) else 1
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
# 生效条件:命令行含 --branch-baseline 时走定点变异自证(退出码 0/1/2);含 --drop-fix 时只抽掉修复本体跑「红→恢复→绿」(退出码 0/1/2);否则依次跑 A–E 五组打印逐条结果与汇总,全部通过返回 0、存在失败返回 1。
|
|
823
|
+
def main() -> int:
|
|
824
|
+
try:
|
|
825
|
+
if "--branch-baseline" in sys.argv:
|
|
826
|
+
return _branch_baseline()
|
|
827
|
+
if "--drop-fix" in sys.argv:
|
|
828
|
+
return _drop_fix()
|
|
829
|
+
_reset_counters()
|
|
830
|
+
for _name, fn in _GROUPS:
|
|
831
|
+
fn()
|
|
832
|
+
finally:
|
|
833
|
+
_cleanup()
|
|
834
|
+
print("\n" + "=" * 64)
|
|
835
|
+
print("B4 分片目录自愈守卫:%d 通过,%d 失败" % (PASS, FAIL))
|
|
836
|
+
if FAILS:
|
|
837
|
+
for f in FAILS:
|
|
838
|
+
print(" - %s" % f)
|
|
839
|
+
return 0 if not FAILS else 1
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
if __name__ == "__main__":
|
|
843
|
+
sys.exit(main())
|