master-skill 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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,190 +0,0 @@
|
|
|
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"] == ["最高"]
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""Tests for master-debate meta.json debate_protocol schema.
|
|
2
|
-
|
|
3
|
-
Verifies:
|
|
4
|
-
1. master-debate/meta.json exists, has a `debate_protocol` object
|
|
5
|
-
2. Required scalar fields with valid types + sane ranges
|
|
6
|
-
3. min_rounds <= default_rounds <= max_rounds
|
|
7
|
-
4. per_pair_overrides keys are <slug_a>-vs-<slug_b> with both slugs being
|
|
8
|
-
real masters under prebuilt/
|
|
9
|
-
5. Each pair listed in per_pair_overrides is also covered bidirectionally
|
|
10
|
-
by cross_critique (i.e. the per-pair list does not invent debate pairs
|
|
11
|
-
that v0.7.1 didn't actually arm with ammo)
|
|
12
|
-
6. per_pair_overrides values have valid default_rounds in
|
|
13
|
-
[debate_protocol.min_rounds, debate_protocol.max_rounds]
|
|
14
|
-
"""
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
import pytest
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
24
|
-
PREBUILT = REPO_ROOT / "prebuilt"
|
|
25
|
-
DEBATE_META = PREBUILT / "master-debate" / "meta.json"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _load_json(p: Path) -> dict:
|
|
29
|
-
return json.loads(p.read_text(encoding="utf-8"))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.fixture(scope="module")
|
|
33
|
-
def debate_meta():
|
|
34
|
-
assert DEBATE_META.exists(), f"missing {DEBATE_META}"
|
|
35
|
-
return _load_json(DEBATE_META)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pytest.fixture(scope="module")
|
|
39
|
-
def protocol(debate_meta):
|
|
40
|
-
proto = debate_meta.get("debate_protocol")
|
|
41
|
-
assert isinstance(proto, dict), "debate_protocol must be an object"
|
|
42
|
-
return proto
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@pytest.fixture(scope="module")
|
|
46
|
-
def known_slugs():
|
|
47
|
-
return {p.parent.name.removeprefix("master-")
|
|
48
|
-
for p in PREBUILT.glob("master-*/meta.json")}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_protocol_required_scalars(protocol):
|
|
52
|
-
for key, typ in [
|
|
53
|
-
("default_rounds", int),
|
|
54
|
-
("max_rounds", int),
|
|
55
|
-
("min_rounds", int),
|
|
56
|
-
("selector", str),
|
|
57
|
-
("stop_on_consensus", bool),
|
|
58
|
-
("subagent_isolation", bool),
|
|
59
|
-
("moderator_summary", bool),
|
|
60
|
-
]:
|
|
61
|
-
assert key in protocol, f"missing debate_protocol.{key}"
|
|
62
|
-
assert isinstance(protocol[key], typ), (
|
|
63
|
-
f"debate_protocol.{key} must be {typ.__name__}, "
|
|
64
|
-
f"got {type(protocol[key]).__name__}"
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_protocol_round_ranges(protocol):
|
|
69
|
-
min_r = protocol["min_rounds"]
|
|
70
|
-
def_r = protocol["default_rounds"]
|
|
71
|
-
max_r = protocol["max_rounds"]
|
|
72
|
-
assert min_r >= 1, "min_rounds must be >= 1"
|
|
73
|
-
assert min_r <= def_r <= max_r, (
|
|
74
|
-
f"require min_rounds <= default_rounds <= max_rounds, "
|
|
75
|
-
f"got {min_r} <= {def_r} <= {max_r}"
|
|
76
|
-
)
|
|
77
|
-
assert max_r <= 10, "max_rounds sanity cap"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_protocol_selector_alternating(protocol):
|
|
81
|
-
assert protocol["selector"] == "alternating", (
|
|
82
|
-
"v0.8 only ships alternating selector"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_protocol_subagent_isolation_on(protocol):
|
|
87
|
-
assert protocol["subagent_isolation"] is True, (
|
|
88
|
-
"v0.8 requires subagent_isolation=true (the whole point of the refactor)"
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_per_pair_overrides_is_object(protocol):
|
|
93
|
-
overrides = protocol.get("per_pair_overrides")
|
|
94
|
-
assert isinstance(overrides, dict), (
|
|
95
|
-
"debate_protocol.per_pair_overrides must be an object"
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_per_pair_keys_well_formed_and_real_slugs(protocol, known_slugs):
|
|
100
|
-
overrides = protocol["per_pair_overrides"]
|
|
101
|
-
for key in overrides:
|
|
102
|
-
assert "-vs-" in key, f"per-pair key '{key}' must contain '-vs-'"
|
|
103
|
-
parts = key.split("-vs-")
|
|
104
|
-
assert len(parts) == 2, f"per-pair key '{key}' must split into exactly 2 slugs"
|
|
105
|
-
a, b = parts
|
|
106
|
-
assert a != b, f"per-pair key '{key}' self-pair"
|
|
107
|
-
assert a in known_slugs, f"per-pair key '{key}': slug '{a}' not a known master"
|
|
108
|
-
assert b in known_slugs, f"per-pair key '{key}': slug '{b}' not a known master"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def test_per_pair_keys_are_alphabetically_sorted(protocol):
|
|
112
|
-
"""SKILL.md tells the orchestrator to compute the lookup key by sorting
|
|
113
|
-
the two slugs alphabetically. If a key here isn't sorted, the orchestrator
|
|
114
|
-
silently misses the override. Lock the convention in."""
|
|
115
|
-
for key in protocol["per_pair_overrides"]:
|
|
116
|
-
a, b = key.split("-vs-", 1)
|
|
117
|
-
assert a < b, (
|
|
118
|
-
f"per-pair key '{key}' must be alphabetically sorted "
|
|
119
|
-
f"('{a}' < '{b}'); orchestrators compute the key by sorting slugs."
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def test_per_pair_default_rounds_in_protocol_range(protocol):
|
|
124
|
-
min_r = protocol["min_rounds"]
|
|
125
|
-
max_r = protocol["max_rounds"]
|
|
126
|
-
for key, val in protocol["per_pair_overrides"].items():
|
|
127
|
-
assert isinstance(val, dict), f"override for '{key}' must be object"
|
|
128
|
-
dr = val.get("default_rounds")
|
|
129
|
-
assert isinstance(dr, int), f"override '{key}'.default_rounds must be int"
|
|
130
|
-
assert min_r <= dr <= max_r, (
|
|
131
|
-
f"override '{key}'.default_rounds={dr} out of "
|
|
132
|
-
f"[{min_r}, {max_r}]"
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def test_per_pair_overrides_cross_critique_covered(protocol):
|
|
137
|
-
"""Each per-pair override must correspond to a pair both directions of
|
|
138
|
-
which appear in cross_critique entries — otherwise the override would
|
|
139
|
-
fire a debate the v0.7.1 ammo system didn't arm."""
|
|
140
|
-
overrides = protocol["per_pair_overrides"]
|
|
141
|
-
|
|
142
|
-
# Build {(src, tgt)} set from all cross_critique entries.
|
|
143
|
-
pairs = set()
|
|
144
|
-
for meta_path in PREBUILT.glob("master-*/meta.json"):
|
|
145
|
-
src = meta_path.parent.name.removeprefix("master-")
|
|
146
|
-
data = _load_json(meta_path)
|
|
147
|
-
for e in data.get("cross_critique", []) or []:
|
|
148
|
-
tgt = e.get("target_master")
|
|
149
|
-
if isinstance(tgt, str):
|
|
150
|
-
pairs.add((src, tgt))
|
|
151
|
-
|
|
152
|
-
for key in overrides:
|
|
153
|
-
a, b = key.split("-vs-", 1)
|
|
154
|
-
assert (a, b) in pairs, (
|
|
155
|
-
f"override '{key}': cross_critique missing direction {a}→{b}"
|
|
156
|
-
)
|
|
157
|
-
assert (b, a) in pairs, (
|
|
158
|
-
f"override '{key}': cross_critique missing direction {b}→{a}"
|
|
159
|
-
)
|
|
@@ -1,202 +0,0 @@
|
|
|
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)
|