Skip to content

Commit 6126552

Browse files
nesquenaclaude
andcommitted
test: add behavioural regression tests for #1188 fuzzy-match fix
9 tests run the live _findModelInDropdown function via Node so the real regex/normalization rules are exercised (no Python mirror to drift). Two locked-bad cases (gpt-5.5 → gpt-5.4-mini, claude-opus-4.7 → claude-opus-4.6) reproduce on master and pass on the PR. Seven preserved-good cases (bare-root prefix match, exact match, unrelated) ensure the tighter check doesn't regress legit fuzzy lookups. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent bec931a commit 6126552

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""
2+
Regression tests for #1188 — _findModelInDropdown step-3 fuzzy match
3+
returning a sibling-version model when the user's session model is unique.
4+
5+
The pre-fix logic stripped the trailing version segment from the target
6+
(e.g. ``gpt-5.5`` → base ``gpt.5``) and matched against any option that
7+
``startsWith(base)`` or ``includes(base)``. That over-matched: ``gpt.5.5``
8+
returned ``@nous:openai/gpt-5.4-mini`` because ``gpt.5.4.mini`` starts
9+
with ``gpt.5``.
10+
11+
The fix: use the FULL normalized target as the prefix when the stripped
12+
base has meaningful content (length > 4 and base !== target). Only fall
13+
back to the shorter base when it is a bare root word (``gpt``, ``claude``,
14+
length ≤ 4) where stripping was effectively a no-op.
15+
16+
Tests below run the live ``_findModelInDropdown`` function via Node so
17+
the real regex/normalization rules are exercised — drift between this
18+
test and the JS would be caught by behavioural mismatch.
19+
"""
20+
import shutil
21+
import subprocess
22+
23+
import pytest
24+
25+
REPO_ROOT = __import__("pathlib").Path(__file__).parent.parent.resolve()
26+
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
27+
NODE = shutil.which("node")
28+
29+
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
30+
31+
32+
_DRIVER_SRC = r"""
33+
const fs = require('fs');
34+
const ui = fs.readFileSync(process.argv[2], 'utf8');
35+
function extractFunc(name) {
36+
const re = new RegExp('function\\s+' + name + '\\s*\\(');
37+
const start = ui.search(re);
38+
if (start < 0) throw new Error(name + ' not found');
39+
let i = ui.indexOf('{', start);
40+
let depth = 1; i++;
41+
while (depth > 0 && i < ui.length) {
42+
if (ui[i] === '{') depth++;
43+
else if (ui[i] === '}') depth--;
44+
i++;
45+
}
46+
return ui.slice(start, i);
47+
}
48+
eval(extractFunc('_findModelInDropdown'));
49+
const args = JSON.parse(process.argv[3]);
50+
const sel = { options: args.options.map(v => ({value: v})) };
51+
const got = _findModelInDropdown(args.modelId, sel);
52+
process.stdout.write(JSON.stringify(got));
53+
"""
54+
55+
56+
@pytest.fixture(scope="module")
57+
def driver_path(tmp_path_factory):
58+
p = tmp_path_factory.mktemp("findmodel_driver") / "driver.js"
59+
p.write_text(_DRIVER_SRC, encoding="utf-8")
60+
return str(p)
61+
62+
63+
def _find(driver_path, model_id: str, options: list[str]):
64+
import json
65+
result = subprocess.run(
66+
[NODE, driver_path, str(UI_JS_PATH),
67+
json.dumps({"modelId": model_id, "options": options})],
68+
capture_output=True, text=True, timeout=10,
69+
)
70+
if result.returncode != 0:
71+
raise RuntimeError(f"node driver failed: {result.stderr}")
72+
return json.loads(result.stdout)
73+
74+
75+
# ── Regression: original #1188 false-match cases ───────────────────────────
76+
77+
78+
class TestNoFalseSiblingVersionMatch:
79+
def test_gpt_5_5_does_not_match_gpt_5_4_mini(self, driver_path):
80+
"""The exact bug from #1188: session model gpt-5.5 should NOT
81+
resolve to @nous:openai/gpt-5.4-mini just because both share
82+
the gpt.5 prefix once the trailing version is stripped."""
83+
got = _find(
84+
driver_path,
85+
"gpt-5.5",
86+
["@nous:openai/gpt-5.4-mini", "@nous:anthropic/claude-opus-4.6"],
87+
)
88+
assert got is None, (
89+
"gpt-5.5 must not fuzzy-match gpt-5.4-mini (#1188)"
90+
)
91+
92+
def test_claude_opus_4_7_does_not_match_claude_opus_4_6(self, driver_path):
93+
"""Same shape: a different minor version must not be a fuzzy hit."""
94+
got = _find(
95+
driver_path,
96+
"claude-opus-4.7",
97+
["@nous:anthropic/claude-opus-4.6"],
98+
)
99+
assert got is None, (
100+
"claude-opus-4.7 must not fuzzy-match claude-opus-4.6"
101+
)
102+
103+
104+
# ── Things that should still match (no regression for legit fuzzy use) ─────
105+
106+
107+
class TestPreservedFuzzyMatches:
108+
def test_gpt_5_5_finds_exact_provider_prefixed(self, driver_path):
109+
got = _find(
110+
driver_path,
111+
"gpt-5.5",
112+
["@nous:openai/gpt-5.5", "@nous:openai/gpt-5.4-mini"],
113+
)
114+
assert got == "@nous:openai/gpt-5.5"
115+
116+
def test_bare_root_gpt_matches_versioned_option(self, driver_path):
117+
"""Short root targets still fall back to the looser prefix match."""
118+
got = _find(driver_path, "gpt", ["@nous:openai/gpt-5.4-mini"])
119+
assert got == "@nous:openai/gpt-5.4-mini"
120+
121+
def test_short_target_gpt_5_falls_back_to_bare_root(self, driver_path):
122+
"""When base after stripping is a bare root (length ≤ 4),
123+
fall back so user-typed shorthand still resolves."""
124+
got = _find(driver_path, "gpt-5", ["@nous:openai/gpt-5.4-mini"])
125+
assert got == "@nous:openai/gpt-5.4-mini"
126+
127+
def test_bare_root_claude_matches(self, driver_path):
128+
got = _find(
129+
driver_path, "claude", ["@nous:anthropic/claude-opus-4.6"]
130+
)
131+
assert got == "@nous:anthropic/claude-opus-4.6"
132+
133+
def test_target_without_version_suffix_still_matches(self, driver_path):
134+
"""claude-opus has no trailing version → base === target → useBase
135+
path → still finds claude-opus-4.6 via prefix."""
136+
got = _find(
137+
driver_path, "claude-opus", ["@nous:anthropic/claude-opus-4.6"]
138+
)
139+
assert got == "@nous:anthropic/claude-opus-4.6"
140+
141+
def test_exact_match_short_circuits(self, driver_path):
142+
got = _find(
143+
driver_path,
144+
"gpt-5.4-mini",
145+
["@nous:openai/gpt-5.4-mini", "@nous:openai/gpt-5.5"],
146+
)
147+
assert got == "@nous:openai/gpt-5.4-mini"
148+
149+
def test_unrelated_target_returns_null(self, driver_path):
150+
got = _find(
151+
driver_path, "mistral-large", ["@nous:openai/gpt-5.4-mini"]
152+
)
153+
assert got is None

0 commit comments

Comments
 (0)