""" Regression tests for #3368 — /model prefix-shadowing. Reporter: `/model mimo-v2.5` selected `mimo-v2.5-pro` because the typed name is a clean prefix of the longer tier variant, and the only `mimo-v2.5*` entry in the Nous catalog is the `-pro` one (there is no bare `mimo-v2.5`). PR #3394 ("Closes #3368") only hardened the `_bestModelMatch` *fallback* in `commands.js`, but for this input `_findModelInDropdown` (ui.js) returns the WRONG longer variant via its step-3 prefix match BEFORE the fallback ever runs — so the fix never executed and the reporter still saw `mimo-v2.5-pro` after upgrading to v0.51.216. This fix hardens BOTH matchers so a COMPLETE versioned query (ends in a digit) never snaps to a `-suffix` tier variant: * `_findModelInDropdown` step 3 (ui.js): a prefix hit on a longer option is only accepted when the extra text continues the VERSION (".``"), not a variant/tier suffix (".`pro`" from "-pro"). Otherwise returns null. * `_bestModelMatch` (commands.js): same boundary rule on the substring fallback. These tests run the live JS via Node so drift is caught behaviourally. Crucially they also assert the #1188 legitimate-fuzzy behaviour still holds (`gpt-5` → `gpt-5.4-mini`, `claude` → `claude-opus-4.6`), since the fix touches the same step-3 path. """ import json import shutil import subprocess from pathlib import Path import pytest REPO_ROOT = Path(__file__).parent.parent.resolve() UI_JS_PATH = REPO_ROOT / "static" / "ui.js" COMMANDS_JS_PATH = REPO_ROOT / "static" / "commands.js" NODE = shutil.which("node") pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") @pytest.fixture(scope="module") def commands_src() -> str: return COMMANDS_JS_PATH.read_text(encoding="utf-8") # ── driver for _findModelInDropdown (ui.js) ───────────────────────────────── _FIND_DRIVER = r""" const fs = require('fs'); const ui = fs.readFileSync(process.argv[2], 'utf8'); function extractFunc(src, name){ const re = new RegExp('function\\s+' + name + '\\s*\\('); const start = src.search(re); if (start < 0) throw new Error(name + ' not found'); let i = src.indexOf('{', start); let depth = 1; i++; while (depth > 0 && i < src.length){ if(src[i]==='{')depth++; else if(src[i]==='}')depth--; i++; } return src.slice(start, i); } function _getOptionProviderId(opt){ if(!opt) return ''; if(opt.dataset && opt.dataset.provider) return opt.dataset.provider; const group = opt.parentElement; if(group && group.tagName==='OPTGROUP' && group.dataset && group.dataset.provider) return group.dataset.provider; const value = String(opt.value||''); if(value.startsWith('@') && value.includes(':')) return value.slice(1, value.lastIndexOf(':')); return ''; } eval(extractFunc(ui, '_findModelInDropdown')); const args = JSON.parse(process.argv[3]); const sel = { options: args.options.map(v => ({value: v})) }; const got = _findModelInDropdown(args.modelId, sel, args.preferredProvider || undefined); process.stdout.write(JSON.stringify(got)); """ # ── driver for _bestModelMatch (commands.js) ──────────────────────────────── _BEST_DRIVER = r""" const fs = require('fs'); const cmds = fs.readFileSync(process.argv[2], 'utf8'); function extractFunc(src, name){ const re = new RegExp('function\\s+' + name + '\\s*\\('); const start = src.search(re); if (start < 0) throw new Error(name + ' not found'); let i = src.indexOf('{', start); let depth = 1; i++; while (depth > 0 && i < src.length){ if(src[i]==='{')depth++; else if(src[i]==='}')depth--; i++; } return src.slice(start, i); } eval(extractFunc(cmds, '_looksLikeVersionedModel')); eval(extractFunc(cmds, '_bestModelMatch')); eval(extractFunc(cmds, '_nearestModelSuggestion')); const args = JSON.parse(process.argv[3]); // options: [{value, text}] const options = args.options.map(o => ({value: o.value, textContent: o.text || o.value})); const out = { best: _bestModelMatch(options, args.query), suggestion: _nearestModelSuggestion(options, args.query), }; process.stdout.write(JSON.stringify(out)); """ @pytest.fixture(scope="module") def find_driver(tmp_path_factory): p = tmp_path_factory.mktemp("find3368") / "driver.js" p.write_text(_FIND_DRIVER, encoding="utf-8") return str(p) @pytest.fixture(scope="module") def best_driver(tmp_path_factory): p = tmp_path_factory.mktemp("best3368") / "driver.js" p.write_text(_BEST_DRIVER, encoding="utf-8") return str(p) # ── driver for _buildModelCandidates + _bestModelMatch over the FULL catalog ── # This is the layer the reporter's case actually needed: on large provider # catalogs the bare model lives in `extra_models` (not a rendered