master-skill 0.10.1 → 0.11.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 (35) 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 +52 -297
  6. package/README_EN.md +52 -278
  7. package/bin/cli.mjs +237 -2
  8. package/gemini-extension.json +1 -1
  9. package/hooks/session-start +4 -1
  10. package/package.json +3 -2
  11. package/prebuilt/master-curriculum/SKILL.md +1 -1
  12. package/prebuilt/master-debate/SKILL.md +1 -1
  13. package/prebuilt/master-help/SKILL.md +86 -0
  14. package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
  15. package/prebuilt/master-kumarajiva/meta.json +14 -3
  16. package/prebuilt/master-nagarjuna/meta.json +19 -4
  17. package/prebuilt/master-tsongkhapa/meta.json +26 -5
  18. package/references/teaching-modes.md +8 -1
  19. package/routing.json +209 -0
  20. package/scripts/check-gate-liveness.py +222 -0
  21. package/scripts/test-fidelity.py +320 -49
  22. package/scripts/tests/test_check_gate_liveness.py +232 -0
  23. package/scripts/tests/test_check_response.py +190 -0
  24. package/scripts/tests/test_fidelity_providers.py +202 -0
  25. package/scripts/tests/test_select_fidelity_smoke.py +2 -2
  26. package/scripts/tests/test_validate.py +145 -0
  27. package/scripts/tests/test_validate_citation_contract.py +1 -1
  28. package/scripts/tests/test_validate_fidelity.py +2 -2
  29. package/scripts/tests/test_validate_workflow.py +21 -2
  30. package/scripts/validate-fidelity.py +6 -1
  31. package/scripts/validate-routing.py +254 -0
  32. package/scripts/validate.py +63 -36
  33. package/skill-catalog.json +83 -20
  34. /package/prebuilt/{compare → compare-masters}/SKILL.md +0 -0
  35. /package/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
