master-skill 0.11.0 → 0.12.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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +48 -55
  5. package/README_EN.md +72 -59
  6. package/bin/cli.mjs +12 -7
  7. package/gemini-extension.json +1 -1
  8. package/hooks/session-start +68 -77
  9. package/hooks/session_start.py +152 -0
  10. package/package.json +5 -2
  11. package/prebuilt/compare-masters/SKILL.md +21 -2
  12. package/prebuilt/master-ajahn-chah/meta.json +6 -0
  13. package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
  14. package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
  15. package/prebuilt/master-curriculum/references/tiantai.md +1 -1
  16. package/prebuilt/master-debate/SKILL.md +14 -2
  17. package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
  18. package/prebuilt/master-help/SKILL.md +9 -1
  19. package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
  20. package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
  21. package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
  22. package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
  23. package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
  24. package/prebuilt/master-ouyi/meta.json +5 -0
  25. package/prebuilt/master-ouyi/references/teaching.md +3 -3
  26. package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
  27. package/prebuilt/master-tsongkhapa/meta.json +6 -0
  28. package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
  29. package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
  30. package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
  31. package/prebuilt/master-zhiyi/meta.json +2 -2
  32. package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
  33. package/scripts/check-audit-ignores.py +105 -0
  34. package/scripts/check-eval-sdk-surface.py +142 -0
  35. package/scripts/check-gate-liveness.py +205 -6
  36. package/scripts/reaudit-report.py +163 -0
  37. package/scripts/regrade-report.py +157 -0
  38. package/scripts/smoke-eval-sdk.py +174 -0
  39. package/scripts/test-fidelity.py +684 -52
  40. package/scripts/validate-citation-references.py +150 -0
  41. package/scripts/validate-citation-templates.py +176 -0
  42. package/scripts/validate-fixture-terms.py +127 -0
  43. package/scripts/verify-adjudication.py +316 -0
  44. package/scripts/verify_citations.py +739 -39
  45. package/tools/cross_reference.py +44 -10
  46. package/tools/fojin-known-absent.json +14 -0
  47. package/tools/fojin_bridge.py +138 -8
  48. package/tools/rag_query.py +45 -2
  49. package/tools/skill_writer.py +50 -7
  50. package/tools/verify_sources.py +240 -15
  51. package/hooks/tests/test_run_hook.sh +0 -114
  52. package/hooks/tests/test_run_hook_cmd.sh +0 -94
  53. package/hooks/tests/test_session_start.sh +0 -149
  54. package/scripts/tests/test_check_gate_liveness.py +0 -232
  55. package/scripts/tests/test_check_manifest_versions.py +0 -217
  56. package/scripts/tests/test_check_response.py +0 -190
  57. package/scripts/tests/test_debate_protocol.py +0 -159
  58. package/scripts/tests/test_fidelity_providers.py +0 -202
  59. package/scripts/tests/test_injection_hardening.py +0 -174
  60. package/scripts/tests/test_select_fidelity_smoke.py +0 -142
  61. package/scripts/tests/test_validate.py +0 -145
  62. package/scripts/tests/test_validate_citation_contract.py +0 -408
  63. package/scripts/tests/test_validate_cross_critique.py +0 -149
  64. package/scripts/tests/test_validate_curriculum_sources.py +0 -144
  65. package/scripts/tests/test_validate_fidelity.py +0 -59
  66. package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
  67. package/scripts/tests/test_validate_persona_fidelity.py +0 -317
  68. package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
  69. package/scripts/tests/test_validate_workflow.py +0 -284
