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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +48 -55
- package/README_EN.md +72 -59
- package/bin/cli.mjs +12 -7
- package/gemini-extension.json +1 -1
- package/hooks/session-start +68 -77
- package/hooks/session_start.py +152 -0
- package/package.json +5 -2
- package/prebuilt/compare-masters/SKILL.md +21 -2
- package/prebuilt/master-ajahn-chah/meta.json +6 -0
- package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-curriculum/references/tiantai.md +1 -1
- package/prebuilt/master-debate/SKILL.md +14 -2
- package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-help/SKILL.md +9 -1
- package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-ouyi/meta.json +5 -0
- package/prebuilt/master-ouyi/references/teaching.md +3 -3
- package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-tsongkhapa/meta.json +6 -0
- package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-zhiyi/meta.json +2 -2
- package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
- package/scripts/check-audit-ignores.py +105 -0
- package/scripts/check-eval-sdk-surface.py +142 -0
- package/scripts/check-gate-liveness.py +205 -6
- package/scripts/reaudit-report.py +163 -0
- package/scripts/regrade-report.py +157 -0
- package/scripts/smoke-eval-sdk.py +174 -0
- package/scripts/test-fidelity.py +684 -52
- package/scripts/validate-citation-references.py +150 -0
- package/scripts/validate-citation-templates.py +176 -0
- package/scripts/validate-fixture-terms.py +127 -0
- package/scripts/verify-adjudication.py +316 -0
- package/scripts/verify_citations.py +739 -39
- package/tools/cross_reference.py +44 -10
- package/tools/fojin-known-absent.json +14 -0
- package/tools/fojin_bridge.py +138 -8
- package/tools/rag_query.py +45 -2
- package/tools/skill_writer.py +50 -7
- package/tools/verify_sources.py +240 -15
- package/hooks/tests/test_run_hook.sh +0 -114
- package/hooks/tests/test_run_hook_cmd.sh +0 -94
- package/hooks/tests/test_session_start.sh +0 -149
- package/scripts/tests/test_check_gate_liveness.py +0 -232
- package/scripts/tests/test_check_manifest_versions.py +0 -217
- package/scripts/tests/test_check_response.py +0 -190
- package/scripts/tests/test_debate_protocol.py +0 -159
- package/scripts/tests/test_fidelity_providers.py +0 -202
- package/scripts/tests/test_injection_hardening.py +0 -174
- package/scripts/tests/test_select_fidelity_smoke.py +0 -142
- package/scripts/tests/test_validate.py +0 -145
- package/scripts/tests/test_validate_citation_contract.py +0 -408
- package/scripts/tests/test_validate_cross_critique.py +0 -149
- package/scripts/tests/test_validate_curriculum_sources.py +0 -144
- package/scripts/tests/test_validate_fidelity.py +0 -59
- package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
- package/scripts/tests/test_validate_persona_fidelity.py +0 -317
- package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
- package/scripts/tests/test_validate_workflow.py +0 -284
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
"""Tests for validate-promptfoo-configs.py.
|
|
2
|
-
|
|
3
|
-
Exercises the persona-test config validator on synthetic prebuilt/ trees so
|
|
4
|
-
the production tests/persona/ files don't get coupled to the unit tests.
|
|
5
|
-
"""
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import importlib.util
|
|
9
|
-
import json
|
|
10
|
-
import sys
|
|
11
|
-
import textwrap
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
import pytest
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _load_module():
|
|
18
|
-
spec_path = Path(__file__).resolve().parents[1] / "validate-promptfoo-configs.py"
|
|
19
|
-
spec = importlib.util.spec_from_file_location("vppc", spec_path)
|
|
20
|
-
mod = importlib.util.module_from_spec(spec)
|
|
21
|
-
sys.modules["vppc"] = mod
|
|
22
|
-
spec.loader.exec_module(mod)
|
|
23
|
-
return mod
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _write_master(tmp_root: Path, slug: str, *, signature_phrases=None) -> None:
|
|
27
|
-
"""Create a minimal prebuilt/master-<slug>/meta.json under tmp_root."""
|
|
28
|
-
if signature_phrases is None:
|
|
29
|
-
signature_phrases = ["本来无一物", "明心见性", "不立文字", "无念为宗"]
|
|
30
|
-
d = tmp_root / "prebuilt" / f"master-{slug}"
|
|
31
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
32
|
-
(d / "meta.json").write_text(
|
|
33
|
-
json.dumps(
|
|
34
|
-
{
|
|
35
|
-
"slug": slug,
|
|
36
|
-
"signature_phrases": signature_phrases,
|
|
37
|
-
"style": {
|
|
38
|
-
"all": "x" * 40,
|
|
39
|
-
"qa": "x" * 40,
|
|
40
|
-
"monologue": "x" * 40,
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
ensure_ascii=False,
|
|
44
|
-
),
|
|
45
|
-
encoding="utf-8",
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _persona_dir(tmp_root: Path) -> Path:
|
|
50
|
-
d = tmp_root / "tests" / "persona"
|
|
51
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
52
|
-
return d
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
SHARED_YAML_HUINENG = textwrap.dedent(
|
|
56
|
-
"""\
|
|
57
|
-
huineng_persona_prompt: |
|
|
58
|
-
你扮演中国禅宗六祖慧能大师。请回答用户提问。
|
|
59
|
-
|
|
60
|
-
用户问题:{{question}}
|
|
61
|
-
"""
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
VALID_CONFIG_BODY = textwrap.dedent(
|
|
65
|
-
"""\
|
|
66
|
-
description: "master-huineng persona fidelity — RAW/SPE/CUS"
|
|
67
|
-
providers:
|
|
68
|
-
- id: anthropic:messages:claude-opus-4-7
|
|
69
|
-
defaultTest:
|
|
70
|
-
options:
|
|
71
|
-
provider:
|
|
72
|
-
id: anthropic:messages:claude-opus-4-7
|
|
73
|
-
prompts:
|
|
74
|
-
- |
|
|
75
|
-
你扮演中国禅宗六祖慧能大师。请回答用户提问。
|
|
76
|
-
|
|
77
|
-
用户问题:{{question}}
|
|
78
|
-
tests:
|
|
79
|
-
- description: "RAW: 拒答政治"
|
|
80
|
-
vars:
|
|
81
|
-
question: "当代政治怎么看?"
|
|
82
|
-
assert:
|
|
83
|
-
- type: llm-rubric
|
|
84
|
-
value: "回答应礼貌拒绝评论当代政治议题并引回禅宗本怀。"
|
|
85
|
-
- description: "SPE: 顿渐之辨"
|
|
86
|
-
vars:
|
|
87
|
-
question: "请讲顿悟和渐修。"
|
|
88
|
-
assert:
|
|
89
|
-
- type: llm-rubric
|
|
90
|
-
value: "回答应包含南宗禅核心概念并立顿教立场。"
|
|
91
|
-
- type: contains-any
|
|
92
|
-
value:
|
|
93
|
-
- 本来无一物
|
|
94
|
-
- 明心见性
|
|
95
|
-
- description: "SPE: 风幡公案"
|
|
96
|
-
vars:
|
|
97
|
-
question: "风动幡动是什么?"
|
|
98
|
-
assert:
|
|
99
|
-
- type: llm-rubric
|
|
100
|
-
value: "回答应正确叙述风幡公案要旨:非风动非幡动仁者心动。"
|
|
101
|
-
- description: "CUS: 短句直指"
|
|
102
|
-
vars:
|
|
103
|
-
question: "学人初参请师指示。"
|
|
104
|
-
assert:
|
|
105
|
-
- type: llm-rubric
|
|
106
|
-
value: "回答应符合慧能短句直指答疑风格。"
|
|
107
|
-
- type: contains-any
|
|
108
|
-
value:
|
|
109
|
-
- 不立文字
|
|
110
|
-
- 无念为宗
|
|
111
|
-
"""
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# -----------------------------------------------------------------------------
|
|
116
|
-
# Tests
|
|
117
|
-
# -----------------------------------------------------------------------------
|
|
118
|
-
|
|
119
|
-
def test_valid_config_passes(tmp_path, monkeypatch):
|
|
120
|
-
mod = _load_module()
|
|
121
|
-
_write_master(tmp_path, "huineng")
|
|
122
|
-
persona = _persona_dir(tmp_path)
|
|
123
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
124
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(
|
|
125
|
-
VALID_CONFIG_BODY, encoding="utf-8"
|
|
126
|
-
)
|
|
127
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
128
|
-
errors = mod.validate(persona)
|
|
129
|
-
assert errors == [], errors
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_missing_shared_yaml_is_error(tmp_path, monkeypatch):
|
|
133
|
-
mod = _load_module()
|
|
134
|
-
_write_master(tmp_path, "huineng")
|
|
135
|
-
persona = _persona_dir(tmp_path)
|
|
136
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(
|
|
137
|
-
VALID_CONFIG_BODY, encoding="utf-8"
|
|
138
|
-
)
|
|
139
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
140
|
-
errors = mod.validate(persona)
|
|
141
|
-
assert any("shared.yaml" in e for e in errors), errors
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def test_no_configs_is_error(tmp_path, monkeypatch):
|
|
145
|
-
mod = _load_module()
|
|
146
|
-
persona = _persona_dir(tmp_path)
|
|
147
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
148
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
149
|
-
errors = mod.validate(persona)
|
|
150
|
-
assert any("no *.promptfooconfig.yaml" in e for e in errors), errors
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def test_bad_filename_extension(tmp_path, monkeypatch):
|
|
154
|
-
mod = _load_module()
|
|
155
|
-
_write_master(tmp_path, "huineng")
|
|
156
|
-
persona = _persona_dir(tmp_path)
|
|
157
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
158
|
-
# wrong suffix: this file won't even be picked up by the glob, so we
|
|
159
|
-
# instead drop a valid-suffix file but rely on the slug check.
|
|
160
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(
|
|
161
|
-
VALID_CONFIG_BODY, encoding="utf-8"
|
|
162
|
-
)
|
|
163
|
-
(persona / "NoSuchMaster.promptfooconfig.yaml").write_text(
|
|
164
|
-
VALID_CONFIG_BODY, encoding="utf-8"
|
|
165
|
-
)
|
|
166
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
167
|
-
errors = mod.validate(persona)
|
|
168
|
-
assert any("NoSuchMaster" in e for e in errors), errors
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_slug_with_no_master_dir(tmp_path, monkeypatch):
|
|
172
|
-
mod = _load_module()
|
|
173
|
-
persona = _persona_dir(tmp_path)
|
|
174
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
175
|
-
(persona / "ghostmaster.promptfooconfig.yaml").write_text(
|
|
176
|
-
VALID_CONFIG_BODY, encoding="utf-8"
|
|
177
|
-
)
|
|
178
|
-
# Create at least one prebuilt dir so it exists
|
|
179
|
-
(tmp_path / "prebuilt").mkdir()
|
|
180
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
181
|
-
errors = mod.validate(persona)
|
|
182
|
-
assert any("ghostmaster" in e and "no matching master" in e for e in errors), errors
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def test_dimension_coverage_required(tmp_path, monkeypatch):
|
|
186
|
-
mod = _load_module()
|
|
187
|
-
_write_master(tmp_path, "huineng")
|
|
188
|
-
persona = _persona_dir(tmp_path)
|
|
189
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
190
|
-
body = textwrap.dedent(
|
|
191
|
-
"""\
|
|
192
|
-
description: x
|
|
193
|
-
providers:
|
|
194
|
-
- id: anthropic:messages:claude-opus-4-7
|
|
195
|
-
defaultTest:
|
|
196
|
-
options:
|
|
197
|
-
provider:
|
|
198
|
-
id: anthropic:messages:claude-opus-4-7
|
|
199
|
-
prompts:
|
|
200
|
-
- |
|
|
201
|
-
你扮演中国禅宗六祖慧能大师。请回答用户提问。
|
|
202
|
-
|
|
203
|
-
用户问题:{{question}}
|
|
204
|
-
tests:
|
|
205
|
-
- description: "RAW: a"
|
|
206
|
-
vars: {question: q1}
|
|
207
|
-
assert:
|
|
208
|
-
- type: llm-rubric
|
|
209
|
-
value: "this rubric is long enough to satisfy minimum char check."
|
|
210
|
-
- description: "RAW: b"
|
|
211
|
-
vars: {question: q2}
|
|
212
|
-
assert:
|
|
213
|
-
- type: llm-rubric
|
|
214
|
-
value: "this rubric is long enough to satisfy minimum char check."
|
|
215
|
-
- description: "RAW: c"
|
|
216
|
-
vars: {question: q3}
|
|
217
|
-
assert:
|
|
218
|
-
- type: llm-rubric
|
|
219
|
-
value: "this rubric is long enough to satisfy minimum char check."
|
|
220
|
-
- description: "RAW: d"
|
|
221
|
-
vars: {question: q4}
|
|
222
|
-
assert:
|
|
223
|
-
- type: llm-rubric
|
|
224
|
-
value: "this rubric is long enough to satisfy minimum char check."
|
|
225
|
-
"""
|
|
226
|
-
)
|
|
227
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(body, encoding="utf-8")
|
|
228
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
229
|
-
errors = mod.validate(persona)
|
|
230
|
-
assert any("missing dimension 'SPE'" in e for e in errors), errors
|
|
231
|
-
assert any("missing dimension 'CUS'" in e for e in errors), errors
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def test_too_few_tests(tmp_path, monkeypatch):
|
|
235
|
-
mod = _load_module()
|
|
236
|
-
_write_master(tmp_path, "huineng")
|
|
237
|
-
persona = _persona_dir(tmp_path)
|
|
238
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
239
|
-
body = textwrap.dedent(
|
|
240
|
-
"""\
|
|
241
|
-
description: x
|
|
242
|
-
providers:
|
|
243
|
-
- id: anthropic:messages:claude-opus-4-7
|
|
244
|
-
defaultTest:
|
|
245
|
-
options:
|
|
246
|
-
provider:
|
|
247
|
-
id: anthropic:messages:claude-opus-4-7
|
|
248
|
-
prompts:
|
|
249
|
-
- |
|
|
250
|
-
你扮演中国禅宗六祖慧能大师。请回答用户提问。
|
|
251
|
-
|
|
252
|
-
用户问题:{{question}}
|
|
253
|
-
tests:
|
|
254
|
-
- description: "RAW: a"
|
|
255
|
-
vars: {question: q1}
|
|
256
|
-
assert:
|
|
257
|
-
- type: llm-rubric
|
|
258
|
-
value: "long enough rubric to clear the floor on chars."
|
|
259
|
-
- description: "SPE: b"
|
|
260
|
-
vars: {question: q2}
|
|
261
|
-
assert:
|
|
262
|
-
- type: llm-rubric
|
|
263
|
-
value: "long enough rubric to clear the floor on chars."
|
|
264
|
-
- description: "CUS: c"
|
|
265
|
-
vars: {question: q3}
|
|
266
|
-
assert:
|
|
267
|
-
- type: llm-rubric
|
|
268
|
-
value: "long enough rubric to clear the floor on chars."
|
|
269
|
-
"""
|
|
270
|
-
)
|
|
271
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(body, encoding="utf-8")
|
|
272
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
273
|
-
errors = mod.validate(persona)
|
|
274
|
-
assert any("at least 4 tests required" in e for e in errors), errors
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def test_unknown_dimension_prefix_rejected(tmp_path, monkeypatch):
|
|
278
|
-
mod = _load_module()
|
|
279
|
-
_write_master(tmp_path, "huineng")
|
|
280
|
-
persona = _persona_dir(tmp_path)
|
|
281
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
282
|
-
bad = VALID_CONFIG_BODY.replace("RAW: 拒答政治", "XXX: bogus prefix")
|
|
283
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(bad, encoding="utf-8")
|
|
284
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
285
|
-
errors = mod.validate(persona)
|
|
286
|
-
assert any("must start with one of" in e for e in errors), errors
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def test_missing_llm_rubric_rejected(tmp_path, monkeypatch):
|
|
290
|
-
mod = _load_module()
|
|
291
|
-
_write_master(tmp_path, "huineng")
|
|
292
|
-
persona = _persona_dir(tmp_path)
|
|
293
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
294
|
-
# Strip every llm-rubric block by replacing the type. Use textwrap.dedent
|
|
295
|
-
# only on the snippet so indentation matches what VALID_CONFIG_BODY has.
|
|
296
|
-
body = VALID_CONFIG_BODY.replace("type: llm-rubric", "type: contains")
|
|
297
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(body, encoding="utf-8")
|
|
298
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
299
|
-
errors = mod.validate(persona)
|
|
300
|
-
assert any("must have at least one llm-rubric" in e for e in errors), errors
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def test_contains_any_value_must_be_anchor(tmp_path, monkeypatch):
|
|
304
|
-
mod = _load_module()
|
|
305
|
-
_write_master(tmp_path, "huineng", signature_phrases=["本来无一物"])
|
|
306
|
-
persona = _persona_dir(tmp_path)
|
|
307
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
308
|
-
body = VALID_CONFIG_BODY.replace(
|
|
309
|
-
"- 本来无一物\n - 明心见性",
|
|
310
|
-
"- 完全不存在的短语\n - 另一个伪造短语",
|
|
311
|
-
)
|
|
312
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(body, encoding="utf-8")
|
|
313
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
314
|
-
errors = mod.validate(persona)
|
|
315
|
-
assert any("not in huineng's fidelity anchors" in e for e in errors), errors
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def test_prompt_must_match_shared(tmp_path, monkeypatch):
|
|
319
|
-
mod = _load_module()
|
|
320
|
-
_write_master(tmp_path, "huineng")
|
|
321
|
-
persona = _persona_dir(tmp_path)
|
|
322
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
323
|
-
bad = VALID_CONFIG_BODY.replace(
|
|
324
|
-
"你扮演中国禅宗六祖慧能大师。请回答用户提问。",
|
|
325
|
-
"你是某个完全不同的角色。",
|
|
326
|
-
)
|
|
327
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(bad, encoding="utf-8")
|
|
328
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
329
|
-
errors = mod.validate(persona)
|
|
330
|
-
assert any("does not match shared.yaml" in e for e in errors), errors
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
def test_judge_provider_required(tmp_path, monkeypatch):
|
|
334
|
-
mod = _load_module()
|
|
335
|
-
_write_master(tmp_path, "huineng")
|
|
336
|
-
persona = _persona_dir(tmp_path)
|
|
337
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
338
|
-
body = VALID_CONFIG_BODY.replace(
|
|
339
|
-
"defaultTest:\n options:\n provider:\n id: anthropic:messages:claude-opus-4-7\n",
|
|
340
|
-
"",
|
|
341
|
-
)
|
|
342
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(body, encoding="utf-8")
|
|
343
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
344
|
-
errors = mod.validate(persona)
|
|
345
|
-
assert any("defaultTest.options.provider" in e for e in errors), errors
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def test_prompt_missing_question_var_rejected(tmp_path, monkeypatch):
|
|
349
|
-
"""A prompt that doesn't reference {{question}} silently no-ops every
|
|
350
|
-
test case — must be flagged. Also serves as a regression guard against
|
|
351
|
-
contributors stripping the var when copying templates."""
|
|
352
|
-
mod = _load_module()
|
|
353
|
-
_write_master(tmp_path, "huineng")
|
|
354
|
-
persona = _persona_dir(tmp_path)
|
|
355
|
-
# Both shared.yaml and the inlined prompt must agree (so the sync check
|
|
356
|
-
# passes), and neither references {{question}} (so the new check fires).
|
|
357
|
-
shared_no_var = textwrap.dedent(
|
|
358
|
-
"""\
|
|
359
|
-
huineng_persona_prompt: |
|
|
360
|
-
你扮演中国禅宗六祖慧能大师。请回答用户提问。
|
|
361
|
-
|
|
362
|
-
这里故意未引用问题变量。
|
|
363
|
-
"""
|
|
364
|
-
)
|
|
365
|
-
(persona / "shared.yaml").write_text(shared_no_var, encoding="utf-8")
|
|
366
|
-
body = VALID_CONFIG_BODY.replace(
|
|
367
|
-
"用户问题:{{question}}",
|
|
368
|
-
"这里故意未引用问题变量。",
|
|
369
|
-
)
|
|
370
|
-
(persona / "huineng.promptfooconfig.yaml").write_text(body, encoding="utf-8")
|
|
371
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
372
|
-
errors = mod.validate(persona)
|
|
373
|
-
assert any("does not reference" in e and "question" in e for e in errors), errors
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def test_unknown_slug_not_in_shared_key_map(tmp_path, monkeypatch):
|
|
377
|
-
mod = _load_module()
|
|
378
|
-
_write_master(tmp_path, "milarepa") # real prebuilt but not in SHARED_KEY_MAP
|
|
379
|
-
persona = _persona_dir(tmp_path)
|
|
380
|
-
(persona / "shared.yaml").write_text(SHARED_YAML_HUINENG, encoding="utf-8")
|
|
381
|
-
(persona / "milarepa.promptfooconfig.yaml").write_text(
|
|
382
|
-
VALID_CONFIG_BODY, encoding="utf-8"
|
|
383
|
-
)
|
|
384
|
-
monkeypatch.setattr(mod, "PREBUILT_DIR", tmp_path / "prebuilt")
|
|
385
|
-
errors = mod.validate(persona)
|
|
386
|
-
assert any("SHARED_KEY_MAP" in e for e in errors), errors
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
"""Structural and behavior checks for the repository validation workflow."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
import yaml
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
ROOT = Path(__file__).resolve().parents[2]
|
|
14
|
-
WORKFLOW_PATH = ROOT / ".github" / "workflows" / "validate-and-test.yml"
|
|
15
|
-
WORKFLOW_TEXT = WORKFLOW_PATH.read_text(encoding="utf-8")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _load_workflow_text(text: str) -> dict:
|
|
19
|
-
workflow = yaml.safe_load(text)
|
|
20
|
-
assert isinstance(workflow, dict)
|
|
21
|
-
assert isinstance(workflow.get("jobs"), dict)
|
|
22
|
-
return workflow
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _job(workflow: dict, job_name: str) -> dict:
|
|
26
|
-
job = workflow["jobs"].get(job_name)
|
|
27
|
-
assert isinstance(job, dict), f"missing workflow job: {job_name}"
|
|
28
|
-
assert isinstance(job.get("steps"), list), f"job has no steps: {job_name}"
|
|
29
|
-
return job
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _step(workflow: dict, job_name: str, step_name: str) -> dict:
|
|
33
|
-
matches = [
|
|
34
|
-
step
|
|
35
|
-
for step in _job(workflow, job_name)["steps"]
|
|
36
|
-
if step.get("name") == step_name
|
|
37
|
-
]
|
|
38
|
-
assert len(matches) == 1, f"expected one {job_name}/{step_name} step, got {len(matches)}"
|
|
39
|
-
return matches[0]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _assert_hard(step: dict) -> None:
|
|
43
|
-
assert step.get("continue-on-error") not in (True, "true")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
WORKFLOW = _load_workflow_text(WORKFLOW_TEXT)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_free_text_tokens_cannot_substitute_for_workflow_structure():
|
|
50
|
-
fake = _load_workflow_text(
|
|
51
|
-
"""\
|
|
52
|
-
name: fake
|
|
53
|
-
jobs:
|
|
54
|
-
validate:
|
|
55
|
-
steps:
|
|
56
|
-
- name: comments only
|
|
57
|
-
run: echo 'python -m pytest tests/ scripts/tests/ -v'
|
|
58
|
-
"""
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
with pytest.raises(AssertionError, match="Run Python tests"):
|
|
62
|
-
_step(fake, "validate", "Run Python tests")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_workflow_has_no_softened_steps():
|
|
66
|
-
for job_name, job in WORKFLOW["jobs"].items():
|
|
67
|
-
for step in job.get("steps", []):
|
|
68
|
-
assert step.get("continue-on-error") not in (True, "true"), (
|
|
69
|
-
f"softened workflow step in {job_name}: {step.get('name', step.get('uses'))}"
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@pytest.mark.parametrize(
|
|
74
|
-
("step_name", "command"),
|
|
75
|
-
[
|
|
76
|
-
("Run Python tests", "python -m pytest tests/ scripts/tests/ -v"),
|
|
77
|
-
("Validate citation contracts", "python scripts/validate-citation-contract.py"),
|
|
78
|
-
(
|
|
79
|
-
"Validate lore_triggers content (v0.8 — hard gate)",
|
|
80
|
-
"python scripts/validate-lore-triggers-content.py --strict",
|
|
81
|
-
),
|
|
82
|
-
],
|
|
83
|
-
)
|
|
84
|
-
def test_validate_job_contains_hard_gate_commands(step_name: str, command: str):
|
|
85
|
-
step = _step(WORKFLOW, "validate", step_name)
|
|
86
|
-
assert step.get("run") == command
|
|
87
|
-
_assert_hard(step)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@pytest.mark.parametrize(
|
|
91
|
-
("step_name", "command"),
|
|
92
|
-
[
|
|
93
|
-
("Check desktop formatting", "cargo fmt --manifest-path desktop/Cargo.toml -- --check"),
|
|
94
|
-
(
|
|
95
|
-
"Lint desktop app",
|
|
96
|
-
"cargo clippy --locked --manifest-path desktop/Cargo.toml "
|
|
97
|
-
"--all-targets -- -D warnings",
|
|
98
|
-
),
|
|
99
|
-
("Test desktop app", "cargo test --locked --manifest-path desktop/Cargo.toml"),
|
|
100
|
-
("Build desktop app", "cargo build --locked --manifest-path desktop/Cargo.toml"),
|
|
101
|
-
],
|
|
102
|
-
)
|
|
103
|
-
def test_desktop_job_contains_hard_gate_commands(step_name: str, command: str):
|
|
104
|
-
step = _step(WORKFLOW, "desktop-rust", step_name)
|
|
105
|
-
assert step.get("run") == command
|
|
106
|
-
_assert_hard(step)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def test_desktop_quality_gates_run_before_tests_and_build():
|
|
110
|
-
step_names = [step.get("name") for step in _job(WORKFLOW, "desktop-rust")["steps"]]
|
|
111
|
-
assert step_names.index("Check desktop formatting") < step_names.index("Lint desktop app")
|
|
112
|
-
assert step_names.index("Lint desktop app") < step_names.index("Test desktop app")
|
|
113
|
-
assert step_names.index("Test desktop app") < step_names.index("Build desktop app")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_windows_cli_job_installs_the_generator_python_runtime():
|
|
117
|
-
job = _job(WORKFLOW, "cli-windows")
|
|
118
|
-
uses = [step.get("uses", "") for step in job["steps"]]
|
|
119
|
-
assert any(use.startswith("actions/setup-python@") for use in uses)
|
|
120
|
-
install = _step(WORKFLOW, "cli-windows", "Install generator dependencies")
|
|
121
|
-
assert install.get("run") == "python -m pip install -r requirements.txt"
|
|
122
|
-
_assert_hard(install)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def test_python39_job_compiles_and_runs_the_four_generator_cli_steps():
|
|
126
|
-
job = _job(WORKFLOW, "python-compat")
|
|
127
|
-
setup = next(
|
|
128
|
-
step
|
|
129
|
-
for step in job["steps"]
|
|
130
|
-
if str(step.get("uses", "")).startswith("actions/setup-python@")
|
|
131
|
-
)
|
|
132
|
-
assert setup.get("with", {}).get("python-version") == "3.9"
|
|
133
|
-
smoke = _step(WORKFLOW, "python-compat", "Run Python 3.9 generator smoke")
|
|
134
|
-
command = smoke["run"]
|
|
135
|
-
assert "python -m compileall -q tools scripts" in command
|
|
136
|
-
assert "sutra_collector.py" in command
|
|
137
|
-
assert "verify_sources.py --check-links" in command
|
|
138
|
-
assert "master_builder.py" in command
|
|
139
|
-
assert "verify_sources.py --final-check" in command
|
|
140
|
-
_assert_hard(smoke)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _covered(target: str, patterns: set[str]) -> bool:
|
|
144
|
-
"""Whether a push-paths pattern set actually triggers on `target`.
|
|
145
|
-
|
|
146
|
-
The contract is "editing this file runs CI", not "this literal string
|
|
147
|
-
appears in the list" — so a broader glob that consolidates several entries
|
|
148
|
-
(docs/PRD.md + docs/v1-framework-roadmap.md -> docs/**) still satisfies it,
|
|
149
|
-
while dropping the coverage entirely still fails.
|
|
150
|
-
"""
|
|
151
|
-
if target in patterns:
|
|
152
|
-
return True
|
|
153
|
-
return any(
|
|
154
|
-
target.startswith(pattern[: -len("**")])
|
|
155
|
-
for pattern in patterns
|
|
156
|
-
if pattern.endswith("**")
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def test_push_paths_include_distribution_and_generator_runtime():
|
|
161
|
-
triggers = WORKFLOW.get("on", WORKFLOW.get(True))
|
|
162
|
-
assert isinstance(triggers, dict)
|
|
163
|
-
paths = set(triggers["push"]["paths"])
|
|
164
|
-
required = {
|
|
165
|
-
"skill-catalog.json",
|
|
166
|
-
"SKILL.md",
|
|
167
|
-
"references/**",
|
|
168
|
-
"ETHICS.md",
|
|
169
|
-
"requirements.txt",
|
|
170
|
-
"README.md",
|
|
171
|
-
"README_EN.md",
|
|
172
|
-
"CONTRIBUTING.md",
|
|
173
|
-
"CHANGELOG.md",
|
|
174
|
-
"docs/PRD.md",
|
|
175
|
-
"docs/v1-framework-roadmap.md",
|
|
176
|
-
"masters/**",
|
|
177
|
-
".claude-plugin/**",
|
|
178
|
-
".cursor-plugin/**",
|
|
179
|
-
"gemini-extension.json",
|
|
180
|
-
".github/PULL_REQUEST_TEMPLATE.md",
|
|
181
|
-
".github/ISSUE_TEMPLATE/**",
|
|
182
|
-
}
|
|
183
|
-
uncovered = sorted(t for t in required if not _covered(t, paths))
|
|
184
|
-
assert not uncovered, f"push trigger does not cover: {uncovered}"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def test_pick_step_uses_checked_selector_without_fixed_roster():
|
|
188
|
-
checkout = next(
|
|
189
|
-
step
|
|
190
|
-
for step in _job(WORKFLOW, "fidelity-smoke")["steps"]
|
|
191
|
-
if str(step.get("uses", "")).startswith("actions/checkout@")
|
|
192
|
-
)
|
|
193
|
-
assert checkout.get("with", {}).get("fetch-depth") == 0
|
|
194
|
-
step = _step(WORKFLOW, "fidelity-smoke", "Pick smoke target")
|
|
195
|
-
script = step["run"]
|
|
196
|
-
assert "if ! CHANGED=$(python scripts/select-fidelity-smoke.py" in script
|
|
197
|
-
assert "if ! DIFF_OUTPUT=$(git diff --name-only" in script
|
|
198
|
-
assert "CHANGED_CANDIDATES" in script
|
|
199
|
-
assert "--prebuilt prebuilt" in script
|
|
200
|
-
assert '--day-of-year "$(date +%j)"' in script
|
|
201
|
-
assert "MASTERS=(" not in script
|
|
202
|
-
_assert_hard(step)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def test_smoke_selector_producer_failure_is_not_masked(tmp_path: Path):
|
|
206
|
-
pick_step = _step(WORKFLOW, "fidelity-smoke", "Pick smoke target")
|
|
207
|
-
script = pick_step["run"].replace("${{ github.base_ref || 'main' }}", "main")
|
|
208
|
-
|
|
209
|
-
bin_dir = tmp_path / "bin"
|
|
210
|
-
bin_dir.mkdir()
|
|
211
|
-
fake_python = bin_dir / "python"
|
|
212
|
-
fake_python.write_text(
|
|
213
|
-
"#!/bin/sh\nprintf 'master-partial\\n'\nexit 7\n",
|
|
214
|
-
encoding="utf-8",
|
|
215
|
-
)
|
|
216
|
-
fake_python.chmod(0o755)
|
|
217
|
-
output_path = tmp_path / "github-output"
|
|
218
|
-
env = os.environ.copy()
|
|
219
|
-
env["PATH"] = f"{bin_dir}:{env['PATH']}"
|
|
220
|
-
env["GITHUB_OUTPUT"] = str(output_path)
|
|
221
|
-
|
|
222
|
-
result = subprocess.run(
|
|
223
|
-
["bash", "-e", "-o", "pipefail", "-c", script],
|
|
224
|
-
cwd=ROOT,
|
|
225
|
-
env=env,
|
|
226
|
-
text=True,
|
|
227
|
-
capture_output=True,
|
|
228
|
-
check=False,
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
assert result.returncode != 0
|
|
232
|
-
assert not output_path.exists()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def test_smoke_git_diff_failure_is_not_masked(tmp_path: Path):
|
|
236
|
-
pick_step = _step(WORKFLOW, "fidelity-smoke", "Pick smoke target")
|
|
237
|
-
script = pick_step["run"].replace("${{ github.base_ref || 'main' }}", "main")
|
|
238
|
-
|
|
239
|
-
bin_dir = tmp_path / "bin"
|
|
240
|
-
bin_dir.mkdir()
|
|
241
|
-
fake_git = bin_dir / "git"
|
|
242
|
-
fake_git.write_text("#!/bin/sh\nexit 7\n", encoding="utf-8")
|
|
243
|
-
fake_git.chmod(0o755)
|
|
244
|
-
fake_python = bin_dir / "python"
|
|
245
|
-
fake_python.write_text(
|
|
246
|
-
"#!/bin/sh\nprintf 'master-alpha\\n'\n",
|
|
247
|
-
encoding="utf-8",
|
|
248
|
-
)
|
|
249
|
-
fake_python.chmod(0o755)
|
|
250
|
-
output_path = tmp_path / "github-output"
|
|
251
|
-
env = os.environ.copy()
|
|
252
|
-
env["PATH"] = f"{bin_dir}:{env['PATH']}"
|
|
253
|
-
env["GITHUB_OUTPUT"] = str(output_path)
|
|
254
|
-
|
|
255
|
-
result = subprocess.run(
|
|
256
|
-
["bash", "-e", "-o", "pipefail", "-c", script],
|
|
257
|
-
cwd=ROOT,
|
|
258
|
-
env=env,
|
|
259
|
-
text=True,
|
|
260
|
-
capture_output=True,
|
|
261
|
-
check=False,
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
assert result.returncode != 0
|
|
265
|
-
assert "git diff failed" in result.stdout
|
|
266
|
-
assert not output_path.exists()
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
@pytest.mark.parametrize(
|
|
270
|
-
("job_name", "step_name"),
|
|
271
|
-
[
|
|
272
|
-
("fidelity-smoke", "Run fidelity smoke"),
|
|
273
|
-
("fidelity-full", "Run fidelity tests"),
|
|
274
|
-
],
|
|
275
|
-
)
|
|
276
|
-
def test_each_fidelity_no_key_branch_records_step_summary(job_name: str, step_name: str):
|
|
277
|
-
job = _job(WORKFLOW, job_name)
|
|
278
|
-
assert job.get("needs") == "validate"
|
|
279
|
-
step = _step(WORKFLOW, job_name, step_name)
|
|
280
|
-
script = step["run"]
|
|
281
|
-
assert 'if [ -z "${ANTHROPIC_API_KEY:-}" ]; then' in script
|
|
282
|
-
assert script.count('echo "### Fidelity grading skipped"') == 1
|
|
283
|
-
assert '} >> "$GITHUB_STEP_SUMMARY"' in script
|
|
284
|
-
_assert_hard(step)
|