kir / tests /unit /test_model_resolver.py
jwadow
feat(model-resolver): add alias system to resolve Cursor IDE conflict (#59)
365d4b3
Raw
History Blame Contribute Delete
69 kB
# -*- coding: utf-8 -*-
"""
Unit tests for Dynamic Model Resolution System.
Tests 5-layer model resolution architecture:
0. Resolve Aliases - custom name mappings (NEW!)
1. Normalize Name - convert client formats to Kiro format
2. Check Dynamic Cache - models from /ListAvailableModels API
3. Check Hidden Models - manual config for undocumented models
4. Pass-through - unknown models are sent to Kiro
"""
import pytest
from dataclasses import FrozenInstanceError
from kiro.model_resolver import (
normalize_model_name,
get_model_id_for_kiro,
extract_model_family,
ModelResolver,
ModelResolution,
)
from kiro.cache import ModelInfoCache
# =============================================================================
# Fixtures
# =============================================================================
@pytest.fixture
def mock_model_cache():
"""
Creates ModelInfoCache with pre-populated models.
Simulates data from Kiro /ListAvailableModels API.
"""
print("Setup: Creating ModelInfoCache with test models...")
cache = ModelInfoCache()
# Directly populate cache (without async update)
cache._cache = {
"auto": {"modelId": "auto", "modelName": "Auto"},
"claude-sonnet-4.5": {"modelId": "claude-sonnet-4.5", "modelName": "Claude Sonnet 4.5"},
"claude-sonnet-4": {"modelId": "claude-sonnet-4", "modelName": "Claude Sonnet 4"},
"claude-haiku-4.5": {"modelId": "claude-haiku-4.5", "modelName": "Claude Haiku 4.5"},
"claude-opus-4.5": {"modelId": "claude-opus-4.5", "modelName": "Claude Opus 4.5"},
}
return cache
@pytest.fixture
def empty_model_cache():
"""Creates empty ModelInfoCache."""
print("Setup: Creating empty ModelInfoCache...")
return ModelInfoCache()
@pytest.fixture
def hidden_models():
"""Hidden models for tests."""
return {
"claude-3.7-sonnet": "CLAUDE_3_7_SONNET_20250219_V1_0",
}
@pytest.fixture
def model_resolver(mock_model_cache, hidden_models):
"""Ready-to-use ModelResolver for tests."""
print("Setup: Creating ModelResolver with cache and hidden models...")
return ModelResolver(cache=mock_model_cache, hidden_models=hidden_models)
@pytest.fixture
def resolver_without_hidden(mock_model_cache):
"""ModelResolver without hidden models."""
print("Setup: Creating ModelResolver without hidden models...")
return ModelResolver(cache=mock_model_cache, hidden_models={})
# =============================================================================
# TestNormalizeModelName - Tests for model name normalization
# =============================================================================
class TestNormalizeModelName:
"""
Tests for normalize_model_name() function.
Checks conversion of client formats to Kiro format:
- Dashes → dots for minor versions
- Removal of date suffix (20251001)
- Removal of 'latest' suffix
- Legacy format (claude-3-7-sonnet)
"""
# === Standard format with minor version ===
def test_normalizes_haiku_dash_to_dot(self):
"""
What it does: claude-haiku-4-5 → claude-haiku-4.5
Goal: Check dash-to-dot conversion for Haiku.
"""
print("Action: Normalizing 'claude-haiku-4-5'...")
result = normalize_model_name("claude-haiku-4-5")
print(f"Comparing result: Expected 'claude-haiku-4.5', Got '{result}'")
assert result == "claude-haiku-4.5"
def test_normalizes_sonnet_dash_to_dot(self):
"""
What it does: claude-sonnet-4-5 → claude-sonnet-4.5
Goal: Check dash-to-dot conversion for Sonnet.
"""
print("Action: Normalizing 'claude-sonnet-4-5'...")
result = normalize_model_name("claude-sonnet-4-5")
print(f"Comparing result: Expected 'claude-sonnet-4.5', Got '{result}'")
assert result == "claude-sonnet-4.5"
def test_normalizes_opus_dash_to_dot(self):
"""
What it does: claude-opus-4-5 → claude-opus-4.5
Goal: Check dash-to-dot conversion for Opus.
"""
print("Action: Normalizing 'claude-opus-4-5'...")
result = normalize_model_name("claude-opus-4-5")
print(f"Comparing result: Expected 'claude-opus-4.5', Got '{result}'")
assert result == "claude-opus-4.5"
# === Removal of date suffix ===
def test_strips_date_suffix_haiku(self):
"""
What it does: claude-haiku-4-5-20251001 → claude-haiku-4.5
Goal: Check date suffix removal for Haiku (Claude Code format).
"""
print("Action: Normalizing 'claude-haiku-4-5-20251001'...")
result = normalize_model_name("claude-haiku-4-5-20251001")
print(f"Comparing result: Expected 'claude-haiku-4.5', Got '{result}'")
assert result == "claude-haiku-4.5"
def test_strips_date_suffix_sonnet(self):
"""
What it does: claude-sonnet-4-5-20250929 → claude-sonnet-4.5
Goal: Check date suffix removal for Sonnet.
"""
print("Action: Normalizing 'claude-sonnet-4-5-20250929'...")
result = normalize_model_name("claude-sonnet-4-5-20250929")
print(f"Comparing result: Expected 'claude-sonnet-4.5', Got '{result}'")
assert result == "claude-sonnet-4.5"
def test_strips_date_suffix_opus(self):
"""
What it does: claude-opus-4-5-20251101 → claude-opus-4.5
Goal: Check date suffix removal for Opus.
"""
print("Action: Normalizing 'claude-opus-4-5-20251101'...")
result = normalize_model_name("claude-opus-4-5-20251101")
print(f"Comparing result: Expected 'claude-opus-4.5', Got '{result}'")
assert result == "claude-opus-4.5"
# === Removal of 'latest' suffix ===
def test_strips_latest_suffix(self):
"""
What it does: claude-haiku-4-5-latest → claude-haiku-4.5
Goal: Check 'latest' suffix removal.
"""
print("Action: Normalizing 'claude-haiku-4-5-latest'...")
result = normalize_model_name("claude-haiku-4-5-latest")
print(f"Comparing result: Expected 'claude-haiku-4.5', Got '{result}'")
assert result == "claude-haiku-4.5"
# === Standard format without minor version ===
def test_keeps_model_without_minor(self):
"""
What it does: claude-sonnet-4 → claude-sonnet-4
Goal: Check that models without minor version are unchanged.
"""
print("Action: Normalizing 'claude-sonnet-4'...")
result = normalize_model_name("claude-sonnet-4")
print(f"Comparing result: Expected 'claude-sonnet-4', Got '{result}'")
assert result == "claude-sonnet-4"
def test_strips_date_from_model_without_minor(self):
"""
What it does: claude-sonnet-4-20250514 → claude-sonnet-4
Goal: Check date suffix removal for model without minor version.
"""
print("Action: Normalizing 'claude-sonnet-4-20250514'...")
result = normalize_model_name("claude-sonnet-4-20250514")
print(f"Comparing result: Expected 'claude-sonnet-4', Got '{result}'")
assert result == "claude-sonnet-4"
# === Legacy format (claude-X-Y-family) ===
def test_normalizes_legacy_format(self):
"""
What it does: claude-3-7-sonnet → claude-3.7-sonnet
Goal: Check legacy format normalization.
"""
print("Action: Normalizing 'claude-3-7-sonnet'...")
result = normalize_model_name("claude-3-7-sonnet")
print(f"Comparing result: Expected 'claude-3.7-sonnet', Got '{result}'")
assert result == "claude-3.7-sonnet"
def test_normalizes_legacy_format_with_date(self):
"""
What it does: claude-3-7-sonnet-20250219 → claude-3.7-sonnet
Goal: Check legacy format normalization with date suffix.
"""
print("Action: Normalizing 'claude-3-7-sonnet-20250219'...")
result = normalize_model_name("claude-3-7-sonnet-20250219")
print(f"Comparing result: Expected 'claude-3.7-sonnet', Got '{result}'")
assert result == "claude-3.7-sonnet"
def test_normalizes_legacy_haiku(self):
"""
What it does: claude-3-5-haiku → claude-3.5-haiku
Goal: Check legacy format normalization for Haiku.
"""
print("Action: Normalizing 'claude-3-5-haiku'...")
result = normalize_model_name("claude-3-5-haiku")
print(f"Comparing result: Expected 'claude-3.5-haiku', Got '{result}'")
assert result == "claude-3.5-haiku"
def test_normalizes_legacy_opus(self):
"""
What it does: claude-3-0-opus → claude-3.0-opus
Goal: Check legacy format normalization for Opus.
"""
print("Action: Normalizing 'claude-3-0-opus'...")
result = normalize_model_name("claude-3-0-opus")
print(f"Comparing result: Expected 'claude-3.0-opus', Got '{result}'")
assert result == "claude-3.0-opus"
# === Inverted format with suffix (Pattern 5 - Cursor IDE) ===
def test_inverted_format_with_high_suffix(self):
"""
What it does: claude-4.5-opus-high → claude-opus-4.5
Goal: Check inverted format normalization with 'high' suffix (Cursor IDE).
Cursor IDE sends model names in inverted format with priority suffix.
This is Pattern 5 from PR #49.
"""
print("Action: Normalizing 'claude-4.5-opus-high'...")
result = normalize_model_name("claude-4.5-opus-high")
print(f"Comparing result: Expected 'claude-opus-4.5', Got '{result}'")
assert result == "claude-opus-4.5"
def test_inverted_format_with_low_suffix(self):
"""
What it does: claude-4.5-sonnet-low → claude-sonnet-4.5
Goal: Check inverted format normalization with 'low' suffix (Cursor IDE).
"""
print("Action: Normalizing 'claude-4.5-sonnet-low'...")
result = normalize_model_name("claude-4.5-sonnet-low")
print(f"Comparing result: Expected 'claude-sonnet-4.5', Got '{result}'")
assert result == "claude-sonnet-4.5"
def test_inverted_format_with_thinking_suffix(self):
"""
What it does: claude-4.5-opus-high-thinking → claude-opus-4.5
Goal: Check inverted format with compound suffix (high-thinking).
The pattern strips ALL suffixes after the family name.
"""
print("Action: Normalizing 'claude-4.5-opus-high-thinking'...")
result = normalize_model_name("claude-4.5-opus-high-thinking")
print(f"Comparing result: Expected 'claude-opus-4.5', Got '{result}'")
assert result == "claude-opus-4.5"
def test_inverted_format_all_families(self):
"""
What it does: Verifies inverted format works for all families.
Goal: Check haiku, sonnet, opus all work with inverted format.
"""
print("Action: Normalizing inverted format for all families...")
print(" Testing haiku...")
result_haiku = normalize_model_name("claude-4.5-haiku-high")
print(f" Comparing: Expected 'claude-haiku-4.5', Got '{result_haiku}'")
assert result_haiku == "claude-haiku-4.5"
print(" Testing sonnet...")
result_sonnet = normalize_model_name("claude-4.5-sonnet-low")
print(f" Comparing: Expected 'claude-sonnet-4.5', Got '{result_sonnet}'")
assert result_sonnet == "claude-sonnet-4.5"
print(" Testing opus...")
result_opus = normalize_model_name("claude-4.5-opus-high")
print(f" Comparing: Expected 'claude-opus-4.5', Got '{result_opus}'")
assert result_opus == "claude-opus-4.5"
def test_inverted_format_requires_suffix(self):
"""
What it does: Verifies that suffix is required (doesn't match claude-3.7-sonnet).
Goal: CRITICAL - ensure Pattern 5 doesn't break already-normalized formats.
This is the most important test for Pattern 5. The regex MUST require a suffix
to avoid matching already-normalized formats like claude-3.7-sonnet.
"""
print("Action: Normalizing 'claude-3.7-sonnet' (should NOT match Pattern 5)...")
result = normalize_model_name("claude-3.7-sonnet")
print(f"Comparing result: Expected 'claude-3.7-sonnet' (unchanged), Got '{result}'")
assert result == "claude-3.7-sonnet"
print("Action: Normalizing 'claude-4.5-sonnet' (should NOT match Pattern 5)...")
result2 = normalize_model_name("claude-4.5-sonnet")
print(f"Comparing result: Expected 'claude-4.5-sonnet' (unchanged), Got '{result2}'")
assert result2 == "claude-4.5-sonnet"
def test_inverted_format_case_insensitive(self):
"""
What it does: CLAUDE-4.5-OPUS-HIGH → claude-opus-4.5
Goal: Check case insensitivity for inverted format.
"""
print("Action: Normalizing 'CLAUDE-4.5-OPUS-HIGH'...")
result = normalize_model_name("CLAUDE-4.5-OPUS-HIGH")
print(f"Comparing result: Expected 'claude-opus-4.5', Got '{result}'")
assert result == "claude-opus-4.5"
# === Already normalized (passthrough) ===
def test_passthrough_already_normalized_haiku(self):
"""
What it does: claude-haiku-4.5 → claude-haiku-4.5
Goal: Check that already normalized models are unchanged.
"""
print("Action: Normalizing 'claude-haiku-4.5'...")
result = normalize_model_name("claude-haiku-4.5")
print(f"Comparing result: Expected 'claude-haiku-4.5', Got '{result}'")
assert result == "claude-haiku-4.5"
def test_passthrough_already_normalized_sonnet(self):
"""
What it does: claude-sonnet-4.5 → claude-sonnet-4.5
Goal: Check passthrough for Sonnet.
"""
print("Action: Normalizing 'claude-sonnet-4.5'...")
result = normalize_model_name("claude-sonnet-4.5")
print(f"Comparing result: Expected 'claude-sonnet-4.5', Got '{result}'")
assert result == "claude-sonnet-4.5"
def test_passthrough_auto(self):
"""
What it does: auto → auto
Goal: Check passthrough for 'auto'.
"""
print("Action: Normalizing 'auto'...")
result = normalize_model_name("auto")
print(f"Comparing result: Expected 'auto', Got '{result}'")
assert result == "auto"
# === Edge cases ===
def test_handles_empty_string(self):
"""
What it does: "" → ""
Goal: Check empty string handling.
"""
print("Action: Normalizing empty string...")
result = normalize_model_name("")
print(f"Comparing result: Expected '', Got '{result}'")
assert result == ""
def test_handles_unknown_format(self):
"""
What it does: gpt-4 → gpt-4 (passthrough)
Goal: Check passthrough for unknown formats.
"""
print("Action: Normalizing 'gpt-4'...")
result = normalize_model_name("gpt-4")
print(f"Comparing result: Expected 'gpt-4', Got '{result}'")
assert result == "gpt-4"
def test_handles_random_model_name(self):
"""
What it does: some-random-model → some-random-model
Goal: Check passthrough for arbitrary names.
"""
print("Action: Normalizing 'some-random-model'...")
result = normalize_model_name("some-random-model")
print(f"Comparing result: Expected 'some-random-model', Got '{result}'")
assert result == "some-random-model"
# =============================================================================
# TestNormalizeModelNameParametrized - Parametrized tests
# =============================================================================
class TestNormalizeModelNameParametrized:
"""Parametrized tests for complete coverage of scenarios."""
@pytest.mark.parametrize("input_model,expected", [
# Standard format with minor version
("claude-haiku-4-5", "claude-haiku-4.5"),
("claude-haiku-4-5-20251001", "claude-haiku-4.5"),
("claude-haiku-4-5-latest", "claude-haiku-4.5"),
("claude-sonnet-4-5", "claude-sonnet-4.5"),
("claude-sonnet-4-5-20250929", "claude-sonnet-4.5"),
("claude-opus-4-5", "claude-opus-4.5"),
("claude-opus-4-5-20251101", "claude-opus-4.5"),
# Without minor version
("claude-sonnet-4", "claude-sonnet-4"),
("claude-sonnet-4-20250514", "claude-sonnet-4"),
("claude-haiku-4", "claude-haiku-4"),
("claude-opus-4", "claude-opus-4"),
# Legacy format
("claude-3-7-sonnet", "claude-3.7-sonnet"),
("claude-3-7-sonnet-20250219", "claude-3.7-sonnet"),
("claude-3-5-haiku", "claude-3.5-haiku"),
("claude-3-0-opus", "claude-3.0-opus"),
# Already normalized
("claude-haiku-4.5", "claude-haiku-4.5"),
("claude-sonnet-4.5", "claude-sonnet-4.5"),
("claude-opus-4.5", "claude-opus-4.5"),
("claude-3.7-sonnet", "claude-3.7-sonnet"),
("auto", "auto"),
# Passthrough for unknown
("gpt-4", "gpt-4"),
("gpt-4-turbo", "gpt-4-turbo"),
("unknown-model", "unknown-model"),
])
def test_normalize_model_name_all_scenarios(self, input_model, expected):
"""
What it does: Checks all normalization scenarios.
Goal: Complete coverage of scenario table.
"""
print(f"Action: Normalizing '{input_model}'...")
result = normalize_model_name(input_model)
print(f"Comparing result: Expected '{expected}', Got '{result}'")
assert result == expected
# =============================================================================
# TestExtractModelFamily - Tests for model family extraction
# =============================================================================
class TestExtractModelFamily:
"""
Tests for extract_model_family() function.
Checks extraction of model family (haiku, sonnet, opus) from name.
"""
def test_extracts_haiku_from_standard_format(self):
"""
What it does: claude-haiku-4.5 → haiku
Goal: Check Haiku family extraction.
"""
print("Action: Extracting family from 'claude-haiku-4.5'...")
result = extract_model_family("claude-haiku-4.5")
print(f"Comparing result: Expected 'haiku', Got '{result}'")
assert result == "haiku"
def test_extracts_sonnet_from_standard_format(self):
"""
What it does: claude-sonnet-4.5 → sonnet
Goal: Check Sonnet family extraction.
"""
print("Action: Extracting family from 'claude-sonnet-4.5'...")
result = extract_model_family("claude-sonnet-4.5")
print(f"Comparing result: Expected 'sonnet', Got '{result}'")
assert result == "sonnet"
def test_extracts_opus_from_standard_format(self):
"""
What it does: claude-opus-4.5 → opus
Goal: Check Opus family extraction.
"""
print("Action: Extracting family from 'claude-opus-4.5'...")
result = extract_model_family("claude-opus-4.5")
print(f"Comparing result: Expected 'opus', Got '{result}'")
assert result == "opus"
def test_extracts_sonnet_from_legacy_format(self):
"""
What it does: claude-3.7-sonnet → sonnet
Goal: Check family extraction from legacy format.
"""
print("Action: Extracting family from 'claude-3.7-sonnet'...")
result = extract_model_family("claude-3.7-sonnet")
print(f"Comparing result: Expected 'sonnet', Got '{result}'")
assert result == "sonnet"
def test_extracts_haiku_from_unnormalized(self):
"""
What it does: claude-haiku-4-5-20251001 → haiku
Goal: Check family extraction from unnormalized name.
"""
print("Action: Extracting family from 'claude-haiku-4-5-20251001'...")
result = extract_model_family("claude-haiku-4-5-20251001")
print(f"Comparing result: Expected 'haiku', Got '{result}'")
assert result == "haiku"
def test_returns_none_for_non_claude(self):
"""
What it does: gpt-4 → None
Goal: Check None return for non-Claude models.
"""
print("Action: Extracting family from 'gpt-4'...")
result = extract_model_family("gpt-4")
print(f"Comparing result: Expected None, Got {result}")
assert result is None
def test_returns_none_for_auto(self):
"""
What it does: auto → None
Goal: Check None return for 'auto'.
"""
print("Action: Extracting family from 'auto'...")
result = extract_model_family("auto")
print(f"Comparing result: Expected None, Got {result}")
assert result is None
def test_case_insensitive(self):
"""
What it does: CLAUDE-HAIKU-4.5 → haiku
Goal: Check case insensitivity.
"""
print("Action: Extracting family from 'CLAUDE-HAIKU-4.5'...")
result = extract_model_family("CLAUDE-HAIKU-4.5")
print(f"Comparing result: Expected 'haiku', Got '{result}'")
assert result == "haiku"
# =============================================================================
# TestGetModelIdForKiro - Tests for converter helper
# =============================================================================
class TestGetModelIdForKiro:
"""
Tests for get_model_id_for_kiro() function.
Checks getting model ID for sending to Kiro API.
"""
def test_normalizes_without_hidden_models(self):
"""
What it does: Normalizes model without hidden models.
Goal: Check basic normalization.
"""
print("Action: get_model_id_for_kiro('claude-haiku-4-5-20251001', {})...")
result = get_model_id_for_kiro("claude-haiku-4-5-20251001", {})
print(f"Comparing result: Expected 'claude-haiku-4.5', Got '{result}'")
assert result == "claude-haiku-4.5"
def test_returns_internal_id_for_hidden_model(self):
"""
What it does: Returns internal ID for hidden model.
Goal: Check hidden model resolution.
"""
hidden = {"claude-3.7-sonnet": "CLAUDE_3_7_SONNET_20250219_V1_0"}
print("Action: get_model_id_for_kiro('claude-3.7-sonnet', hidden)...")
result = get_model_id_for_kiro("claude-3.7-sonnet", hidden)
print(f"Comparing result: Expected 'CLAUDE_3_7_SONNET_20250219_V1_0', Got '{result}'")
assert result == "CLAUDE_3_7_SONNET_20250219_V1_0"
def test_normalizes_then_checks_hidden(self):
"""
What it does: Normalizes first, then checks hidden.
Goal: Check operation order.
"""
hidden = {"claude-3.7-sonnet": "CLAUDE_3_7_SONNET_20250219_V1_0"}
print("Action: get_model_id_for_kiro('claude-3-7-sonnet', hidden)...")
result = get_model_id_for_kiro("claude-3-7-sonnet", hidden)
print(f"Comparing result: Expected 'CLAUDE_3_7_SONNET_20250219_V1_0', Got '{result}'")
assert result == "CLAUDE_3_7_SONNET_20250219_V1_0"
def test_normalizes_with_date_then_checks_hidden(self):
"""
What it does: Normalizes with date suffix, then checks hidden.
Goal: Check full normalization chain.
"""
hidden = {"claude-3.7-sonnet": "CLAUDE_3_7_SONNET_20250219_V1_0"}
print("Action: get_model_id_for_kiro('claude-3-7-sonnet-20250219', hidden)...")
result = get_model_id_for_kiro("claude-3-7-sonnet-20250219", hidden)
print(f"Comparing result: Expected 'CLAUDE_3_7_SONNET_20250219_V1_0', Got '{result}'")
assert result == "CLAUDE_3_7_SONNET_20250219_V1_0"
def test_passthrough_unknown_model(self):
"""
What it does: Passthrough for unknown models.
Goal: Check that unknown models pass through normalized.
"""
print("Action: get_model_id_for_kiro('claude-unknown-model', {})...")
result = get_model_id_for_kiro("claude-unknown-model", {})
print(f"Comparing result: Expected 'claude-unknown-model', Got '{result}'")
assert result == "claude-unknown-model"
# =============================================================================
# TestModelResolver - Tests for ModelResolver class
# =============================================================================
class TestModelResolverInitialization:
"""Tests for ModelResolver initialization."""
def test_init_with_cache_and_hidden_models(self, mock_model_cache, hidden_models):
"""
What it does: Creates ModelResolver with cache and hidden models.
Goal: Check correct initialization.
"""
print("Action: Creating ModelResolver...")
resolver = ModelResolver(cache=mock_model_cache, hidden_models=hidden_models)
print("Check: Attributes set correctly...")
assert resolver.cache is mock_model_cache
assert resolver.hidden_models == hidden_models
def test_init_with_empty_hidden_models(self, mock_model_cache):
"""
What it does: Creates ModelResolver without hidden models.
Goal: Check work with empty dict.
"""
print("Action: Creating ModelResolver without hidden models...")
resolver = ModelResolver(cache=mock_model_cache, hidden_models={})
print("Check: hidden_models is empty...")
assert resolver.hidden_models == {}
def test_init_with_none_hidden_models(self, mock_model_cache):
"""
What it does: Creates ModelResolver with hidden_models=None.
Goal: Check default value.
"""
print("Action: Creating ModelResolver with hidden_models=None...")
resolver = ModelResolver(cache=mock_model_cache, hidden_models=None)
print("Check: hidden_models initialized as empty dict...")
assert resolver.hidden_models == {}
class TestModelResolverResolve:
"""Tests for resolve() method of ModelResolver class."""
def test_resolve_finds_model_in_cache(self, model_resolver):
"""
What it does: Finds model in cache.
Goal: Check Layer 2 (Dynamic Cache).
"""
print("Action: Resolving 'claude-haiku-4-5'...")
result = model_resolver.resolve("claude-haiku-4-5")
print(f"Check result: {result}")
print(f"Comparing internal_id: Expected 'claude-haiku-4.5', Got '{result.internal_id}'")
assert result.internal_id == "claude-haiku-4.5"
print(f"Comparing source: Expected 'cache', Got '{result.source}'")
assert result.source == "cache"
print(f"Comparing is_verified: Expected True, Got {result.is_verified}")
assert result.is_verified is True
print(f"Comparing normalized: Expected 'claude-haiku-4.5', Got '{result.normalized}'")
assert result.normalized == "claude-haiku-4.5"
print(f"Comparing original_request: Expected 'claude-haiku-4-5', Got '{result.original_request}'")
assert result.original_request == "claude-haiku-4-5"
def test_resolve_finds_model_in_hidden(self, model_resolver):
"""
What it does: Finds model in hidden models.
Goal: Check Layer 3 (Hidden Models).
"""
print("Action: Resolving 'claude-3-7-sonnet'...")
result = model_resolver.resolve("claude-3-7-sonnet")
print(f"Check result: {result}")
print(f"Comparing internal_id: Expected 'CLAUDE_3_7_SONNET_20250219_V1_0', Got '{result.internal_id}'")
assert result.internal_id == "CLAUDE_3_7_SONNET_20250219_V1_0"
print(f"Comparing source: Expected 'hidden', Got '{result.source}'")
assert result.source == "hidden"
print(f"Comparing is_verified: Expected True, Got {result.is_verified}")
assert result.is_verified is True
def test_resolve_passthrough_for_unknown(self, model_resolver):
"""
What it does: Passthrough for unknown model.
Goal: Check Layer 4 (Pass-through).
"""
print("Action: Resolving 'claude-haiku-4-6' (does not exist)...")
result = model_resolver.resolve("claude-haiku-4-6")
print(f"Check result: {result}")
print(f"Comparing internal_id: Expected 'claude-haiku-4.6', Got '{result.internal_id}'")
assert result.internal_id == "claude-haiku-4.6"
print(f"Comparing source: Expected 'passthrough', Got '{result.source}'")
assert result.source == "passthrough"
print(f"Comparing is_verified: Expected False, Got {result.is_verified}")
assert result.is_verified is False
def test_resolve_normalizes_before_lookup(self, model_resolver):
"""
What it does: Normalizes name before cache lookup.
Goal: Check Layer 1 (Normalize Name).
"""
print("Action: Resolving 'claude-haiku-4-5-20251001'...")
result = model_resolver.resolve("claude-haiku-4-5-20251001")
print(f"Comparing normalized: Expected 'claude-haiku-4.5', Got '{result.normalized}'")
assert result.normalized == "claude-haiku-4.5"
print(f"Comparing source: Expected 'cache', Got '{result.source}'")
assert result.source == "cache"
def test_resolve_never_raises(self, model_resolver):
"""
What it does: Never raises exception.
Goal: Check that resolve() always returns ModelResolution.
"""
print("Action: Resolving strange input data...")
# Empty string
result1 = model_resolver.resolve("")
print(f"Empty string: {result1}")
assert isinstance(result1, ModelResolution)
# Special characters
result2 = model_resolver.resolve("!@#$%^&*()")
print(f"Special characters: {result2}")
assert isinstance(result2, ModelResolution)
# Very long name
result3 = model_resolver.resolve("a" * 1000)
print(f"Long name: source={result3.source}")
assert isinstance(result3, ModelResolution)
def test_resolve_auto_model(self, model_resolver):
"""
What it does: Resolves 'auto' model.
Goal: Check that 'auto' is in cache.
"""
print("Action: Resolving 'auto'...")
result = model_resolver.resolve("auto")
print(f"Comparing internal_id: Expected 'auto', Got '{result.internal_id}'")
assert result.internal_id == "auto"
print(f"Comparing source: Expected 'cache', Got '{result.source}'")
assert result.source == "cache"
def test_resolve_with_empty_cache(self, empty_model_cache, hidden_models):
"""
What it does: Resolves model with empty cache.
Goal: Check work with only hidden models.
"""
print("Setup: Creating resolver with empty cache...")
resolver = ModelResolver(cache=empty_model_cache, hidden_models=hidden_models)
print("Action: Resolving 'claude-3.7-sonnet'...")
result = resolver.resolve("claude-3.7-sonnet")
print(f"Comparing source: Expected 'hidden', Got '{result.source}'")
assert result.source == "hidden"
print("Action: Resolving 'claude-haiku-4.5' (not in cache)...")
result2 = resolver.resolve("claude-haiku-4.5")
print(f"Comparing source: Expected 'passthrough', Got '{result2.source}'")
assert result2.source == "passthrough"
class TestModelResolverGetAvailableModels:
"""Tests for get_available_models() method."""
def test_get_available_models_combines_cache_and_hidden(self, model_resolver):
"""
What it does: Returns models from cache and hidden.
Goal: Check combining sources.
"""
print("Action: Getting list of available models...")
models = model_resolver.get_available_models()
print(f"Received models: {models}")
# Check cache models
print("Check: Cache models present...")
assert "claude-haiku-4.5" in models
assert "claude-sonnet-4.5" in models
assert "claude-opus-4.5" in models
assert "auto" in models
# Check hidden models
print("Check: Hidden models present...")
assert "claude-3.7-sonnet" in models
def test_get_available_models_returns_sorted_list(self, model_resolver):
"""
What it does: Returns sorted list.
Goal: Check sorting.
"""
print("Action: Getting list of available models...")
models = model_resolver.get_available_models()
print(f"Received models: {models}")
print(f"Sorted: {sorted(models)}")
assert models == sorted(models)
def test_get_available_models_no_duplicates(self, mock_model_cache):
"""
What it does: Does not return duplicates.
Goal: Check uniqueness.
"""
# Add hidden model that already exists in cache
hidden = {"claude-haiku-4.5": "SOME_INTERNAL_ID"}
resolver = ModelResolver(cache=mock_model_cache, hidden_models=hidden)
print("Action: Getting list with potential duplicate...")
models = resolver.get_available_models()
print(f"Received models: {models}")
# Check uniqueness
assert len(models) == len(set(models))
class TestModelResolverGetModelsByFamily:
"""Tests for get_models_by_family() method."""
def test_get_models_by_family_haiku(self, model_resolver):
"""
What it does: Returns only Haiku models.
Goal: Check filtering by family.
"""
print("Action: Getting Haiku models...")
models = model_resolver.get_models_by_family("haiku")
print(f"Received models: {models}")
assert "claude-haiku-4.5" in models
assert "claude-sonnet-4.5" not in models
assert "claude-opus-4.5" not in models
def test_get_models_by_family_sonnet(self, model_resolver):
"""
What it does: Returns only Sonnet models.
Goal: Check Sonnet filtering.
"""
print("Action: Getting Sonnet models...")
models = model_resolver.get_models_by_family("sonnet")
print(f"Received models: {models}")
assert "claude-sonnet-4.5" in models
assert "claude-sonnet-4" in models
assert "claude-3.7-sonnet" in models # Hidden model
assert "claude-haiku-4.5" not in models
def test_get_models_by_family_opus(self, model_resolver):
"""
What it does: Returns only Opus models.
Goal: Check Opus filtering.
"""
print("Action: Getting Opus models...")
models = model_resolver.get_models_by_family("opus")
print(f"Received models: {models}")
assert "claude-opus-4.5" in models
assert "claude-sonnet-4.5" not in models
def test_get_models_by_family_case_insensitive(self, model_resolver):
"""
What it does: Filtering is case insensitive.
Goal: Check case-insensitivity.
"""
print("Action: Getting HAIKU models (uppercase)...")
models = model_resolver.get_models_by_family("HAIKU")
print(f"Received models: {models}")
assert "claude-haiku-4.5" in models
class TestModelResolverGetSuggestionsForModel:
"""Tests for get_suggestions_for_model() method."""
def test_get_suggestions_returns_same_family(self, model_resolver):
"""
What it does: Returns models of same family.
Goal: Check that suggestions are from same family.
"""
print("Action: Getting suggestions for 'claude-haiku-4-6'...")
suggestions = model_resolver.get_suggestions_for_model("claude-haiku-4-6")
print(f"Received suggestions: {suggestions}")
# All suggestions should be Haiku
for s in suggestions:
print(f"Check: '{s}' contains 'haiku'...")
assert "haiku" in s.lower()
def test_get_suggestions_no_cross_family(self, model_resolver):
"""
What it does: NEVER suggests models from other family.
Goal: Critical check - Opus never becomes Sonnet!
"""
print("Action: Getting suggestions for 'claude-opus-5'...")
suggestions = model_resolver.get_suggestions_for_model("claude-opus-5")
print(f"Received suggestions: {suggestions}")
# Should NOT be Sonnet or Haiku
for s in suggestions:
print(f"Check: '{s}' does NOT contain 'sonnet' or 'haiku'...")
assert "sonnet" not in s.lower()
assert "haiku" not in s.lower()
def test_get_suggestions_returns_all_for_unknown_family(self, model_resolver):
"""
What it does: Returns all models for unknown family.
Goal: Check fallback for non-Claude models.
"""
print("Action: Getting suggestions for 'gpt-4'...")
suggestions = model_resolver.get_suggestions_for_model("gpt-4")
print(f"Received suggestions: {suggestions}")
# Should be all models
all_models = model_resolver.get_available_models()
assert set(suggestions) == set(all_models)
# =============================================================================
# TestModelResolution - Tests for ModelResolution dataclass
# =============================================================================
class TestModelResolution:
"""Tests for ModelResolution dataclass."""
def test_model_resolution_fields(self):
"""
What it does: Checks all ModelResolution fields.
Goal: Ensure correct structure.
"""
print("Action: Creating ModelResolution...")
resolution = ModelResolution(
internal_id="claude-haiku-4.5",
source="cache",
original_request="claude-haiku-4-5",
normalized="claude-haiku-4.5",
is_verified=True
)
print(f"Check fields: {resolution}")
assert resolution.internal_id == "claude-haiku-4.5"
assert resolution.source == "cache"
assert resolution.original_request == "claude-haiku-4-5"
assert resolution.normalized == "claude-haiku-4.5"
assert resolution.is_verified is True
def test_model_resolution_is_frozen(self):
"""
What it does: Checks that ModelResolution is immutable.
Goal: Ensure immutability (frozen=True).
"""
print("Action: Creating ModelResolution...")
resolution = ModelResolution(
internal_id="test",
source="cache",
original_request="test",
normalized="test",
is_verified=True
)
print("Check: Attempt to modify field should raise error...")
with pytest.raises(FrozenInstanceError):
resolution.internal_id = "changed"
def test_model_resolution_equality(self):
"""
What it does: Checks comparison of two ModelResolution objects.
Goal: Ensure correct __eq__ implementation.
"""
print("Action: Creating two identical ModelResolution objects...")
resolution1 = ModelResolution(
internal_id="test",
source="cache",
original_request="test",
normalized="test",
is_verified=True
)
resolution2 = ModelResolution(
internal_id="test",
source="cache",
original_request="test",
normalized="test",
is_verified=True
)
print(f"Comparing: {resolution1} == {resolution2}")
assert resolution1 == resolution2
def test_model_resolution_inequality(self):
"""
What it does: Checks inequality of different ModelResolution objects.
Goal: Ensure correct __eq__ implementation.
"""
print("Action: Creating two different ModelResolution objects...")
resolution1 = ModelResolution(
internal_id="test1",
source="cache",
original_request="test",
normalized="test",
is_verified=True
)
resolution2 = ModelResolution(
internal_id="test2",
source="hidden",
original_request="test",
normalized="test",
is_verified=True
)
print(f"Comparing: {resolution1} != {resolution2}")
assert resolution1 != resolution2
# =============================================================================
# TestModelInfoCacheNewMethods - Tests for new cache methods
# =============================================================================
class TestModelInfoCacheIsValidModel:
"""Tests for is_valid_model() method in ModelInfoCache."""
@pytest.mark.asyncio
async def test_is_valid_model_returns_true_for_cached(self):
"""
What it does: Returns True for model in cache.
Goal: Check basic functionality.
"""
print("Setup: Creating and populating cache...")
cache = ModelInfoCache()
await cache.update([{"modelId": "claude-sonnet-4.5"}])
print("Action: Checking is_valid_model('claude-sonnet-4.5')...")
result = cache.is_valid_model("claude-sonnet-4.5")
print(f"Comparing result: Expected True, Got {result}")
assert result is True
@pytest.mark.asyncio
async def test_is_valid_model_returns_false_for_unknown(self):
"""
What it does: Returns False for unknown model.
Goal: Check negative case.
"""
print("Setup: Creating and populating cache...")
cache = ModelInfoCache()
await cache.update([{"modelId": "claude-sonnet-4.5"}])
print("Action: Checking is_valid_model('unknown-model')...")
result = cache.is_valid_model("unknown-model")
print(f"Comparing result: Expected False, Got {result}")
assert result is False
def test_is_valid_model_on_empty_cache(self):
"""
What it does: Returns False for empty cache.
Goal: Check edge case.
"""
print("Setup: Creating empty cache...")
cache = ModelInfoCache()
print("Action: Checking is_valid_model('any-model')...")
result = cache.is_valid_model("any-model")
print(f"Comparing result: Expected False, Got {result}")
assert result is False
class TestModelInfoCacheAddHiddenModel:
"""Tests for add_hidden_model() method in ModelInfoCache."""
def test_add_hidden_model_adds_to_cache(self):
"""
What it does: Adds hidden model to cache.
Goal: Check basic functionality.
"""
print("Setup: Creating empty cache...")
cache = ModelInfoCache()
print("Action: Adding hidden model...")
cache.add_hidden_model("claude-3.7-sonnet", "CLAUDE_3_7_SONNET_20250219_V1_0")
print("Check: Model added to cache...")
assert cache.is_valid_model("claude-3.7-sonnet") is True
def test_add_hidden_model_stores_internal_id(self):
"""
What it does: Stores internal ID in _internal_id field.
Goal: Check data structure.
"""
print("Setup: Creating empty cache...")
cache = ModelInfoCache()
print("Action: Adding hidden model...")
cache.add_hidden_model("claude-3.7-sonnet", "CLAUDE_3_7_SONNET_20250219_V1_0")
print("Check: _internal_id saved...")
model_info = cache.get("claude-3.7-sonnet")
print(f"model_info: {model_info}")
assert model_info["_internal_id"] == "CLAUDE_3_7_SONNET_20250219_V1_0"
assert model_info["_is_hidden"] is True
def test_add_hidden_model_sets_model_id(self):
"""
What it does: Sets modelId equal to display_name.
Goal: Check data consistency.
"""
print("Setup: Creating empty cache...")
cache = ModelInfoCache()
print("Action: Adding hidden model...")
cache.add_hidden_model("claude-3.7-sonnet", "INTERNAL_ID")
print("Check: modelId set...")
model_info = cache.get("claude-3.7-sonnet")
assert model_info["modelId"] == "claude-3.7-sonnet"
assert model_info["modelName"] == "claude-3.7-sonnet"
@pytest.mark.asyncio
async def test_add_hidden_model_does_not_overwrite_existing(self):
"""
What it does: Does not overwrite existing model.
Goal: Check protection from overwriting.
"""
print("Setup: Creating cache with model...")
cache = ModelInfoCache()
await cache.update([{
"modelId": "claude-3.7-sonnet",
"modelName": "Original Name",
"tokenLimits": {"maxInputTokens": 200000}
}])
print("Action: Attempting to add hidden model with same ID...")
cache.add_hidden_model("claude-3.7-sonnet", "NEW_INTERNAL_ID")
print("Check: Original data preserved...")
model_info = cache.get("claude-3.7-sonnet")
assert model_info["modelName"] == "Original Name"
assert "_internal_id" not in model_info # Should not be added
def test_add_hidden_model_appears_in_get_all_model_ids(self):
"""
What it does: Hidden model appears in list of all models.
Goal: Check integration with get_all_model_ids().
"""
print("Setup: Creating empty cache...")
cache = ModelInfoCache()
print("Action: Adding hidden model...")
cache.add_hidden_model("claude-3.7-sonnet", "INTERNAL_ID")
print("Check: Model in list...")
model_ids = cache.get_all_model_ids()
assert "claude-3.7-sonnet" in model_ids
# =============================================================================
# TestCriticalSafetyPrinciple - Critical security tests
# =============================================================================
class TestCriticalSafetyPrinciple:
"""
Critical security tests: Family Isolation.
IMPORTANT: Resolver MUST NEVER cross model family boundaries!
"""
def test_opus_never_becomes_sonnet(self, model_resolver):
"""
What it does: Opus request NEVER becomes Sonnet.
Goal: Critical check for Family Isolation.
"""
print("Action: Resolving non-existent Opus model...")
result = model_resolver.resolve("claude-opus-5")
print(f"Result: {result}")
# Should be passthrough, NOT fallback to Sonnet
print("Check: Does NOT contain 'sonnet'...")
assert "sonnet" not in result.internal_id.lower()
print("Check: Does NOT contain 'haiku'...")
assert "haiku" not in result.internal_id.lower()
def test_haiku_never_becomes_opus(self, model_resolver):
"""
What it does: Haiku request NEVER becomes Opus.
Goal: Critical check for Family Isolation.
"""
print("Action: Resolving non-existent Haiku model...")
result = model_resolver.resolve("claude-haiku-5")
print(f"Result: {result}")
# Should be passthrough, NOT fallback to Opus
print("Check: Does NOT contain 'opus'...")
assert "opus" not in result.internal_id.lower()
print("Check: Does NOT contain 'sonnet'...")
assert "sonnet" not in result.internal_id.lower()
def test_sonnet_never_becomes_haiku(self, model_resolver):
"""
What it does: Sonnet request NEVER becomes Haiku.
Goal: Critical check for Family Isolation.
"""
print("Action: Resolving non-existent Sonnet model...")
result = model_resolver.resolve("claude-sonnet-5")
print(f"Result: {result}")
# Should be passthrough, NOT fallback to Haiku
print("Check: Does NOT contain 'haiku'...")
assert "haiku" not in result.internal_id.lower()
print("Check: Does NOT contain 'opus'...")
assert "opus" not in result.internal_id.lower()
def test_suggestions_respect_family_boundaries(self, model_resolver):
"""
What it does: Suggestions only from same family.
Goal: Check that get_suggestions_for_model() respects boundaries.
"""
families = ["haiku", "sonnet", "opus"]
for family in families:
print(f"Check family: {family}...")
suggestions = model_resolver.get_suggestions_for_model(f"claude-{family}-99")
for suggestion in suggestions:
print(f" Suggestion: {suggestion}")
# Each suggestion must contain same family
assert family in suggestion.lower(), \
f"Suggestion '{suggestion}' not from family '{family}'!"
# =============================================================================
# TestModelAliasSystem - Tests for Layer 0 (Aliases)
# =============================================================================
class TestModelAliasSystemBasics:
"""
Tests for model alias system (Layer 0).
Aliases allow custom names that map to real model IDs.
Use case: Avoid conflicts with IDE-specific model names (e.g., Cursor's "auto").
"""
def test_alias_resolves_to_target_model(self, mock_model_cache):
"""
What it does: Alias resolves to target model.
Purpose: Basic alias functionality.
"""
print("Setup: Creating resolver with alias...")
aliases = {"auto-kiro": "auto"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'auto-kiro'...")
result = resolver.resolve("auto-kiro")
print(f"Comparing internal_id: Expected 'auto', Got '{result.internal_id}'")
assert result.internal_id == "auto"
print(f"Comparing source: Expected 'cache', Got '{result.source}'")
assert result.source == "cache"
print(f"Comparing original_request: Expected 'auto-kiro', Got '{result.original_request}'")
assert result.original_request == "auto-kiro"
def test_non_aliased_models_work_normally(self, mock_model_cache):
"""
What it does: Non-aliased models work as before.
Purpose: Ensure aliases don't break existing functionality.
"""
print("Setup: Creating resolver with alias...")
aliases = {"auto-kiro": "auto"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'claude-haiku-4.5' (not aliased)...")
result = resolver.resolve("claude-haiku-4.5")
print(f"Comparing internal_id: Expected 'claude-haiku-4.5', Got '{result.internal_id}'")
assert result.internal_id == "claude-haiku-4.5"
print(f"Comparing source: Expected 'cache', Got '{result.source}'")
assert result.source == "cache"
def test_alias_with_normalization_chain(self, mock_model_cache):
"""
What it does: Alias → Normalization → Cache.
Purpose: Ensure alias works with normalization.
"""
print("Setup: Creating resolver with alias to unnormalized name...")
aliases = {"my-haiku": "claude-haiku-4-5"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'my-haiku'...")
result = resolver.resolve("my-haiku")
print(f"Comparing internal_id: Expected 'claude-haiku-4.5', Got '{result.internal_id}'")
assert result.internal_id == "claude-haiku-4.5"
print(f"Comparing normalized: Expected 'claude-haiku-4.5', Got '{result.normalized}'")
assert result.normalized == "claude-haiku-4.5"
def test_multiple_aliases(self, mock_model_cache):
"""
What it does: Multiple aliases work correctly.
Purpose: Ensure multiple aliases don't interfere.
"""
print("Setup: Creating resolver with multiple aliases...")
aliases = {
"auto-kiro": "auto",
"my-opus": "claude-opus-4.5",
"fast": "claude-haiku-4.5"
}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving all aliases...")
result1 = resolver.resolve("auto-kiro")
result2 = resolver.resolve("my-opus")
result3 = resolver.resolve("fast")
print(f"Comparing: auto-kiro → {result1.internal_id}")
assert result1.internal_id == "auto"
print(f"Comparing: my-opus → {result2.internal_id}")
assert result2.internal_id == "claude-opus-4.5"
print(f"Comparing: fast → {result3.internal_id}")
assert result3.internal_id == "claude-haiku-4.5"
class TestModelAliasSystemEdgeCases:
"""Edge cases and boundary conditions for alias system."""
def test_alias_to_non_existent_model(self, mock_model_cache):
"""
What it does: Alias pointing to non-existent model.
Purpose: Ensure passthrough works for aliased non-existent models.
"""
print("Setup: Creating resolver with alias to non-existent model...")
aliases = {"future-model": "claude-haiku-5.0"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'future-model'...")
result = resolver.resolve("future-model")
print(f"Comparing internal_id: Expected 'claude-haiku-5.0', Got '{result.internal_id}'")
assert result.internal_id == "claude-haiku-5.0"
print(f"Comparing source: Expected 'passthrough', Got '{result.source}'")
assert result.source == "passthrough"
print(f"Comparing is_verified: Expected False, Got {result.is_verified}")
assert result.is_verified is False
def test_alias_to_hidden_model(self, mock_model_cache):
"""
What it does: Alias pointing to hidden model.
Purpose: Ensure alias works with hidden models.
"""
print("Setup: Creating resolver with alias to hidden model...")
hidden = {"claude-3.7-sonnet": "CLAUDE_3_7_SONNET_20250219_V1_0"}
aliases = {"legacy-sonnet": "claude-3.7-sonnet"}
resolver = ModelResolver(cache=mock_model_cache, hidden_models=hidden, aliases=aliases)
print("Action: Resolving 'legacy-sonnet'...")
result = resolver.resolve("legacy-sonnet")
print(f"Comparing internal_id: Expected 'CLAUDE_3_7_SONNET_20250219_V1_0', Got '{result.internal_id}'")
assert result.internal_id == "CLAUDE_3_7_SONNET_20250219_V1_0"
print(f"Comparing source: Expected 'hidden', Got '{result.source}'")
assert result.source == "hidden"
def test_empty_aliases_dict(self, mock_model_cache):
"""
What it does: Empty aliases dict works correctly.
Purpose: Ensure empty dict doesn't break anything.
"""
print("Setup: Creating resolver with empty aliases...")
resolver = ModelResolver(cache=mock_model_cache, aliases={})
print("Action: Resolving 'auto'...")
result = resolver.resolve("auto")
print(f"Comparing internal_id: Expected 'auto', Got '{result.internal_id}'")
assert result.internal_id == "auto"
def test_none_aliases(self, mock_model_cache):
"""
What it does: None aliases parameter works correctly.
Purpose: Ensure None is handled as empty dict.
"""
print("Setup: Creating resolver with aliases=None...")
resolver = ModelResolver(cache=mock_model_cache, aliases=None)
print("Check: aliases initialized as empty dict...")
assert resolver.aliases == {}
def test_alias_with_same_name_as_real_model(self, mock_model_cache):
"""
What it does: Alias with same name as real model.
Purpose: CRITICAL - alias should take precedence!
"""
print("Setup: Creating resolver with alias shadowing real model...")
# Alias "auto" to point to "claude-sonnet-4.5"
aliases = {"auto": "claude-sonnet-4.5"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'auto'...")
result = resolver.resolve("auto")
print(f"Comparing internal_id: Expected 'claude-sonnet-4.5', Got '{result.internal_id}'")
assert result.internal_id == "claude-sonnet-4.5"
print("CRITICAL: Alias takes precedence over cache!")
def test_alias_case_sensitivity(self, mock_model_cache):
"""
What it does: Aliases are case-sensitive.
Purpose: Ensure case sensitivity is preserved.
"""
print("Setup: Creating resolver with lowercase alias...")
aliases = {"auto-kiro": "auto"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'AUTO-KIRO' (uppercase)...")
result = resolver.resolve("AUTO-KIRO")
print(f"Comparing internal_id: Expected 'AUTO-KIRO' (passthrough preserves case), Got '{result.internal_id}'")
# Should NOT match alias, should passthrough as-is (preserving original case)
assert result.internal_id == "AUTO-KIRO"
assert result.source == "passthrough"
class TestHiddenFromListFunctionality:
"""Tests for HIDDEN_FROM_LIST feature."""
def test_hidden_model_not_in_available_list(self, mock_model_cache):
"""
What it does: Hidden model doesn't appear in get_available_models().
Purpose: Basic HIDDEN_FROM_LIST functionality.
"""
print("Setup: Creating resolver with hidden_from_list...")
hidden_from_list = ["auto"]
resolver = ModelResolver(cache=mock_model_cache, hidden_from_list=hidden_from_list)
print("Action: Getting available models...")
models = resolver.get_available_models()
print(f"Received models: {models}")
print("Check: 'auto' NOT in list...")
assert "auto" not in models
print("Check: Other models still present...")
assert "claude-haiku-4.5" in models
assert "claude-sonnet-4.5" in models
def test_hidden_model_still_works_when_requested(self, mock_model_cache):
"""
What it does: Hidden model still works when requested directly.
Purpose: CRITICAL - hiding from list doesn't disable the model!
"""
print("Setup: Creating resolver with hidden_from_list...")
hidden_from_list = ["auto"]
resolver = ModelResolver(cache=mock_model_cache, hidden_from_list=hidden_from_list)
print("Action: Resolving 'auto' directly...")
result = resolver.resolve("auto")
print(f"Comparing internal_id: Expected 'auto', Got '{result.internal_id}'")
assert result.internal_id == "auto"
print(f"Comparing source: Expected 'cache', Got '{result.source}'")
assert result.source == "cache"
print("CRITICAL: Hidden model still works!")
def test_alias_appears_when_original_hidden(self, mock_model_cache):
"""
What it does: Alias appears in list when original is hidden.
Purpose: This is the main use case - show alias, hide original.
"""
print("Setup: Creating resolver with alias and hidden original...")
aliases = {"auto-kiro": "auto"}
hidden_from_list = ["auto"]
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases, hidden_from_list=hidden_from_list)
print("Action: Getting available models...")
models = resolver.get_available_models()
print(f"Received models: {models}")
print("Check: 'auto-kiro' (alias) IS in list...")
assert "auto-kiro" in models
print("Check: 'auto' (original) NOT in list...")
assert "auto" not in models
print("SUCCESS: Alias visible, original hidden!")
def test_multiple_hidden_models(self, mock_model_cache):
"""
What it does: Multiple models can be hidden.
Purpose: Ensure list works with multiple entries.
"""
print("Setup: Creating resolver with multiple hidden models...")
hidden_from_list = ["auto", "claude-sonnet-4"]
resolver = ModelResolver(cache=mock_model_cache, hidden_from_list=hidden_from_list)
print("Action: Getting available models...")
models = resolver.get_available_models()
print(f"Received models: {models}")
print("Check: Both hidden models NOT in list...")
assert "auto" not in models
assert "claude-sonnet-4" not in models
print("Check: Other models still present...")
assert "claude-haiku-4.5" in models
def test_empty_hidden_from_list(self, mock_model_cache):
"""
What it does: Empty hidden_from_list works correctly.
Purpose: Ensure empty list doesn't break anything.
"""
print("Setup: Creating resolver with empty hidden_from_list...")
resolver = ModelResolver(cache=mock_model_cache, hidden_from_list=[])
print("Action: Getting available models...")
models = resolver.get_available_models()
print(f"Received models: {models}")
print("Check: All models present...")
assert "auto" in models
assert "claude-haiku-4.5" in models
def test_none_hidden_from_list(self, mock_model_cache):
"""
What it does: None hidden_from_list parameter works correctly.
Purpose: Ensure None is handled as empty set.
"""
print("Setup: Creating resolver with hidden_from_list=None...")
resolver = ModelResolver(cache=mock_model_cache, hidden_from_list=None)
print("Check: hidden_from_list initialized as empty set...")
assert resolver.hidden_from_list == set()
class TestAliasSystemIntegration:
"""Integration tests for alias system with existing layers."""
def test_cursor_auto_conflict_solution(self, mock_model_cache):
"""
What it does: Solves Cursor IDE "auto" conflict.
Purpose: This is the MAIN use case from issue #59!
"""
print("Setup: Simulating Cursor conflict solution...")
aliases = {"auto-kiro": "auto"}
hidden_from_list = ["auto"]
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases, hidden_from_list=hidden_from_list)
print("Action: Getting available models (what Cursor sees)...")
models = resolver.get_available_models()
print(f"Models visible to Cursor: {models}")
print("Check: 'auto' NOT visible (no conflict)...")
assert "auto" not in models
print("Check: 'auto-kiro' IS visible...")
assert "auto-kiro" in models
print("Action: User requests 'auto-kiro' in Cursor...")
result = resolver.resolve("auto-kiro")
print(f"Comparing: 'auto-kiro' resolves to '{result.internal_id}'")
assert result.internal_id == "auto"
print("Action: Old code with 'auto' still works...")
result2 = resolver.resolve("auto")
print(f"Comparing: 'auto' still resolves to '{result2.internal_id}'")
assert result2.internal_id == "auto"
print("SUCCESS: Cursor conflict solved! ✅")
def test_no_duplicates_in_available_models(self, mock_model_cache):
"""
What it does: No duplicates when alias points to existing model.
Purpose: Ensure set logic works correctly.
"""
print("Setup: Creating resolver with alias to existing model...")
aliases = {"my-haiku": "claude-haiku-4.5"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Getting available models...")
models = resolver.get_available_models()
print(f"Received models: {models}")
print("Check: No duplicates...")
assert len(models) == len(set(models))
print("Check: Both alias and original present...")
assert "my-haiku" in models
assert "claude-haiku-4.5" in models
def test_alias_with_hidden_models_and_hidden_from_list(self, mock_model_cache):
"""
What it does: Complex scenario with all features.
Purpose: Ensure all features work together.
"""
print("Setup: Creating resolver with all features...")
hidden_models = {"claude-3.7-sonnet": "CLAUDE_3_7_SONNET_20250219_V1_0"}
aliases = {"auto-kiro": "auto", "legacy": "claude-3.7-sonnet"}
hidden_from_list = ["auto"]
resolver = ModelResolver(
cache=mock_model_cache,
hidden_models=hidden_models,
aliases=aliases,
hidden_from_list=hidden_from_list
)
print("Action: Getting available models...")
models = resolver.get_available_models()
print(f"Received models: {models}")
print("Check: Alias 'auto-kiro' present...")
assert "auto-kiro" in models
print("Check: Original 'auto' hidden...")
assert "auto" not in models
print("Check: Hidden model 'claude-3.7-sonnet' present...")
assert "claude-3.7-sonnet" in models
print("Check: Alias to hidden model 'legacy' present...")
assert "legacy" in models
print("Check: Cache models present...")
assert "claude-haiku-4.5" in models
class TestAliasSystemSecurity:
"""Security tests - ensure aliases don't break safety principles."""
def test_alias_cannot_bypass_family_isolation(self, mock_model_cache):
"""
What it does: Alias doesn't break family isolation.
Purpose: CRITICAL - ensure aliases don't create security holes!
"""
print("Setup: Creating resolver with alias...")
aliases = {"my-model": "claude-opus-5"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Resolving 'my-model' (points to non-existent Opus)...")
result = resolver.resolve("my-model")
print(f"Result: {result}")
print("Check: Does NOT fallback to Sonnet or Haiku...")
assert "sonnet" not in result.internal_id.lower()
assert "haiku" not in result.internal_id.lower()
print("Check: Passthrough preserves family...")
assert "opus" in result.internal_id.lower()
def test_alias_suggestions_respect_target_family(self, mock_model_cache):
"""
What it does: Suggestions for aliased model respect target family.
Purpose: Ensure get_suggestions_for_model() works with aliases.
"""
print("Setup: Creating resolver with alias...")
aliases = {"fast": "claude-haiku-4.5"}
resolver = ModelResolver(cache=mock_model_cache, aliases=aliases)
print("Action: Getting suggestions for 'fast'...")
# Note: get_suggestions_for_model() extracts family from the NAME
# So it won't find "haiku" in "fast", will return all models
suggestions = resolver.get_suggestions_for_model("fast")
print(f"Received suggestions: {suggestions}")
# This is expected behavior - alias name doesn't contain family
assert len(suggestions) > 0