@@ -1,144 +0,0 @@
1
- """Tests for validate-curriculum-sources.py.
2
-
3
- Verifies that citations (CBETA T-numbers, X-numbers, Toh, compiled-teaching ids)
4
- and /master-<slug> references in curriculum reference files cross-check against
5
- the actual prebuilt master metadata.
6
- """
7
- from __future__ import annotations
8
-
9
- import json
10
- from pathlib import Path
11
-
12
- import pytest
13
-
14
- import importlib.util
15
- import sys
16
-
17
-
18
- def _load_module():
19
- spec_path = Path(__file__).resolve().parents[1] / "validate-curriculum-sources.py"
20
- spec = importlib.util.spec_from_file_location("vcs", spec_path)
21
- mod = importlib.util.module_from_spec(spec)
22
- sys.modules["vcs"] = mod
23
- spec.loader.exec_module(mod)
24
- return mod
25
-
26
-
27
- @pytest.fixture
28
- def fake_prebuilt(tmp_path):
29
- """Fake prebuilt tree with two masters and a curriculum dir."""
30
- prebuilt = tmp_path / "prebuilt"
31
- # Master A: cbeta source
32
- (prebuilt / "master-aaa").mkdir(parents=True)
33
- (prebuilt / "master-aaa" / "meta.json").write_text(json.dumps({
34
- "slug": "aaa",
35
- "sources": [{"type": "cbeta", "id": "T48n2008", "title": "demo"}],
36
- }), encoding="utf-8")
37
- # Master B: compiled_teaching source + Toh
38
- (prebuilt / "master-bbb").mkdir(parents=True)
39
- (prebuilt / "master-bbb" / "meta.json").write_text(json.dumps({
40
- "slug": "bbb",
41
- "sources": [
42
- {"type": "compiled_teaching", "id": "AjahnChah:FoodForTheHeart", "title": "demo"},
43
- {"type": "84000", "id": "Toh 21", "title": "demo"},
44
- ],
45
- }), encoding="utf-8")
46
- (prebuilt / "master-curriculum" / "references").mkdir(parents=True)
47
- return prebuilt
48
-
49
-
50
- def test_extract_citations_finds_T_numbers():
51
- vcs = _load_module()
52
- text = "See 《坛经》【T48n2008】 for details."
53
- assert "T48n2008" in vcs.extract_citations(text)
54
-
55
-
56
- def test_extract_citations_finds_Toh():
57
- vcs = _load_module()
58
- text = "藏文藏经 Toh 21 心经..."
59
- assert "Toh 21" in vcs.extract_citations(text)
60
-
61
-
62
- def test_extract_citations_finds_compiled_teaching():
63
- vcs = _load_module()
64
- text = "见 AjahnChah:FoodForTheHeart ..."
65
- assert "AjahnChah:FoodForTheHeart" in vcs.extract_citations(text)
66
-
67
-
68
- def test_extract_master_slugs():
69
- vcs = _load_module()
70
- text = "推荐 master:/master-aaa ;交叉 /master-bbb 。"
71
- assert vcs.extract_master_slugs(text) == {"aaa", "bbb"}
72
-
73
-
74
- def test_collect_known_citations(fake_prebuilt):
75
- vcs = _load_module()
76
- known = vcs.collect_known_citations(fake_prebuilt)
77
- assert "T48n2008" in known
78
- assert "AjahnChah:FoodForTheHeart" in known
79
- assert "Toh 21" in known
80
-
81
-
82
- def test_validate_pass(fake_prebuilt):
83
- vcs = _load_module()
84
- ref = fake_prebuilt / "master-curriculum" / "references" / "ok.md"
85
- ref.write_text(
86
- "## Path\n- 主用经《坛经》【T48n2008】\n- master:/master-aaa\n- 藏:Toh 21\n",
87
- encoding="utf-8",
88
- )
89
- errors = vcs.validate(fake_prebuilt)
90
- assert errors == []
91
-
92
-
93
- def test_validate_fails_on_fabricated_citation(fake_prebuilt):
94
- vcs = _load_module()
95
- ref = fake_prebuilt / "master-curriculum" / "references" / "bad.md"
96
- ref.write_text("【T99n9999】 fake\n", encoding="utf-8")
97
- errors = vcs.validate(fake_prebuilt)
98
- assert any("T99n9999" in e for e in errors)
99
-
100
-
101
- def test_validate_fails_on_unknown_master_slug(fake_prebuilt):
102
- vcs = _load_module()
103
- ref = fake_prebuilt / "master-curriculum" / "references" / "bad.md"
104
- ref.write_text("/master-ghost\n", encoding="utf-8")
105
- errors = vcs.validate(fake_prebuilt)
106
- assert any("ghost" in e for e in errors)
107
-
108
-
109
- def test_validate_ignores_meta_skill_slugs(fake_prebuilt):
110
- """/master-curriculum and /master-debate are meta-skills suppressed via
111
- META_SKILL_SLUGS. /compare-masters never matches _SLUG (no /master- prefix),
112
- so it is naturally ignored. None of these may raise validate errors."""
113
- vcs = _load_module()
114
- ref = fake_prebuilt / "master-curriculum" / "references" / "ok.md"
115
- ref.write_text(
116
- "延伸 → /compare-masters · /master-debate · /master-curriculum\n",
117
- encoding="utf-8",
118
- )
119
- errors = vcs.validate(fake_prebuilt)
120
- assert errors == []
121
-
122
-
123
- def test_validate_returns_error_when_refs_dir_missing(tmp_path):
124
- vcs = _load_module()
125
- prebuilt = tmp_path / "prebuilt"
126
- prebuilt.mkdir()
127
- # curriculum/references missing
128
- errors = vcs.validate(prebuilt)
129
- assert len(errors) == 1
130
- assert "references dir not found" in errors[0]
131
-
132
-
133
- def test_extract_citations_finds_hyphenated_compiled_teaching():
134
- vcs = _load_module()
135
- text = "PTS:DN-Comm 注释 / BDRC:Pha-chos-Bu-chos"
136
- cits = vcs.extract_citations(text)
137
- assert "PTS:DN-Comm" in cits
138
- assert "BDRC:Pha-chos-Bu-chos" in cits
139
-
140
-
141
- def test_extract_citations_finds_Toh_with_colon():
142
- vcs = _load_module()
143
- text = "见 Toh:3948"
144
- assert "Toh:3948" in vcs.extract_citations(text)
@@ -1,59 +0,0 @@
1
- import json
2
- import importlib.util
3
- from pathlib import Path
4
-
5
- MODULE_PATH = Path(__file__).resolve().parents[1] / "validate-fidelity.py"
6
- SPEC = importlib.util.spec_from_file_location("validate_fidelity", MODULE_PATH)
7
- validate_fidelity = importlib.util.module_from_spec(SPEC)
8
- SPEC.loader.exec_module(validate_fidelity)
9
-
10
-
11
- def _write_fixture(tmp_path: Path, master_name: str, cases: list[dict]) -> Path:
12
- master_dir = tmp_path / master_name
13
- tests_dir = master_dir / "tests"
14
- tests_dir.mkdir(parents=True)
15
- payload = "\n".join(json.dumps(case, ensure_ascii=False) for case in cases) + "\n"
16
- (tests_dir / "fidelity.jsonl").write_text(payload, encoding="utf-8")
17
- return master_dir
18
-
19
-
20
- def test_compare_requires_framework_output_sections(tmp_path):
21
- master_dir = _write_fixture(
22
- tmp_path,
23
- "compare-masters",
24
- [
25
- {
26
- "q": "禅和净怎么比较?",
27
- "must_select_masters": ["huineng", "yinguang"],
28
- "must_have_sections": ["分歧雷达"],
29
- }
30
- for _ in range(5)
31
- ],
32
- )
33
-
34
- errors = validate_fidelity.validate_master(master_dir)
35
-
36
- assert any("共同点" in error for error in errors)
37
- assert any("引用来源" in error for error in errors)
38
-
39
-
40
- def test_compare_accepts_required_framework_output_sections(tmp_path):
41
- case = {
42
- "q": "禅和净怎么比较?",
43
- "must_select_masters": ["huineng", "yinguang"],
44
- "must_have_sections": sorted(validate_fidelity.COMPARE_REQUIRED_SECTIONS),
45
- }
46
- cases = [case.copy() for _ in range(5)]
47
- cases.append(
48
- {
49
- "q": "哪个更好?",
50
- "test_type": "boundary",
51
- "boundary": "sectarian_judgment",
52
- "must_not_contain": ["更好"],
53
- }
54
- )
55
- master_dir = _write_fixture(tmp_path, "compare-masters", cases)
56
-
57
- errors = validate_fidelity.validate_master(master_dir)
58
-
59
- assert errors == []
@@ -1,372 +0,0 @@
1
- """Tests for validate-lore-triggers-content.py.
2
-
3
- Covers:
4
- - normalize() drops punctuation/whitespace, folds trad->simp
5
- - longest_common_substring_len returns true LCS length
6
- - similarity_ratio is windowed correctly for large bodies
7
- - check_entry passes a true verbatim quote in sources/
8
- - check_entry passes a quote with trad/simp variation
9
- - check_entry FAILS a fabricated quote
10
- - check_entry FAILS short quote when only LCS of 2 chars overlap
11
- - check_entry returns the in_references_only soft-pass when match
12
- is in references/ but not sources/
13
- - validate() walks tmp_path tree end to end
14
- - main() advisory mode exits 0 even when failures present
15
- - main() --strict exits non-zero on failures
16
- """
17
- from __future__ import annotations
18
-
19
- import importlib.util
20
- import json
21
- import subprocess
22
- import sys
23
- from pathlib import Path
24
-
25
- import pytest
26
-
27
-
28
- def _load_module():
29
- spec_path = (
30
- Path(__file__).resolve().parents[1]
31
- / "validate-lore-triggers-content.py"
32
- )
33
- spec = importlib.util.spec_from_file_location("vltc", spec_path)
34
- mod = importlib.util.module_from_spec(spec)
35
- sys.modules["vltc"] = mod
36
- spec.loader.exec_module(mod)
37
- return mod
38
-
39
-
40
- def _setup_master(
41
- prebuilt: Path,
42
- slug: str,
43
- lore_triggers: list,
44
- excerpts_text: str | None = None,
45
- references_text: str | None = None,
46
- excerpts_filename: str = "tanjing-excerpts.md",
47
- ):
48
- d = prebuilt / f"master-{slug}"
49
- (d / "sources").mkdir(parents=True, exist_ok=True)
50
- (d / "references").mkdir(parents=True, exist_ok=True)
51
- meta = {
52
- "slug": slug,
53
- "sources": [
54
- {"type": "cbeta", "id": "T48n2008", "title": "坛经"}
55
- ],
56
- "lore_triggers": lore_triggers,
57
- }
58
- (d / "meta.json").write_text(
59
- json.dumps(meta, ensure_ascii=False), encoding="utf-8"
60
- )
61
- if excerpts_text is not None:
62
- (d / "sources" / excerpts_filename).write_text(
63
- excerpts_text, encoding="utf-8"
64
- )
65
- if references_text is not None:
66
- (d / "references" / "teaching.md").write_text(
67
- references_text, encoding="utf-8"
68
- )
69
- return d
70
-
71
-
72
- # ---------- normalize ----------
73
-
74
-
75
- def test_normalize_drops_punctuation():
76
- m = _load_module()
77
- assert m.normalize("我此法门,从上以来。") == "我此法门从上以来"
78
-
79
-
80
- def test_normalize_drops_whitespace():
81
- m = _load_module()
82
- assert m.normalize("定 慧 一 体\n") == "定慧一体"
83
-
84
-
85
- def test_normalize_folds_traditional_to_simplified():
86
- m = _load_module()
87
- # 介尔 in meta vs 介爾 in excerpts must compare equal post-normalize.
88
- assert m.normalize("介爾有心") == m.normalize("介尔有心")
89
- # 於 vs 于
90
- assert m.normalize("於相而离相") == m.normalize("于相而离相")
91
-
92
-
93
- # ---------- LCS ----------
94
-
95
-
96
- def test_lcs_finds_long_overlap():
97
- m = _load_module()
98
- a = "abcdefghijklmnop"
99
- b = "zzabcdefghijxxxx"
100
- assert m.longest_common_substring_len(a, b) == 10 # abcdefghij
101
-
102
-
103
- def test_lcs_empty_inputs():
104
- m = _load_module()
105
- assert m.longest_common_substring_len("", "abc") == 0
106
- assert m.longest_common_substring_len("abc", "") == 0
107
-
108
-
109
- # ---------- similarity ratio ----------
110
-
111
-
112
- def test_similarity_ratio_high_for_identical():
113
- m = _load_module()
114
- s = "一空一切空,无假无中而不空,总空观也"
115
- assert m.similarity_ratio(s, s) >= 0.99
116
-
117
-
118
- def test_similarity_ratio_low_for_unrelated():
119
- m = _load_module()
120
- a = "菩提本无树明镜亦非台"
121
- b = "完全无关的废话填充字符串赞美和谐社会"
122
- assert m.similarity_ratio(a, b) < 0.4
123
-
124
-
125
- # ---------- extract_quotation ----------
126
-
127
-
128
- def test_extract_quotation_strips_gloss():
129
- m = _load_module()
130
- content = "定慧一体,不是二。——此乃慧能定慧不二说"
131
- assert m.extract_quotation(content) == "定慧一体,不是二。"
132
-
133
-
134
- def test_extract_quotation_no_gloss():
135
- m = _load_module()
136
- assert m.extract_quotation("无相为体") == "无相为体"
137
-
138
-
139
- # ---------- check_entry: positive case ----------
140
-
141
-
142
- def test_check_entry_passes_verbatim_quote(tmp_path):
143
- m = _load_module()
144
- prebuilt = tmp_path / "prebuilt"
145
- quote = (
146
- "我此法门,从上以来,先立无念为宗,无相为体,无住为本。"
147
- "无相者,于相而离相;无念者,于念而无念;无住者,人之本性。"
148
- )
149
- excerpts = f"# 坛经 (T48n2008)\n\n> {quote}\n"
150
- d = _setup_master(
151
- prebuilt,
152
- slug="huineng",
153
- lore_triggers=[
154
- {
155
- "keys": ["无念"],
156
- "content": quote + "——此为三纲领",
157
- "source_ref": "T48n2008#定慧品",
158
- }
159
- ],
160
- excerpts_text=excerpts,
161
- )
162
- res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
163
- assert res["passed"] is True
164
- assert res["passed_in_sources"] is True
165
-
166
-
167
- def test_check_entry_passes_with_trad_simp_variation(tmp_path):
168
- m = _load_module()
169
- prebuilt = tmp_path / "prebuilt"
170
- meta_quote = (
171
- "夫一心具十法界,一法界又具十法界、百法界;"
172
- "此三千在一念心,介尔有心即具三千。"
173
- )
174
- excerpts_quote = meta_quote.replace("介尔", "介爾") # excerpts use trad
175
- d = _setup_master(
176
- prebuilt,
177
- slug="zhiyi",
178
- lore_triggers=[
179
- {
180
- "keys": ["一念三千"],
181
- "content": meta_quote + "——此乃一念三千",
182
- "source_ref": "T46n1911#卷五上",
183
- }
184
- ],
185
- excerpts_text=f"# 摩诃止观\n\n> {excerpts_quote}\n",
186
- excerpts_filename="mohezhiguan-excerpts.md",
187
- )
188
- res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
189
- assert res["passed"] is True
190
-
191
-
192
- # ---------- check_entry: negative case ----------
193
-
194
-
195
- def test_check_entry_fails_fabricated_quote(tmp_path):
196
- m = _load_module()
197
- prebuilt = tmp_path / "prebuilt"
198
- fabricated = (
199
- "念佛是谁?这是一个完全编造的引文,不可能在任何坛经原典里出现,"
200
- "纯属虚构以测试 validator 是否能识别。"
201
- )
202
- d = _setup_master(
203
- prebuilt,
204
- slug="huineng",
205
- lore_triggers=[
206
- {
207
- "keys": ["念佛是谁"],
208
- "content": fabricated + "——此乃伪造",
209
- "source_ref": "T48n2008#行由品",
210
- }
211
- ],
212
- # A real excerpts file with totally unrelated content
213
- excerpts_text="# 坛经\n\n> 菩提自性,本来清净,但用此心,直了成佛。\n",
214
- )
215
- res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
216
- assert res["passed"] is False, (
217
- f"Fabricated quote slipped through with lcs={res['best_lcs']} "
218
- f"ratio={res['best_ratio']}"
219
- )
220
-
221
-
222
- def test_check_entry_short_quote_low_overlap_fails(tmp_path):
223
- m = _load_module()
224
- prebuilt = tmp_path / "prebuilt"
225
- # 20-char quote, only 2 chars overlap with excerpts
226
- d = _setup_master(
227
- prebuilt,
228
- slug="huineng",
229
- lore_triggers=[
230
- {
231
- "keys": ["菩提本无树"],
232
- "content": "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。",
233
- "source_ref": "T48n2008#行由品",
234
- }
235
- ],
236
- excerpts_text="# 坛经\n\n> 菩提自性,本来清净,但用此心,直了成佛。\n",
237
- )
238
- res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
239
- assert res["passed"] is False
240
-
241
-
242
- # ---------- references soft-pass ----------
243
-
244
-
245
- def test_check_entry_soft_pass_via_references(tmp_path):
246
- m = _load_module()
247
- prebuilt = tmp_path / "prebuilt"
248
- quote = "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。"
249
- d = _setup_master(
250
- prebuilt,
251
- slug="huineng",
252
- lore_triggers=[
253
- {
254
- "keys": ["菩提本无树"],
255
- "content": quote + "——此偈显自性",
256
- "source_ref": "T48n2008#行由品",
257
- }
258
- ],
259
- excerpts_text="# 坛经\n\n> 菩提自性,本来清净,直了成佛。\n",
260
- references_text=f"## 菩提本无树偈\n\n慧能偈:「{quote}」",
261
- )
262
- res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
263
- assert res["passed"] is True
264
- assert res["passed_in_sources"] is False
265
- assert res["in_references_only"] is True
266
- assert res["ref_file"] == "teaching.md"
267
-
268
-
269
- # ---------- validate() walks tree ----------
270
-
271
-
272
- def test_validate_walks_full_tree(tmp_path):
273
- m = _load_module()
274
- prebuilt = tmp_path / "prebuilt"
275
- _setup_master(
276
- prebuilt,
277
- slug="alpha",
278
- lore_triggers=[
279
- {
280
- "keys": ["a"],
281
- "content": "x" * 100 + "——gloss",
282
- "source_ref": "T00n0000",
283
- }
284
- ],
285
- excerpts_text="x" * 200,
286
- )
287
- _setup_master(
288
- prebuilt,
289
- slug="beta",
290
- lore_triggers=[],
291
- excerpts_text="anything",
292
- )
293
- results = m.validate(prebuilt)
294
- assert len(results) == 1
295
- assert results[0]["master"] == "master-alpha"
296
- assert results[0]["passed"] is True
297
-
298
-
299
- def test_validate_skips_meta_skill(tmp_path):
300
- m = _load_module()
301
- prebuilt = tmp_path / "prebuilt"
302
- d = prebuilt / "master-meta"
303
- d.mkdir(parents=True)
304
- (d / "meta.json").write_text(
305
- json.dumps(
306
- {
307
- "slug": "meta",
308
- "kind": "meta-skill",
309
- "lore_triggers": [
310
- {
311
- "keys": ["x"],
312
- "content": "fake" * 30,
313
- "source_ref": "T00n0000",
314
- }
315
- ],
316
- },
317
- ensure_ascii=False,
318
- ),
319
- encoding="utf-8",
320
- )
321
- results = m.validate(prebuilt)
322
- assert results == []
323
-
324
-
325
- # ---------- CLI: advisory vs strict ----------
326
-
327
-
328
- def _run_cli(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
329
- script = (
330
- Path(__file__).resolve().parents[1]
331
- / "validate-lore-triggers-content.py"
332
- )
333
- return subprocess.run(
334
- [sys.executable, str(script), *args],
335
- capture_output=True,
336
- text=True,
337
- cwd=cwd,
338
- env={"PYTHONIOENCODING": "utf-8", "PATH": "/usr/bin:/bin"},
339
- )
340
-
341
-
342
- def test_cli_advisory_exits_zero_on_real_repo():
343
- """Smoke test against the live prebuilt/ tree — must exit 0 advisory."""
344
- repo_root = Path(__file__).resolve().parents[2]
345
- proc = _run_cli([], cwd=repo_root)
346
- assert proc.returncode == 0, proc.stdout + proc.stderr
347
-
348
-
349
- def test_cli_strict_exits_nonzero_when_failures(tmp_path):
350
- """Build a fake tree with a fabricated entry; --strict must fail."""
351
- m = _load_module()
352
- prebuilt = tmp_path / "prebuilt"
353
- fabricated = "完全编造无任何根据的伪造引文" * 5
354
- _setup_master(
355
- prebuilt,
356
- slug="huineng",
357
- lore_triggers=[
358
- {
359
- "keys": ["x"],
360
- "content": fabricated + "——伪造测试",
361
- "source_ref": "T48n2008",
362
- }
363
- ],
364
- excerpts_text="无关内容",
365
- )
366
- # Run the validator pointed at this fake tree by patching PREBUILT_DIR.
367
- results = m.validate(prebuilt)
368
- assert any(not r["passed"] for r in results)
369
- # Now confirm CLI --strict would exit 1: invoke main() directly.
370
- # We can't easily invoke main on a fake tree without monkeypatching, so
371
- # assert the precondition only — the integration is exercised in the
372
- # advisory smoke test above.