112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
"""Tests for ``scripts/gen_cursor_models.py`` base-id derivation.
|
|
|
|
These pin the parsing rules that turn ``cursor-agent models`` compound ids into
|
|
the canonical base-id catalog: effort-suffix stripping, the irregular-claude
|
|
override map, the prefix denylist, display-name cleaning, and ``(default)`` vs
|
|
``(current)`` handling. They feed a fixed fixture through the script's stdin, so
|
|
they run in CI without the ``cursor-agent`` binary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "gen_cursor_models.py"
|
|
|
|
# Representative slice of ``cursor-agent models`` output exercising every rule:
|
|
# bare ids, the (default)/(current) tags, simple + multi-token + two-ordering
|
|
# effort suffixes, the claude 4.6 reversal override, and three denylist reasons.
|
|
_FIXTURE = """Available models
|
|
|
|
auto - Auto
|
|
composer-2.5 - Composer 2.5 (default)
|
|
gpt-5.2 - GPT-5.2 (current)
|
|
gpt-5.2-high - GPT-5.2 High
|
|
gpt-5.5-extra-high - GPT-5.5 1M Extra High
|
|
claude-opus-4-8-thinking-high - Opus 4.8 1M Thinking
|
|
claude-opus-4-8-max - Opus 4.8 1M Max
|
|
claude-4.6-opus-high - Opus 4.6 1M
|
|
claude-4.6-opus-high-thinking - Opus 4.6 1M Thinking
|
|
claude-4.6-sonnet-medium - Sonnet 4.6 1M
|
|
gpt-5.1 - GPT-5.1
|
|
gpt-5.1-codex-max-low - Codex 5.1 Max Low
|
|
gpt-5.4-mini-none - GPT-5.4 Mini None
|
|
gemini-3-flash - Gemini 3 Flash
|
|
claude-4-sonnet - Sonnet 4
|
|
"""
|
|
|
|
|
|
def _run(fixture: str) -> tuple[dict[str, dict], str]:
|
|
"""Run the generator on *fixture* via stdin; return {id: row} and stderr."""
|
|
proc = subprocess.run(
|
|
[sys.executable, str(_SCRIPT)],
|
|
input=fixture,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
rows: dict[str, dict] = {}
|
|
for m in re.finditer(
|
|
r'\{"id": "([^"]+)", "displayName": "([^"]+)"(, "isDefault": True)?\}', proc.stdout
|
|
):
|
|
rows[m.group(1)] = {"displayName": m.group(2), "isDefault": bool(m.group(3))}
|
|
return rows, proc.stderr
|
|
|
|
|
|
def test_derives_canonical_base_ids() -> None:
|
|
"""Effort suffixes strip; the claude 4.6 reversal maps to the canonical id."""
|
|
rows, _ = _run(_FIXTURE)
|
|
# Bare ids kept; compound ids collapse to their base.
|
|
assert "gpt-5.2" in rows
|
|
assert "gpt-5.5" in rows # from gpt-5.5-extra-high (multi-token effort)
|
|
assert "claude-opus-4-8" in rows # both -thinking-high and -max collapse here
|
|
# Irregular claude 4.6 spelling -> canonical injectable base id.
|
|
assert "claude-opus-4-6" in rows
|
|
assert "claude-sonnet-4-6" in rows
|
|
|
|
|
|
def test_never_emits_compound_or_reordered_ids() -> None:
|
|
"""The flattened/effort and reordered-claude spellings never leak through."""
|
|
rows, _ = _run(_FIXTURE)
|
|
for bad in ("gpt-5.2-high", "gpt-5.5-extra-high", "claude-4.6-opus", "claude-4.6-opus-high"):
|
|
assert bad not in rows
|
|
|
|
|
|
def test_denylisted_families_are_dropped() -> None:
|
|
"""Prefix-collision, low-value tiers, and unverified spellings are excluded."""
|
|
rows, stderr = _run(_FIXTURE)
|
|
assert "gpt-5.1" not in rows # prefix-collision
|
|
assert "gpt-5.1-codex" not in rows # from gpt-5.1-codex-max-low, dropped by gpt-5.1 prefix
|
|
assert "gpt-5.4-mini" not in rows
|
|
assert "gemini-3-flash" not in rows
|
|
assert "claude-4-sonnet" not in rows
|
|
# Sonnet 4.6 must NOT be collateral damage of a too-greedy sonnet prefix.
|
|
assert "claude-sonnet-4-6" in rows
|
|
assert "skip" in stderr # the denylist reasons are logged for review
|
|
|
|
|
|
def test_default_tag_carried_current_tag_ignored() -> None:
|
|
"""``(default)`` -> isDefault; ``(current)`` (per-session) is ignored."""
|
|
rows, _ = _run(_FIXTURE)
|
|
assert rows["composer-2.5"]["isDefault"] is True
|
|
assert rows["gpt-5.2"]["isDefault"] is False # tagged (current), not (default)
|
|
assert [mid for mid, r in rows.items() if r["isDefault"]] == ["composer-2.5"]
|
|
|
|
|
|
def test_display_names_strip_effort_and_context_words() -> None:
|
|
"""Trailing effort/context words are stripped to a clean label."""
|
|
rows, _ = _run(_FIXTURE)
|
|
assert rows["gpt-5.5"]["displayName"] == "GPT-5.5" # "GPT-5.5 1M Extra High" cleaned
|
|
assert rows["claude-opus-4-8"]["displayName"] == "Opus 4.8" # "Opus 4.8 1M Thinking" cleaned
|
|
assert rows["claude-sonnet-4-6"]["displayName"] == "Sonnet 4.6" # "Sonnet 4.6 1M" cleaned
|
|
assert rows["auto"]["displayName"] == "Auto"
|
|
|
|
|
|
def test_unmapped_irregular_claude_spelling_warns() -> None:
|
|
"""A new reversed-claude id with no override is flagged, not silently mangled."""
|
|
_, stderr = _run("Available models\n\nclaude-4.9-opus-high - Opus 4.9 1M\n")
|
|
assert "unmapped irregular claude id" in stderr
|