master-skill 0.10.1 → 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 (77) 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/GEMINI.md +1 -1
  5. package/README.md +84 -336
  6. package/README_EN.md +108 -321
  7. package/bin/cli.mjs +249 -9
  8. package/gemini-extension.json +1 -1
  9. package/hooks/session-start +68 -74
  10. package/hooks/session_start.py +152 -0
  11. package/package.json +6 -2
  12. package/prebuilt/{compare → compare-masters}/SKILL.md +21 -2
  13. package/prebuilt/master-ajahn-chah/meta.json +6 -0
  14. package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
  15. package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
  16. package/prebuilt/master-curriculum/SKILL.md +1 -1
  17. package/prebuilt/master-curriculum/references/tiantai.md +1 -1
  18. package/prebuilt/master-debate/SKILL.md +15 -3
  19. package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
  20. package/prebuilt/master-help/SKILL.md +94 -0
  21. package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
  22. package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
  23. package/prebuilt/master-kumarajiva/meta.json +14 -3
  24. package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
  25. package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
  26. package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
  27. package/prebuilt/master-nagarjuna/meta.json +19 -4
  28. package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
  29. package/prebuilt/master-ouyi/meta.json +5 -0
  30. package/prebuilt/master-ouyi/references/teaching.md +3 -3
  31. package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
  32. package/prebuilt/master-tsongkhapa/meta.json +32 -5
  33. package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
  34. package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
  35. package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
  36. package/prebuilt/master-zhiyi/meta.json +2 -2
  37. package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
  38. package/references/teaching-modes.md +8 -1
  39. package/routing.json +209 -0
  40. package/scripts/check-audit-ignores.py +105 -0
  41. package/scripts/check-eval-sdk-surface.py +142 -0
  42. package/scripts/check-gate-liveness.py +421 -0
  43. package/scripts/reaudit-report.py +163 -0
  44. package/scripts/regrade-report.py +157 -0
  45. package/scripts/smoke-eval-sdk.py +174 -0
  46. package/scripts/test-fidelity.py +992 -89
  47. package/scripts/validate-citation-references.py +150 -0
  48. package/scripts/validate-citation-templates.py +176 -0
  49. package/scripts/validate-fidelity.py +6 -1
  50. package/scripts/validate-fixture-terms.py +127 -0
  51. package/scripts/validate-routing.py +254 -0
  52. package/scripts/validate.py +63 -36
  53. package/scripts/verify-adjudication.py +316 -0
  54. package/scripts/verify_citations.py +739 -39
  55. package/skill-catalog.json +83 -20
  56. package/tools/cross_reference.py +44 -10
  57. package/tools/fojin-known-absent.json +14 -0
  58. package/tools/fojin_bridge.py +138 -8
  59. package/tools/rag_query.py +45 -2
  60. package/tools/skill_writer.py +50 -7
  61. package/tools/verify_sources.py +240 -15
  62. package/hooks/tests/test_run_hook.sh +0 -114
  63. package/hooks/tests/test_run_hook_cmd.sh +0 -94
  64. package/hooks/tests/test_session_start.sh +0 -149
  65. package/scripts/tests/test_check_manifest_versions.py +0 -217
  66. package/scripts/tests/test_debate_protocol.py +0 -159
  67. package/scripts/tests/test_injection_hardening.py +0 -174
  68. package/scripts/tests/test_select_fidelity_smoke.py +0 -142
  69. package/scripts/tests/test_validate_citation_contract.py +0 -408
  70. package/scripts/tests/test_validate_cross_critique.py +0 -149
  71. package/scripts/tests/test_validate_curriculum_sources.py +0 -144
  72. package/scripts/tests/test_validate_fidelity.py +0 -59
  73. package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
  74. package/scripts/tests/test_validate_persona_fidelity.py +0 -317
  75. package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
  76. package/scripts/tests/test_validate_workflow.py +0 -265
  77. /package/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