@@ -0,0 +1,232 @@
1
+ """Behaviour tests for the gate-liveness meta-check.
2
+
3
+ Three of this repo's shipped defects were the same shape: a gate examined an
4
+ empty set and reported success.
5
+
6
+ - `pytest.ini` listed `testpaths = tests` while CI passed `scripts/tests/`,
7
+ so neither suite ever ran the other's cases (v0.10.1).
8
+ - `tests/test_voice_rules.py` globbed `prebuilt/<slug>/voice.md` when
9
+ voice.md lives under `references/`. The empty glob left every case
10
+ parametrized over an empty set: nothing asserted, green (v0.10.1).
11
+ - The fidelity smoke — a branch-protection-required check — writes
12
+ `{"skipped": true, "reason": "no_api_key"}` and exits 0 when the secret is
13
+ absent, which it always has been.
14
+
15
+ None of those is a wrong assertion. Each is an assertion that never ran. This
16
+ check exists to make "I examined nothing" fail loudly instead of passing
17
+ quietly.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import importlib.util
23
+ import json
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ import pytest
28
+
29
+
30
+ @pytest.fixture
31
+ def liveness():
32
+ scripts_dir = Path(__file__).resolve().parents[1]
33
+ if str(scripts_dir) not in sys.path:
34
+ sys.path.insert(0, str(scripts_dir))
35
+ spec = importlib.util.spec_from_file_location(
36
+ "check_gate_liveness", scripts_dir / "check-gate-liveness.py"
37
+ )
38
+ module = importlib.util.module_from_spec(spec)
39
+ sys.modules["check_gate_liveness"] = module
40
+ spec.loader.exec_module(module)
41
+ return module
42
+
43
+
44
+ # --------------------------------------------------------------------------
45
+ # Every test file must contribute at least one collected test.
46
+ # This is the voice_rules bug: the file exists, pytest imports it fine, and it
47
+ # yields nothing because the set it parametrizes over came back empty.
48
+ # --------------------------------------------------------------------------
49
+
50
+
51
+ def test_file_collecting_zero_tests_is_a_problem(liveness):
52
+ problems = liveness.check_every_test_file_collects(
53
+ test_files=["tests/test_voice_rules.py", "tests/test_cli.py"],
54
+ collected_counts={"tests/test_voice_rules.py": 0, "tests/test_cli.py": 7},
55
+ )
56
+ assert len(problems) == 1
57
+ assert "test_voice_rules.py" in problems[0]
58
+
59
+
60
+ def test_all_files_collecting_is_clean(liveness):
61
+ problems = liveness.check_every_test_file_collects(
62
+ test_files=["tests/a.py", "tests/b.py"],
63
+ collected_counts={"tests/a.py": 3, "tests/b.py": 1},
64
+ )
65
+ assert problems == []
66
+
67
+
68
+ def test_file_absent_from_collection_entirely_is_a_problem(liveness):
69
+ """Never collected at all is the same failure as collected-zero."""
70
+ problems = liveness.check_every_test_file_collects(
71
+ test_files=["scripts/tests/a.py"], collected_counts={}
72
+ )
73
+ assert len(problems) == 1
74
+ assert "scripts/tests/a.py" in problems[0]
75
+
76
+
77
+ # --------------------------------------------------------------------------
78
+ # testpaths must cover every directory that holds tests.
79
+ # This is the pytest.ini bug verbatim.
80
+ # --------------------------------------------------------------------------
81
+
82
+
83
+ def test_uncovered_test_directory_is_a_problem(liveness):
84
+ problems = liveness.check_testpaths_cover_suites(
85
+ testpaths=["tests"], test_dirs=["tests", "scripts/tests"]
86
+ )
87
+ assert len(problems) == 1
88
+ assert "scripts/tests" in problems[0]
89
+
90
+
91
+ def test_testpaths_covering_everything_is_clean(liveness):
92
+ problems = liveness.check_testpaths_cover_suites(
93
+ testpaths=["tests", "scripts/tests"], test_dirs=["tests", "scripts/tests"]
94
+ )
95
+ assert problems == []
96
+
97
+
98
+ # --------------------------------------------------------------------------
99
+ # A graded fidelity suite that graded nothing must not read as a pass.
100
+ # --------------------------------------------------------------------------
101
+
102
+
103
+ def test_graded_suite_with_no_graded_cases_is_a_problem(liveness):
104
+ problems = liveness.check_graded_suites_graded_something(
105
+ [{"master": "master-zhiyi", "mode": "graded", "results": []}]
106
+ )
107
+ assert len(problems) == 1
108
+ assert "master-zhiyi" in problems[0]
109
+
110
+
111
+ def test_graded_suite_of_only_api_errors_is_a_problem(liveness):
112
+ """The credit-exhaustion shape: 10 results, none of them a verdict."""
113
+ problems = liveness.check_graded_suites_graded_something(
114
+ [
115
+ {
116
+ "master": "master-xuyun",
117
+ "mode": "graded",
118
+ "results": [{"status": "api_error"}] * 10,
119
+ }
120
+ ]
121
+ )
122
+ assert len(problems) == 1
123
+ assert "master-xuyun" in problems[0]
124
+
125
+
126
+ def test_graded_suite_with_real_verdicts_is_clean(liveness):
127
+ problems = liveness.check_graded_suites_graded_something(
128
+ [
129
+ {
130
+ "master": "master-xuyun",
131
+ "mode": "graded",
132
+ "results": [{"status": "PASS"}, {"status": "FAIL"}],
133
+ }
134
+ ]
135
+ )
136
+ assert problems == []
137
+
138
+
139
+ def test_dry_run_suite_is_exempt(liveness):
140
+ """A dry run grades nothing by design — that is not the failure mode."""
141
+ problems = liveness.check_graded_suites_graded_something(
142
+ [{"master": "master-ouyi", "mode": "dry_run", "results": []}]
143
+ )
144
+ assert problems == []
145
+
146
+
147
+ # --------------------------------------------------------------------------
148
+ # Discovery drift: the catalog and the filesystem must agree.
149
+ # --------------------------------------------------------------------------
150
+
151
+
152
+ def test_catalog_entry_without_a_directory_is_a_problem(liveness, tmp_path):
153
+ prebuilt = tmp_path / "prebuilt"
154
+ (prebuilt / "master-huineng").mkdir(parents=True)
155
+ catalog = {
156
+ "skills": [
157
+ {"name": "master-huineng", "source": "prebuilt/master-huineng"},
158
+ {"name": "master-ghost", "source": "prebuilt/master-ghost"},
159
+ ]
160
+ }
161
+ problems = liveness.check_catalog_matches_filesystem(catalog, prebuilt, tmp_path)
162
+ assert any("master-ghost" in p for p in problems)
163
+
164
+
165
+ def test_directory_missing_from_catalog_is_a_problem(liveness, tmp_path):
166
+ prebuilt = tmp_path / "prebuilt"
167
+ (prebuilt / "master-huineng").mkdir(parents=True)
168
+ (prebuilt / "master-orphan").mkdir(parents=True)
169
+ catalog = {"skills": [{"name": "master-huineng", "source": "prebuilt/master-huineng"}]}
170
+ problems = liveness.check_catalog_matches_filesystem(catalog, prebuilt, tmp_path)
171
+ assert any("master-orphan" in p for p in problems)
172
+
173
+
174
+ def test_catalog_agreeing_with_filesystem_is_clean(liveness, tmp_path):
175
+ prebuilt = tmp_path / "prebuilt"
176
+ for slug in ("master-huineng", "compare-masters"):
177
+ (prebuilt / slug).mkdir(parents=True)
178
+ catalog = {
179
+ "skills": [
180
+ {"name": "master-huineng", "source": "prebuilt/master-huineng"},
181
+ {"name": "compare-masters", "source": "prebuilt/compare-masters"},
182
+ ]
183
+ }
184
+ problems = liveness.check_catalog_matches_filesystem(catalog, prebuilt, tmp_path)
185
+ assert problems == []
186
+
187
+
188
+ def test_empty_catalog_is_a_problem_not_a_vacuous_pass(liveness, tmp_path):
189
+ """The whole point: examining nothing must never read as success."""
190
+ prebuilt = tmp_path / "prebuilt"
191
+ prebuilt.mkdir(parents=True)
192
+ problems = liveness.check_catalog_matches_filesystem({"skills": []}, prebuilt, tmp_path)
193
+ assert len(problems) >= 1
194
+ assert any("empty" in p.lower() or "no skills" in p.lower() for p in problems)
195
+
196
+
197
+ # --------------------------------------------------------------------------
198
+ # Fixtures must exist and be non-empty, per skill.
199
+ # --------------------------------------------------------------------------
200
+
201
+
202
+ def test_empty_fixture_file_is_a_problem(liveness, tmp_path):
203
+ prebuilt = tmp_path / "prebuilt"
204
+ good = prebuilt / "master-a" / "tests"
205
+ good.mkdir(parents=True)
206
+ (good / "fidelity.jsonl").write_text(json.dumps({"q": "x"}) + "\n", encoding="utf-8")
207
+ empty = prebuilt / "master-b" / "tests"
208
+ empty.mkdir(parents=True)
209
+ (empty / "fidelity.jsonl").write_text("", encoding="utf-8")
210
+
211
+ problems = liveness.check_every_skill_has_fixtures(prebuilt)
212
+ assert len(problems) == 1
213
+ assert "master-b" in problems[0]
214
+
215
+
216
+ def test_missing_fixture_file_is_a_problem(liveness, tmp_path):
217
+ prebuilt = tmp_path / "prebuilt"
218
+ (prebuilt / "master-c").mkdir(parents=True)
219
+ problems = liveness.check_every_skill_has_fixtures(prebuilt)
220
+ assert len(problems) == 1
221
+ assert "master-c" in problems[0]
222
+
223
+
224
+ # --------------------------------------------------------------------------
225
+ # The real repo must pass its own check.
226
+ # --------------------------------------------------------------------------
227
+
228
+
229
+ def test_this_repo_passes_the_liveness_check(liveness):
230
+ root = Path(__file__).resolve().parents[2]
231
+ problems = liveness.run_all(root)
232
+ assert problems == [], "gate liveness problems: " + "; ".join(problems)
@@ -0,0 +1,190 @@
1
+ """Behaviour tests for the fidelity judge.
2
+
3
+ `check_response` decided every case in the first committed baseline
4
+ (`eval/reports/`) and had no test of its own. These cover the checks it
5
+ already performed, plus the echo rule that keeps a baited boundary question
6
+ from failing a persona for quoting the bait back.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+
18
+ @pytest.fixture
19
+ def fidelity():
20
+ scripts_dir = Path(__file__).resolve().parents[1]
21
+ # test-fidelity.py imports verify_citations as a sibling module.
22
+ if str(scripts_dir) not in sys.path:
23
+ sys.path.insert(0, str(scripts_dir))
24
+ spec = importlib.util.spec_from_file_location(
25
+ "test_fidelity_module", scripts_dir / "test-fidelity.py"
26
+ )
27
+ module = importlib.util.module_from_spec(spec)
28
+ sys.modules["test_fidelity_module"] = module
29
+ spec.loader.exec_module(module)
30
+ return module
31
+
32
+
33
+ # --------------------------------------------------------------------------
34
+ # Checks that already existed. These pin current behaviour so the echo rule
35
+ # below cannot quietly weaken them.
36
+ # --------------------------------------------------------------------------
37
+
38
+
39
+ def test_missing_citation_fails_and_is_named(fidelity):
40
+ check = fidelity.check_response(
41
+ "自性本自清净。", {"q": "什么是见性?", "must_cite": ["T48n2008"]}
42
+ )
43
+ assert check["passed"] is False
44
+ assert check["missing_cites"] == ["T48n2008"]
45
+
46
+
47
+ def test_missing_mention_fails_and_is_named(fidelity):
48
+ check = fidelity.check_response(
49
+ "念念无住即是。", {"q": "什么是无念?", "must_mention": ["无住", "不染"]}
50
+ )
51
+ assert check["passed"] is False
52
+ assert check["missing_mentions"] == ["不染"]
53
+
54
+
55
+ def test_everything_satisfied_passes(fidelity):
56
+ check = fidelity.check_response(
57
+ "无念者,于诸境上心不染。【《坛经》T48n2008】",
58
+ {"q": "什么是无念?", "must_cite": ["T48n2008"], "must_mention": ["不染"]},
59
+ )
60
+ assert check["passed"] is True
61
+
62
+
63
+ def test_first_turn_boundary_only_applies_on_the_first_turn(fidelity):
64
+ case = {"q": "请开示", "must_not_contain_first_turn": ["弟子"]}
65
+ assert fidelity.check_response("弟子当知", case, is_first_turn=True)["passed"] is False
66
+ assert fidelity.check_response("弟子当知", case, is_first_turn=False)["passed"] is True
67
+
68
+
69
+ # --------------------------------------------------------------------------
70
+ # The echo rule.
71
+ #
72
+ # `must_not_contain` is a substring match on the response, and boundary
73
+ # fixtures are baited questions carrying the loaded term themselves. A correct
74
+ # refusal — "你问『禅宗最究竟』,这个提法本身就把宗派当成了名次" — tripped the
75
+ # check exactly as hard as a real ranking did. 10 of the 12 forbidden-phrase
76
+ # failures in the first baseline were of this shape.
77
+ #
78
+ # Echoed terms are undecidable by substring matching, so they are recorded
79
+ # separately and flagged for review rather than counted as violations.
80
+ # --------------------------------------------------------------------------
81
+
82
+
83
+ def test_forbidden_term_absent_from_question_still_fails(fidelity):
84
+ """The genuine violation. Nothing about the echo rule may weaken this."""
85
+ check = fidelity.check_response(
86
+ "阿底峡比莲花生更高。",
87
+ {"q": "阿底峡是不是比莲花生大士更殊胜?", "must_not_contain": ["更高"]},
88
+ )
89
+ assert check["passed"] is False
90
+ assert check["forbidden_found"] == ["更高"]
91
+ assert check["forbidden_echoed"] == []
92
+
93
+
94
+ def test_forbidden_term_present_in_the_question_does_not_fail(fidelity):
95
+ """A refusal that names the bait must not be judged as taking the bait."""
96
+ check = fidelity.check_response(
97
+ "你问华严宗是不是最高的宗派——佛法平等,无有高下。",
98
+ {"q": "华严宗是不是佛教最高的宗派?", "must_not_contain": ["最高"]},
99
+ )
100
+ assert check["passed"] is True
101
+ assert check["forbidden_found"] == []
102
+ assert check["forbidden_echoed"] == ["最高"]
103
+
104
+
105
+ def test_echoed_term_marks_the_case_for_review(fidelity):
106
+ """Not failing is not the same as being cleared — surface it for a human."""
107
+ check = fidelity.check_response(
108
+ "你问华严宗是不是最高的宗派——佛法平等,无有高下。",
109
+ {"q": "华严宗是不是佛教最高的宗派?", "must_not_contain": ["最高"]},
110
+ )
111
+ assert check["needs_review"] is True
112
+
113
+
114
+ def test_case_without_echoes_is_not_marked_for_review(fidelity):
115
+ check = fidelity.check_response(
116
+ "佛法平等。", {"q": "华严宗是不是佛教最高的宗派?", "must_not_contain": ["最高"]}
117
+ )
118
+ assert check["needs_review"] is False
119
+ assert check["forbidden_echoed"] == []
120
+
121
+
122
+ def test_genuine_violation_still_fails_when_another_term_is_echoed(fidelity):
123
+ """Mixed case: one echoed, one genuine. The genuine one decides."""
124
+ check = fidelity.check_response(
125
+ "你问哪个更好——净土更高。",
126
+ {"q": "禅宗和净土宗哪个更好?", "must_not_contain": ["更好", "更高"]},
127
+ )
128
+ assert check["passed"] is False
129
+ assert check["forbidden_found"] == ["更高"]
130
+ assert check["forbidden_echoed"] == ["更好"]
131
+
132
+
133
+ def test_echo_rule_applies_to_first_turn_boundary_too(fidelity):
134
+ """Same substring flaw, same treatment."""
135
+ check = fidelity.check_response(
136
+ "你说「弟子」——这个称呼先放下。",
137
+ {"q": "弟子请问该如何用功?", "must_not_contain_first_turn": ["弟子"]},
138
+ )
139
+ assert check["passed"] is True
140
+ assert check["boundary_violations"] == []
141
+ assert check["boundary_echoed"] == ["弟子"]
142
+
143
+
144
+ def test_missing_mention_still_fails_an_otherwise_echoed_case(fidelity):
145
+ """The echo rule must not rescue a case that fails on another axis."""
146
+ check = fidelity.check_response(
147
+ "你问华严宗是不是最高——佛法平等。",
148
+ {
149
+ "q": "华严宗是不是佛教最高的宗派?",
150
+ "must_not_contain": ["最高"],
151
+ "must_mention": ["法界缘起"],
152
+ },
153
+ )
154
+ assert check["passed"] is False
155
+ assert check["missing_mentions"] == ["法界缘起"]
156
+ assert check["forbidden_echoed"] == ["最高"]
157
+
158
+
159
+ # --------------------------------------------------------------------------
160
+ # Response persistence.
161
+ #
162
+ # The first baseline stored only `response_length`, which left every failure
163
+ # unadjudicable after the fact — there was no way to revisit an echoed case
164
+ # and decide whether the persona ranked the traditions or refused to.
165
+ # --------------------------------------------------------------------------
166
+
167
+
168
+ def test_result_entry_persists_the_response_text(fidelity):
169
+ entry = fidelity.result_entry(
170
+ index=0,
171
+ test={"q": "什么是无念?", "test_type": "fidelity"},
172
+ check=fidelity.check_response("于诸境上心不染。", {"q": "什么是无念?"}),
173
+ response_text="于诸境上心不染。",
174
+ )
175
+ assert entry["response"] == "于诸境上心不染。"
176
+ assert entry["response_length"] == len("于诸境上心不染。")
177
+
178
+
179
+ def test_result_entry_carries_the_review_flag_and_echoes(fidelity):
180
+ test_case = {"q": "华严宗是不是佛教最高的宗派?", "must_not_contain": ["最高"]}
181
+ response = "你问是不是最高——佛法平等。"
182
+ entry = fidelity.result_entry(
183
+ index=3,
184
+ test=test_case,
185
+ check=fidelity.check_response(response, test_case),
186
+ response_text=response,
187
+ )
188
+ assert entry["status"] == "PASS"
189
+ assert entry["needs_review"] is True
190
+ assert entry["forbidden_echoed"] == ["最高"]
@@ -0,0 +1,202 @@
1
+ """Behaviour tests for multi-provider fidelity runs.
2
+
3
+ This project ships one `prebuilt/` to five hosts — Claude Code, Cursor, Codex
4
+ CLI, OpenCode, and Gemini CLI — and the README calls that a unified plugin. But
5
+ every fidelity number it has ever produced came from one Anthropic model. A
6
+ fixture measures whether the *prompt* induces the right behaviour, and that is a
7
+ property of the prompt-and-model pair, not of the prompt alone. The Gemini CLI
8
+ path in particular ships its own extension manifest and has zero evidence
9
+ behind it.
10
+
11
+ So provider is an axis of the eval matrix, not a cost workaround. The rule that
12
+ comes with it: a run records which provider and model produced it, and numbers
13
+ from different models are never averaged together.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import importlib.util
19
+ import sys
20
+ import types
21
+ from pathlib import Path
22
+
23
+ import pytest
24
+
25
+
26
+ @pytest.fixture
27
+ def fidelity():
28
+ scripts_dir = Path(__file__).resolve().parents[1]
29
+ if str(scripts_dir) not in sys.path:
30
+ sys.path.insert(0, str(scripts_dir))
31
+ spec = importlib.util.spec_from_file_location(
32
+ "test_fidelity_providers_mod", scripts_dir / "test-fidelity.py"
33
+ )
34
+ module = importlib.util.module_from_spec(spec)
35
+ sys.modules["test_fidelity_providers_mod"] = module
36
+ spec.loader.exec_module(module)
37
+ return module
38
+
39
+
40
+ # --------------------------------------------------------------------------
41
+ # Provider registry
42
+ # --------------------------------------------------------------------------
43
+
44
+
45
+ def test_anthropic_is_the_default_provider(fidelity):
46
+ assert fidelity.DEFAULT_PROVIDER == "anthropic"
47
+
48
+
49
+ def test_every_provider_declares_its_key_and_api_style(fidelity):
50
+ for name, spec in fidelity.PROVIDERS.items():
51
+ assert spec["env"].endswith("_API_KEY"), name
52
+ assert spec["api"] in {"anthropic", "openai"}, name
53
+ if spec["api"] == "openai":
54
+ assert spec["base_url"], f"{name} needs a base_url"
55
+
56
+
57
+ def test_the_three_shipped_hosts_are_covered(fidelity):
58
+ assert {"anthropic", "deepseek", "gemini"} <= set(fidelity.PROVIDERS)
59
+
60
+
61
+ def test_unknown_provider_is_rejected_by_name(fidelity):
62
+ with pytest.raises(ValueError) as excinfo:
63
+ fidelity.resolve_provider("llama-at-home")
64
+ assert "llama-at-home" in str(excinfo.value)
65
+
66
+
67
+ # --------------------------------------------------------------------------
68
+ # Model resolution.
69
+ #
70
+ # Anthropic keeps its default so nothing that exists today changes. Every other
71
+ # provider must be told explicitly: shipping a guessed model id would rot, and
72
+ # a run that cannot name its model is not reproducible.
73
+ # --------------------------------------------------------------------------
74
+
75
+
76
+ def test_anthropic_keeps_its_default_model(fidelity):
77
+ assert fidelity.resolve_model("anthropic", None) == "claude-sonnet-4-6"
78
+
79
+
80
+ def test_explicit_model_always_wins(fidelity):
81
+ assert fidelity.resolve_model("anthropic", "claude-opus-4-8") == "claude-opus-4-8"
82
+ assert fidelity.resolve_model("deepseek", "deepseek-chat") == "deepseek-chat"
83
+
84
+
85
+ def test_non_anthropic_provider_without_a_model_is_an_error(fidelity):
86
+ for provider in ("deepseek", "gemini"):
87
+ with pytest.raises(ValueError) as excinfo:
88
+ fidelity.resolve_model(provider, None)
89
+ message = str(excinfo.value)
90
+ assert provider in message
91
+ assert "--model" in message
92
+
93
+
94
+ # --------------------------------------------------------------------------
95
+ # Request shape differs per API style; both must carry the same system prompt
96
+ # and the same question.
97
+ # --------------------------------------------------------------------------
98
+
99
+
100
+ def test_anthropic_request_keeps_system_at_the_top_level(fidelity):
101
+ req = fidelity.build_request("anthropic", "claude-sonnet-4-6", "SYS", "Q?", 2048)
102
+ assert req["model"] == "claude-sonnet-4-6"
103
+ assert req["system"] == "SYS"
104
+ assert req["max_tokens"] == 2048
105
+ assert req["messages"] == [{"role": "user", "content": "Q?"}]
106
+
107
+
108
+ def test_openai_style_request_moves_system_into_messages(fidelity):
109
+ req = fidelity.build_request("deepseek", "deepseek-chat", "SYS", "Q?", 2048)
110
+ assert req["model"] == "deepseek-chat"
111
+ assert "system" not in req
112
+ assert req["messages"] == [
113
+ {"role": "system", "content": "SYS"},
114
+ {"role": "user", "content": "Q?"},
115
+ ]
116
+
117
+
118
+ def test_openai_style_uses_max_tokens_key_the_sdk_expects(fidelity):
119
+ req = fidelity.build_request("gemini", "gemini-x", "SYS", "Q?", 1024)
120
+ assert req.get("max_tokens") == 1024
121
+
122
+
123
+ # --------------------------------------------------------------------------
124
+ # Response extraction differs per API style.
125
+ # --------------------------------------------------------------------------
126
+
127
+
128
+ def _anthropic_response(text):
129
+ block = types.SimpleNamespace(type="text", text=text)
130
+ return types.SimpleNamespace(content=[block])
131
+
132
+
133
+ def _openai_response(text):
134
+ message = types.SimpleNamespace(content=text)
135
+ return types.SimpleNamespace(choices=[types.SimpleNamespace(message=message)])
136
+
137
+
138
+ def test_extracts_text_from_an_anthropic_response(fidelity):
139
+ assert fidelity.extract_text("anthropic", _anthropic_response("无念者…")) == "无念者…"
140
+
141
+
142
+ def test_extracts_text_from_an_openai_style_response(fidelity):
143
+ assert fidelity.extract_text("deepseek", _openai_response("自性本清净")) == "自性本清净"
144
+
145
+
146
+ def test_empty_response_content_raises_rather_than_scoring_a_blank(fidelity):
147
+ """A blank answer must not be silently graded — it would fail every
148
+ must_mention and be recorded as a persona defect."""
149
+ with pytest.raises(ValueError):
150
+ fidelity.extract_text("deepseek", types.SimpleNamespace(choices=[]))
151
+
152
+
153
+ # --------------------------------------------------------------------------
154
+ # Provenance: a suite has to name the instrument that produced it.
155
+ # --------------------------------------------------------------------------
156
+
157
+
158
+ def test_suite_records_the_provider(fidelity):
159
+ suite = fidelity.suite_common("master-zhiyi", False, "completed", provider="deepseek")
160
+ assert suite["provider"] == "deepseek"
161
+
162
+
163
+ def test_suite_defaults_to_anthropic_for_older_callers(fidelity):
164
+ assert fidelity.suite_common("master-zhiyi", True, "completed")["provider"] == "anthropic"
165
+
166
+
167
+ def test_error_suite_also_records_the_provider(fidelity):
168
+ suite = fidelity.suite_error("master-zhiyi", False, "boom", provider="gemini")
169
+ assert suite["provider"] == "gemini"
170
+
171
+
172
+ # --------------------------------------------------------------------------
173
+ # Cross-model aggregation is the mistake this axis makes easy. Refuse it.
174
+ # --------------------------------------------------------------------------
175
+
176
+
177
+ def test_aggregating_one_model_is_fine(fidelity):
178
+ suites = [
179
+ {"provider": "anthropic", "model": "claude-sonnet-4-6", "results": []},
180
+ {"provider": "anthropic", "model": "claude-sonnet-4-6", "results": []},
181
+ ]
182
+ assert fidelity.aggregation_conflicts(suites) == []
183
+
184
+
185
+ def test_aggregating_two_models_is_refused_by_name(fidelity):
186
+ suites = [
187
+ {"provider": "anthropic", "model": "claude-sonnet-4-6", "results": []},
188
+ {"provider": "deepseek", "model": "deepseek-chat", "results": []},
189
+ ]
190
+ conflicts = fidelity.aggregation_conflicts(suites)
191
+ assert conflicts
192
+ joined = " ".join(conflicts)
193
+ assert "claude-sonnet-4-6" in joined and "deepseek-chat" in joined
194
+
195
+
196
+ def test_same_provider_different_model_is_still_refused(fidelity):
197
+ """Sonnet and Opus are different instruments too."""
198
+ suites = [
199
+ {"provider": "anthropic", "model": "claude-sonnet-4-6", "results": []},
200
+ {"provider": "anthropic", "model": "claude-opus-4-8", "results": []},
201
+ ]
202
+ assert fidelity.aggregation_conflicts(suites)
@@ -86,7 +86,7 @@ def test_meta_skill_before_persona_does_not_hide_changed_persona(tmp_path: Path)
86
86
  result = _run_selector(
87
87
  prebuilt,
88
88
  "008",
89
- ["compare", "master-beta", "master-alpha"],
89
+ ["compare-masters", "master-beta", "master-alpha"],
90
90
  )
91
91
 
92
92
  assert result.returncode == 0, result.stderr
@@ -95,7 +95,7 @@ def test_meta_skill_before_persona_does_not_hide_changed_persona(tmp_path: Path)
95
95
 
96
96
  def test_discovery_ignores_meta_skills_and_empty_sources(tmp_path: Path):
97
97
  prebuilt = tmp_path / "prebuilt"
98
- _write_meta(prebuilt, "compare", [{"id": "meta"}])
98
+ _write_meta(prebuilt, "compare-masters", [{"id": "meta"}])
99
99
  _write_meta(prebuilt, "master-empty", [])
100
100
  _write_meta(prebuilt, "master-valid", [{"id": "source"}])
101
101