User decision (10 Oct 2026): combos keep their own name; single models read as short provider/model with the version kept (cc/opus-5-5, agy/gemini-3.8-flash-high, bekci/kurnaz). No-think copies show their real provider with a "(no-think)" suffix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
10 KiB
Python
142 lines
10 KiB
Python
"""The shipped IDE must use the same public OmniRoute catalog as every member."""
|
||
import io
|
||
import json
|
||
import unittest
|
||
from unittest.mock import patch
|
||
import ide_catalog as c
|
||
|
||
|
||
def row(mid, name, source="omniroute", capabilities=("chat",)):
|
||
return {"id": mid, "turkservis": {"display_name": name, "source": source,
|
||
"capabilities": list(capabilities)}}
|
||
|
||
|
||
class MemberCatalog(unittest.TestCase):
|
||
def test_metadata_classifies_only_public_models(self):
|
||
public = [row("plain", "Plain"), row("team", "Team")]
|
||
metadata = [{"id": "plain", "kind": "model"}, {"id": "team", "kind": "combo"},
|
||
{"id": "internal", "kind": "combo"}]
|
||
replies = [io.BytesIO(json.dumps({"data": public}).encode()),
|
||
io.BytesIO(json.dumps({"models": metadata}).encode())]
|
||
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
|
||
patch.object(c, "anahtar_oku", return_value="test-key"), \
|
||
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
|
||
patch.object(c.urllib.request, "urlopen", side_effect=replies):
|
||
status, _, _, selected = c.yukle()
|
||
self.assertEqual(status, "tamam")
|
||
self.assertEqual({m["id"]: m["kind"] for m in selected}, {"plain": "model", "team": "combo"})
|
||
|
||
def test_empty_catalog_is_a_refresh_failure(self):
|
||
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
|
||
patch.object(c, "anahtar_oku", return_value="test-key"), \
|
||
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
|
||
patch.object(c.urllib.request, "urlopen", return_value=io.BytesIO(b'{"data": []}')):
|
||
with self.assertRaises(ValueError):
|
||
c.yukle()
|
||
|
||
def test_public_omniroute_models_do_not_need_private_local_configuration(self):
|
||
rows = [row("9router-nano-x", "9R · nano/x"), row("agnes-flat", "Agnes Flat"),
|
||
row("Ajan-Arac", "Ajan-Arac"), row("image", "image", capabilities=("image",))]
|
||
self.assertEqual({m["id"] for m in c.sec(rows, [])},
|
||
{"9router-nano-x", "agnes-flat", "Ajan-Arac"})
|
||
|
||
def test_direct_route_is_never_added_beside_omniroute(self):
|
||
rows = [row("9router-nano-x-direct", "9R · nano/x", "9router"),
|
||
row("9router-nano-x", "9R · nano/x")]
|
||
self.assertEqual([m["id"] for m in c.sec(rows, ["nano/x"], "hepsi")], ["9router-nano-x"])
|
||
|
||
def test_model_order_uses_combo_metadata_and_preserves_every_model(self):
|
||
models = [{"slug": "gpt-z", "display_name": "GPT Z", "priority": 1},
|
||
{"slug": "ide/bekci-kurnaz", "display_name": "kurnaz", "priority": 2},
|
||
{"slug": "ide/Z-Team", "display_name": "Z-Team", "priority": 5},
|
||
{"slug": "ide/9router-cc-opus", "display_name": "9R · cc/opus", "priority": 3},
|
||
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 4}]
|
||
extras = [{"slug": m["slug"], "kind": "combo" if m["slug"].endswith("Team") else "model"}
|
||
for m in models if m["slug"].startswith("ide/")]
|
||
c.sirala_codex(models, extras)
|
||
self.assertEqual([m["slug"] for m in models],
|
||
["gpt-z", "ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus"])
|
||
self.assertEqual([m["priority"] for m in models], [1, 2, 3, 4, 5])
|
||
|
||
def test_subscription_models_keep_their_own_order_priority_and_name(self):
|
||
models = [{"slug": "ide/GPT-6-Astra", "display_name": "GPT-6-Astra", "priority": 3},
|
||
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2},
|
||
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 0},
|
||
{"slug": "gpt-6.1-sol", "display_name": "GPT-6.1-Sol", "priority": 1},
|
||
{"slug": "codex-auto-review", "display_name": "Codex Auto Review", "priority": 43}]
|
||
models.append({"slug": "ide/GPT-6-1-Sol", "display_name": "GPT-6-1-Sol", "priority": 2})
|
||
extras = [{"slug": "ide/GPT-6-Astra", "kind": "combo"}, {"slug": "ide/A-Team", "kind": "combo"},
|
||
{"slug": "ide/GPT-6-1-Sol", "kind": "combo"}]
|
||
originals = [dict(m) for m in models if not m["slug"].startswith("ide/")]
|
||
c.sirala_codex(models, extras)
|
||
self.assertEqual([m for m in models if not m["slug"].startswith("ide/")], originals)
|
||
self.assertEqual([m["slug"] for m in models],
|
||
["gpt-6-astra", "gpt-6.1-sol", "codex-auto-review", "ide/A-Team", "ide/GPT-6-1-Sol",
|
||
"ide/GPT-6-Astra"])
|
||
self.assertEqual([m["priority"] for m in models[3:]], [44, 45, 46])
|
||
self.assertEqual([m["display_name"] for m in models[4:]], ["GPT-6-1-Sol (AITURK)", "GPT-6-Astra (AITURK)"])
|
||
|
||
def test_description_line_tells_what_the_entry_is_and_fits_a_phone_row(self):
|
||
yedek = {"kind": "combo", "model": "Yedek", "strategy": "priority", "members": [
|
||
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
|
||
{"kind": "model", "model": "nano/abliteration-ai/abliterated-model-large-v2"}]}
|
||
takim = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||
{"kind": "combo", "model": "Takim-Hizli", "strategy": "fusion", "members": [
|
||
{"kind": "model", "model": "ag/gemini-3.8-flash-high"}, {"kind": "model", "model": "cc/claude-sonnet-5-5"}]},
|
||
yedek]}}
|
||
self.assertEqual(c.aciklama("Takim-Hizli", "Takim-Hizli", "combo", takim), "Takım: gemini+sonnet")
|
||
sirali = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||
{"kind": "model", "model": "cx/gpt-6-astra"}, {"kind": "model", "model": "cc/claude-opus-5-5"},
|
||
{"kind": "model", "model": "cc/claude-sonnet-5-5"}, {"kind": "model", "model": "ag/gemini-3.8-flash-high"}]}}
|
||
self.assertEqual(c.aciklama("Ajan-Arac", "Ajan-Arac", "combo", sirali), "Sıralı: astra>opus>sonnet +1")
|
||
# An unresolved first member stays visible instead of being hidden behind the fallback.
|
||
eksik = {"topology": {"kind": "combo", "strategy": "priority", "members": [{"kind": "combo", "model": None}, yedek]}}
|
||
self.assertEqual(c.aciklama("GPT-6-Sol", "GPT-6-Sol", "combo", eksik), "Sıralı: ?>glm>abliterated")
|
||
donen = {"topology": {"kind": "combo", "strategy": "round-robin", "members": [yedek, yedek, yedek]}}
|
||
self.assertEqual(c.aciklama("Takim-Sira", "Takim-Sira", "combo", donen), "Dönüşümlü: 3 üye")
|
||
self.assertEqual(c.aciklama("X", "X", "combo", None), "Takım")
|
||
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"), "Tek model · Claude")
|
||
self.assertEqual(c.aciklama("9router-nano-qwen-qwen3.8-27b-uncensored-thinking",
|
||
"9R · nano/qwen3.8-27b-uncensored:thinking", "model"),
|
||
"NanoGPT · sansürsüz · düşünen")
|
||
self.assertEqual(c.aciklama("bekci-kurnaz", "kurnaz", "model"), "Tek model · Bekçi")
|
||
# 9Router aliases: cc = Claude Code, cx = OpenAI Codex, ag = Antigravity.
|
||
self.assertEqual(c.aciklama("9router-cx-gpt-6-astra", "9R · cx/gpt-6-astra", "model"), "Tek model · Codex")
|
||
self.assertEqual(c.aciklama("9router-ag-gemini-3.8-flash-high", "9R · ag/gemini-3.8-flash-high", "model"), "Tek model · Antigravity")
|
||
uzun = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||
{"kind": "model", "model": f"x/model{chr(97 + i) * 9}"} for i in range(8)]}}
|
||
self.assertLessEqual(len(c.aciklama("U", "U", "combo", uzun)), c.ACIKLAMA_SINIRI)
|
||
for ad in ("Takim-Hizli", "9R · cc/claude-opus-5-5"):
|
||
self.assertNotIn(ad, c.aciklama(ad, ad, "combo" if "Takim" in ad else "model", takim))
|
||
|
||
def test_picker_names_are_combo_name_or_short_provider_slash_model(self):
|
||
# User decision, 10 Oct 2026: no "9R" mark; combos by name; single models as provider/model.
|
||
rows = [("Takim-Hizli", "Takim-Hizli", "combo"),
|
||
("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"),
|
||
("9router-cc-claude-opus-4-8", "9R · cc/claude-opus-4-8", "model"),
|
||
("9router-cx-gpt-6-astra", "9R · cx/gpt-6-astra", "model"),
|
||
("9router-ag-gemini-3.8-flash-high", "9R · ag/gemini-3.8-flash-high", "model"),
|
||
("9router-nano-z-ai-glm-5.3-flash-uncensored", "9R · nano/glm-5.3-flash-uncensored", "model"),
|
||
("agnes-agnes-2.0-flash", "Agnes 2.0 Flash", "model"),
|
||
("agnes-agnes-2.0-flash-high", "agnes-2.0-flash-high", "model"),
|
||
("bekci-bekci", "bekci", "model"), ("bekci-kurnaz", "kurnaz", "model"),
|
||
("hf-deepseek-ai-DeepSeek-V3", "hf/DeepSeek V3", "model"),
|
||
("no-think-9router-cc-claude-opus-5-5", "no-think/claude-opus-5-5", "model"),
|
||
("no-think-9router-ag-claude-opus-4-6-thinking", "no-think/claude-opus-4-6-thinking", "model")]
|
||
adlar = c.adlandir([(k, a) for k, a, _ in rows], turler={k: t for k, _, t in rows})
|
||
self.assertEqual([adlar[k] for k, _, _ in rows],
|
||
["Takim-Hizli", "cc/opus-5-5", "cc/opus-4-8", "cx/gpt-6-astra", "agy/gemini-3.8-flash-high",
|
||
"nano/glm-5.3-flash-uncensored", "agnes/agnes-2.0-flash", "agnes/agnes-2.0-flash-high",
|
||
"bekci/bekci", "bekci/kurnaz", "hf/DeepSeek-V3", "cc/opus-5-5 (no-think)",
|
||
"agy/opus-4-6-thinking (no-think)"])
|
||
self.assertFalse([a for a in adlar.values() if "9R" in a])
|
||
# Added models sort after the combos by provider then model.
|
||
models = [{"slug": "ide/" + k, "display_name": adlar[k], "priority": 0} for k, _, _ in rows]
|
||
c.sirala_codex(models, [{"slug": "ide/" + k, "kind": t} for k, _, t in rows])
|
||
gorunen = [m["display_name"] for m in models]
|
||
self.assertEqual(gorunen[0], "Takim-Hizli")
|
||
self.assertEqual(gorunen[1:], sorted(gorunen[1:], key=lambda a: tuple(p.casefold() for p in a.partition("/")[::2])))
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|