@@ -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,265 +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 test_push_paths_include_distribution_and_generator_runtime():
144
- triggers = WORKFLOW.get("on", WORKFLOW.get(True))
145
- assert isinstance(triggers, dict)
146
- paths = set(triggers["push"]["paths"])
147
- assert {
148
- "skill-catalog.json",
149
- "SKILL.md",
150
- "references/**",
151
- "ETHICS.md",
152
- "requirements.txt",
153
- "README.md",
154
- "README_EN.md",
155
- "CONTRIBUTING.md",
156
- "CHANGELOG.md",
157
- "docs/PRD.md",
158
- "docs/v1-framework-roadmap.md",
159
- "masters/**",
160
- ".claude-plugin/**",
161
- ".cursor-plugin/**",
162
- "gemini-extension.json",
163
- ".github/PULL_REQUEST_TEMPLATE.md",
164
- ".github/ISSUE_TEMPLATE/**",
165
- } <= paths
166
-
167
-
168
- def test_pick_step_uses_checked_selector_without_fixed_roster():
169
- checkout = next(
170
- step
171
- for step in _job(WORKFLOW, "fidelity-smoke")["steps"]
172
- if str(step.get("uses", "")).startswith("actions/checkout@")
173
- )
174
- assert checkout.get("with", {}).get("fetch-depth") == 0
175
- step = _step(WORKFLOW, "fidelity-smoke", "Pick smoke target")
176
- script = step["run"]
177
- assert "if ! CHANGED=$(python scripts/select-fidelity-smoke.py" in script
178
- assert "if ! DIFF_OUTPUT=$(git diff --name-only" in script
179
- assert "CHANGED_CANDIDATES" in script
180
- assert "--prebuilt prebuilt" in script
181
- assert '--day-of-year "$(date +%j)"' in script
182
- assert "MASTERS=(" not in script
183
- _assert_hard(step)
184
-
185
-
186
- def test_smoke_selector_producer_failure_is_not_masked(tmp_path: Path):
187
- pick_step = _step(WORKFLOW, "fidelity-smoke", "Pick smoke target")
188
- script = pick_step["run"].replace("${{ github.base_ref || 'main' }}", "main")
189
-
190
- bin_dir = tmp_path / "bin"
191
- bin_dir.mkdir()
192
- fake_python = bin_dir / "python"
193
- fake_python.write_text(
194
- "#!/bin/sh\nprintf 'master-partial\\n'\nexit 7\n",
195
- encoding="utf-8",
196
- )
197
- fake_python.chmod(0o755)
198
- output_path = tmp_path / "github-output"
199
- env = os.environ.copy()
200
- env["PATH"] = f"{bin_dir}:{env['PATH']}"
201
- env["GITHUB_OUTPUT"] = str(output_path)
202
-
203
- result = subprocess.run(
204
- ["bash", "-e", "-o", "pipefail", "-c", script],
205
- cwd=ROOT,
206
- env=env,
207
- text=True,
208
- capture_output=True,
209
- check=False,
210
- )
211
-
212
- assert result.returncode != 0
213
- assert not output_path.exists()
214
-
215
-
216
- def test_smoke_git_diff_failure_is_not_masked(tmp_path: Path):
217
- pick_step = _step(WORKFLOW, "fidelity-smoke", "Pick smoke target")
218
- script = pick_step["run"].replace("${{ github.base_ref || 'main' }}", "main")
219
-
220
- bin_dir = tmp_path / "bin"
221
- bin_dir.mkdir()
222
- fake_git = bin_dir / "git"
223
- fake_git.write_text("#!/bin/sh\nexit 7\n", encoding="utf-8")
224
- fake_git.chmod(0o755)
225
- fake_python = bin_dir / "python"
226
- fake_python.write_text(
227
- "#!/bin/sh\nprintf 'master-alpha\\n'\n",
228
- encoding="utf-8",
229
- )
230
- fake_python.chmod(0o755)
231
- output_path = tmp_path / "github-output"
232
- env = os.environ.copy()
233
- env["PATH"] = f"{bin_dir}:{env['PATH']}"
234
- env["GITHUB_OUTPUT"] = str(output_path)
235
-
236
- result = subprocess.run(
237
- ["bash", "-e", "-o", "pipefail", "-c", script],
238
- cwd=ROOT,
239
- env=env,
240
- text=True,
241
- capture_output=True,
242
- check=False,
243
- )
244
-
245
- assert result.returncode != 0
246
- assert "git diff failed" in result.stdout
247
- assert not output_path.exists()
248
-
249
-
250
- @pytest.mark.parametrize(
251
- ("job_name", "step_name"),
252
- [
253
- ("fidelity-smoke", "Run fidelity smoke"),
254
- ("fidelity-full", "Run fidelity tests"),
255
- ],
256
- )
257
- def test_each_fidelity_no_key_branch_records_step_summary(job_name: str, step_name: str):
258
- job = _job(WORKFLOW, job_name)
259
- assert job.get("needs") == "validate"
260
- step = _step(WORKFLOW, job_name, step_name)
261
- script = step["run"]
262
- assert 'if [ -z "${ANTHROPIC_API_KEY:-}" ]; then' in script
263
- assert script.count('echo "### Fidelity grading skipped"') == 1
264
- assert '} >> "$GITHUB_STEP_SUMMARY"' in script
265
- _assert_hard(step)