1367 lines
46 KiB
Python
1367 lines
46 KiB
Python
"""Tests for onboarding mutations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from opensquilla.gateway.config import GatewayConfig
|
|
from opensquilla.gateway.llm_runtime import resolve_llm_runtime_config
|
|
from opensquilla.onboarding.mutations import (
|
|
MutationResult,
|
|
list_channel_entries,
|
|
remove_channel,
|
|
set_channel_enabled,
|
|
upsert_channel,
|
|
upsert_image_generation_provider,
|
|
upsert_llm_ensemble,
|
|
upsert_llm_provider,
|
|
upsert_memory_embedding,
|
|
upsert_router,
|
|
upsert_search_provider,
|
|
validate_channel_entry,
|
|
)
|
|
from opensquilla.onboarding.redaction import REDACTED_PLACEHOLDER
|
|
|
|
|
|
def test_upsert_provider_persists_fields():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="deepseek/deepseek-v4-flash",
|
|
api_key="sk-test",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
assert isinstance(res, MutationResult)
|
|
assert res.config.llm.provider == "openrouter"
|
|
assert res.config.llm.model == "deepseek/deepseek-v4-flash"
|
|
assert res.config.llm.api_key == "sk-test"
|
|
assert res.changed is True
|
|
|
|
|
|
def test_upsert_provider_strips_trailing_paste_punctuation_from_api_key():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="deepseek/deepseek-v4-flash",
|
|
api_key="sk-test、",
|
|
)
|
|
|
|
assert res.config.llm.api_key == "sk-test"
|
|
|
|
|
|
def test_provider_payload_redacts_api_key():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(cfg, provider_id="openrouter", model="x", api_key="sk-test")
|
|
assert res.public_payload["api_key"] == REDACTED_PLACEHOLDER
|
|
|
|
|
|
def test_upsert_memory_embedding_local_requires_restart():
|
|
cfg = GatewayConfig()
|
|
res = upsert_memory_embedding(
|
|
cfg,
|
|
provider="local",
|
|
model="BAAI/bge-small-zh-v1.5",
|
|
onnx_dir="models/bge",
|
|
)
|
|
assert res.restart_required is True
|
|
assert res.config.memory.embedding.requested_provider == "local"
|
|
assert res.config.memory.embedding.local.onnx_dir == "models/bge"
|
|
|
|
|
|
def test_upsert_memory_embedding_remote_redacts_key():
|
|
cfg = GatewayConfig()
|
|
res = upsert_memory_embedding(
|
|
cfg,
|
|
provider="openai",
|
|
model="text-embedding-3-small",
|
|
api_key="mem-secret",
|
|
base_url="https://api.openai.com/v1",
|
|
)
|
|
assert res.config.memory.embedding.remote.api_key == "mem-secret"
|
|
assert res.public_payload["remote"]["api_key"] == REDACTED_PLACEHOLDER
|
|
|
|
|
|
def test_upsert_memory_embedding_remote_can_use_env_key_reference():
|
|
cfg = GatewayConfig()
|
|
res = upsert_memory_embedding(
|
|
cfg,
|
|
provider="openai",
|
|
model="text-embedding-3-small",
|
|
api_key_env="OPENAI_EMBEDDINGS_API_KEY",
|
|
base_url="https://api.openai.com/v1",
|
|
)
|
|
|
|
remote = res.config.memory.embedding.remote
|
|
assert remote.api_key in {"", None}
|
|
assert remote.api_key_env == "OPENAI_EMBEDDINGS_API_KEY"
|
|
assert res.public_payload["remote"]["api_key_env"] == "OPENAI_EMBEDDINGS_API_KEY"
|
|
|
|
|
|
def test_upsert_memory_embedding_auto_can_store_remote_fallback():
|
|
cfg = GatewayConfig()
|
|
res = upsert_memory_embedding(
|
|
cfg,
|
|
provider="auto",
|
|
model="text-embedding-3-small",
|
|
api_key="mem-secret",
|
|
base_url="https://embeddings.example/v1",
|
|
)
|
|
assert res.config.memory.embedding.requested_provider == "auto"
|
|
assert res.config.memory.embedding.remote.api_key == "mem-secret"
|
|
assert res.config.memory.embedding.remote.base_url == "https://embeddings.example/v1"
|
|
assert res.config.memory.embedding.remote.model == "text-embedding-3-small"
|
|
assert res.public_payload["remote"]["api_key"] == REDACTED_PLACEHOLDER
|
|
|
|
|
|
def test_upsert_memory_embedding_explicit_remote_reuses_auto_remote_key():
|
|
cfg = GatewayConfig(
|
|
memory={
|
|
"embedding": {
|
|
"provider": "auto",
|
|
"remote": {
|
|
"api_key": "mem-secret",
|
|
"base_url": "https://embeddings.example/v1",
|
|
"model": "embed-model",
|
|
},
|
|
}
|
|
}
|
|
)
|
|
res = upsert_memory_embedding(cfg, provider="openai", api_key="")
|
|
assert res.config.memory.embedding.requested_provider == "openai"
|
|
assert res.config.memory.embedding.remote.api_key == "mem-secret"
|
|
assert res.config.memory.embedding.remote.base_url == "https://embeddings.example/v1"
|
|
assert res.config.memory.embedding.remote.model == "embed-model"
|
|
|
|
|
|
def test_upsert_memory_embedding_auto_without_changes_does_not_require_restart():
|
|
cfg = GatewayConfig()
|
|
res = upsert_memory_embedding(cfg, provider="auto")
|
|
assert res.changed is False
|
|
assert res.restart_required is False
|
|
|
|
|
|
def test_unsupported_provider_rejected():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="not runtime-supported"):
|
|
upsert_llm_provider(cfg, provider_id="github_copilot", model="x")
|
|
|
|
|
|
def test_experimental_provider_configurable_with_required_fields():
|
|
# azure is experimental (registry-runnable, unverified) — configurable,
|
|
# but its base_url requirement still validates.
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="base_url"):
|
|
upsert_llm_provider(cfg, provider_id="azure", model="x", api_key="k")
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="azure",
|
|
model="x",
|
|
api_key="k",
|
|
base_url="https://example.openai.azure.com/v1",
|
|
)
|
|
assert res.config.llm.provider == "azure"
|
|
|
|
|
|
def test_byteplus_coding_plan_provider_configurable_with_protocol_endpoint():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="byteplus_coding_plan",
|
|
model="seed-2-0-lite-260228",
|
|
api_key="k",
|
|
)
|
|
|
|
assert res.changed is True
|
|
assert res.config.llm.provider == "byteplus_coding_plan"
|
|
assert res.config.llm.model == "seed-2-0-lite-260228"
|
|
assert res.config.llm.api_key == "k"
|
|
assert res.config.llm.base_url == "https://ark.ap-southeast.bytepluses.com/api/coding/v3"
|
|
|
|
|
|
def test_volcengine_coding_plan_provider_configurable():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="volcengine_coding_plan",
|
|
model="doubao-seed-2.0-pro",
|
|
api_key="k",
|
|
)
|
|
|
|
assert res.changed is True
|
|
assert res.config.llm.provider == "volcengine_coding_plan"
|
|
assert res.config.llm.model == "doubao-seed-2.0-pro"
|
|
assert res.config.llm.api_key == "k"
|
|
assert res.config.llm.base_url == "https://ark.cn-beijing.volces.com/api/coding/v3"
|
|
assert res.config.squilla_router.tiers["c0"]["model"] == "doubao-seed-2.0-lite"
|
|
assert res.config.squilla_router.tiers["c3"]["model"] == "doubao-seed-2.0-code"
|
|
|
|
|
|
def test_ollama_does_not_require_api_key():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(cfg, provider_id="ollama", model="llama3.1")
|
|
assert res.changed is True
|
|
assert res.config.llm.provider == "ollama"
|
|
|
|
|
|
def test_provider_save_outside_packaged_presets_seeds_inline_router_tiers():
|
|
"""Provider-neutral strategy setup seeds usable tiers for every chat model.
|
|
|
|
Non-packaged providers still persist inline tiers instead of tier_profile so
|
|
they do not masquerade as curated presets.
|
|
"""
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="groq",
|
|
model="test-model",
|
|
api_key="sk-test",
|
|
)
|
|
assert res.config.llm.provider == "groq"
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile is None
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
assert res.config.squilla_router.tiers[tier]["provider"] == "groq"
|
|
assert res.config.squilla_router.tiers[tier]["model"] == "test-model"
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert "tier_profile" not in persisted
|
|
assert persisted["tiers"]["c0"]["model"] == "test-model"
|
|
|
|
|
|
def test_tokenrhythm_provider_save_seeds_curated_inline_ladder():
|
|
"""A tokenrhythm key alone yields the curated c0-c3 ladder as inline tiers.
|
|
|
|
The tokenrhythm preset is curated but non-persistable (its id must never
|
|
appear as a tier_profile — downgrade contract), and the model field may be
|
|
omitted because the preset supplies the default direct model.
|
|
"""
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="tokenrhythm",
|
|
api_key="sk-test",
|
|
)
|
|
assert res.config.llm.provider == "tokenrhythm"
|
|
assert res.config.llm.model == "deepseek-v4-pro"
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile is None
|
|
expected = {
|
|
"c0": "deepseek-v4-flash",
|
|
"c1": "deepseek-v4-pro",
|
|
"c2": "kimi-k2.7-code",
|
|
"c3": "glm-5.2",
|
|
"image_model": "kimi-k2.6",
|
|
}
|
|
for tier, model in expected.items():
|
|
assert res.config.squilla_router.tiers[tier]["provider"] == "tokenrhythm"
|
|
assert res.config.squilla_router.tiers[tier]["model"] == model
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert "tier_profile" not in persisted
|
|
assert persisted["tiers"]["c3"]["model"] == "glm-5.2"
|
|
|
|
|
|
def test_upsert_channel_appends_new():
|
|
cfg = GatewayConfig()
|
|
res = upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "work",
|
|
"token": "xoxb-secret",
|
|
"signing_secret": "ss-secret",
|
|
},
|
|
)
|
|
assert res.restart_required is True
|
|
entries = list_channel_entries(res.config)
|
|
assert len(entries) == 1
|
|
assert entries[0]["name"] == "work"
|
|
assert entries[0]["type"] == "slack"
|
|
|
|
|
|
def test_upsert_channel_updates_same_name():
|
|
cfg = GatewayConfig()
|
|
res1 = upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "work",
|
|
"token": "old",
|
|
"signing_secret": "ss-old",
|
|
},
|
|
)
|
|
res2 = upsert_channel(
|
|
res1.config,
|
|
entry_payload={"type": "slack", "name": "work", "token": "new", "slack_channel_id": "C123"},
|
|
)
|
|
entries = list_channel_entries(res2.config)
|
|
assert len(entries) == 1
|
|
assert entries[0]["slack_channel_id"] == "C123"
|
|
|
|
|
|
def test_upsert_channel_redacts_secrets_in_payload():
|
|
cfg = GatewayConfig()
|
|
res = upsert_channel(
|
|
cfg,
|
|
entry_payload={"type": "telegram", "name": "tg", "token": "abc"},
|
|
)
|
|
assert res.public_payload["token"] == REDACTED_PLACEHOLDER
|
|
|
|
|
|
def test_slack_webhook_channel_requires_signing_secret():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="signing_secret"):
|
|
upsert_channel(
|
|
cfg,
|
|
entry_payload={"type": "slack", "name": "w", "token": "xoxb-test"},
|
|
)
|
|
|
|
|
|
def test_slack_socket_channel_does_not_require_signing_secret():
|
|
cfg = GatewayConfig()
|
|
res = upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "w",
|
|
"token": "xoxb-test",
|
|
"connection_mode": "socket",
|
|
"app_token": "xapp-test",
|
|
},
|
|
)
|
|
|
|
entry = list_channel_entries(res.config)[0]
|
|
assert entry["connection_mode"] == "socket"
|
|
assert "signing_secret" not in entry or entry["signing_secret"] in (None, "")
|
|
|
|
|
|
def test_slack_socket_channel_requires_app_token():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="app_token"):
|
|
upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "w",
|
|
"token": "xoxb-test",
|
|
"connection_mode": "socket",
|
|
},
|
|
)
|
|
|
|
|
|
def test_remove_channel():
|
|
cfg = GatewayConfig()
|
|
res1 = upsert_channel(
|
|
cfg,
|
|
entry_payload={"type": "slack", "name": "w", "token": "x", "signing_secret": "ss"},
|
|
)
|
|
res2 = remove_channel(res1.config, name="w")
|
|
assert list_channel_entries(res2.config) == []
|
|
assert res2.restart_required is True
|
|
|
|
|
|
def test_remove_missing_channel_raises():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(KeyError, match="w"):
|
|
remove_channel(cfg, name="w")
|
|
|
|
|
|
def test_set_channel_enabled_toggles():
|
|
cfg = GatewayConfig()
|
|
res1 = upsert_channel(
|
|
cfg,
|
|
entry_payload={"type": "slack", "name": "w", "token": "x", "signing_secret": "ss"},
|
|
)
|
|
res2 = set_channel_enabled(res1.config, name="w", enabled=False)
|
|
assert list_channel_entries(res2.config)[0]["enabled"] is False
|
|
|
|
|
|
def test_invalid_channel_type_rejected():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="unknown channel type"):
|
|
upsert_channel(cfg, entry_payload={"type": "nope", "name": "x"})
|
|
|
|
|
|
def test_upsert_llm_ensemble_accepts_structured_candidates_partial_merge():
|
|
cfg = GatewayConfig(
|
|
llm_ensemble={
|
|
"enabled": True,
|
|
"selection_mode": "router_dynamic",
|
|
"model_options": ["legacy/model"],
|
|
"min_successful_proposers": 2,
|
|
}
|
|
)
|
|
|
|
res = upsert_llm_ensemble(
|
|
cfg,
|
|
candidates=[
|
|
{
|
|
"provider": "openrouter",
|
|
"model": "qwen/qwen3.7-max",
|
|
"source": "custom",
|
|
"enabled": True,
|
|
}
|
|
],
|
|
)
|
|
|
|
assert res.changed is True
|
|
assert res.config.llm_ensemble.enabled is True
|
|
assert res.config.llm_ensemble.selection_mode == "router_dynamic"
|
|
assert res.config.llm_ensemble.model_options == ["legacy/model"]
|
|
assert res.config.llm_ensemble.min_successful_proposers == 2
|
|
assert [candidate.model_dump() for candidate in res.config.llm_ensemble.candidates] == [
|
|
{
|
|
"provider": "openrouter",
|
|
"model": "qwen/qwen3.7-max",
|
|
"source": "custom",
|
|
"enabled": True,
|
|
"role": "",
|
|
}
|
|
]
|
|
assert res.public_payload["candidates"] == [
|
|
{
|
|
"provider": "openrouter",
|
|
"model": "qwen/qwen3.7-max",
|
|
"source": "custom",
|
|
"enabled": True,
|
|
"role": "",
|
|
}
|
|
]
|
|
|
|
|
|
def test_upsert_llm_ensemble_keeps_legacy_model_options_payload():
|
|
cfg = GatewayConfig(llm_ensemble={"selection_mode": "router_dynamic"})
|
|
|
|
res = upsert_llm_ensemble(cfg, model_options=[" custom/model ", "custom/model"])
|
|
|
|
assert res.config.llm_ensemble.model_options == ["custom/model"]
|
|
assert res.public_payload["model_options"] == ["custom/model"]
|
|
|
|
|
|
def test_telegram_webhook_requires_webhook_url():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="webhook_url"):
|
|
upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "telegram",
|
|
"name": "t",
|
|
"token": "x",
|
|
"transport_name": "webhook",
|
|
},
|
|
)
|
|
|
|
|
|
def test_upsert_llm_provider_preserves_existing_api_key_on_same_provider():
|
|
cfg = GatewayConfig()
|
|
res1 = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="m1",
|
|
api_key="sk-existing",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
)
|
|
# Reconfigure model only, leaving api_key blank — should reuse existing.
|
|
res2 = upsert_llm_provider(
|
|
res1.config,
|
|
provider_id="openrouter",
|
|
model="m2",
|
|
api_key="",
|
|
)
|
|
assert res2.config.llm.api_key == "sk-existing"
|
|
assert res2.config.llm.model == "m2"
|
|
|
|
|
|
def test_upsert_llm_provider_can_use_env_key_without_secret():
|
|
cfg = GatewayConfig()
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="deepseek/deepseek-v4-flash",
|
|
api_key="",
|
|
api_key_env="OPENROUTER_API_KEY",
|
|
)
|
|
|
|
assert res.config.llm.api_key == ""
|
|
assert res.config.llm.api_key_env == "OPENROUTER_API_KEY"
|
|
assert res.public_payload["api_key_source"] == "env"
|
|
|
|
|
|
def test_upsert_llm_provider_recomputes_router_preset_on_provider_switch():
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
assert cfg.squilla_router.enabled is True
|
|
# Fresh OpenRouter configs remain the openrouter-mix/direct-router shape.
|
|
# Saving a different packaged provider below should still recompute that
|
|
# provider's compact router profile.
|
|
assert cfg.squilla_router.tier_profile is None
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="deepseek",
|
|
model="deepseek-chat",
|
|
api_key_env="DEEPSEEK_API_KEY",
|
|
)
|
|
|
|
assert res.config.llm.provider == "deepseek"
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile == "deepseek"
|
|
assert res.config.squilla_router.tiers["c0"]["provider"] == "deepseek"
|
|
assert "tiers" not in res.config.to_toml_dict()["squilla_router"]
|
|
|
|
|
|
def test_upsert_llm_provider_without_preset_matches_todays_reconcile_behavior():
|
|
# D18-intact: a plain provider save (no presetId) must behave exactly as
|
|
# before — the reconcile path assigns the legacy tier_profile and writes
|
|
# no inline tiers. This freezes that the preset feature did not change the
|
|
# no-preset default.
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
|
|
res = upsert_llm_provider(cfg, provider_id="deepseek", model="deepseek-chat",
|
|
api_key_env="DEEPSEEK_API_KEY")
|
|
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert persisted["tier_profile"] == "deepseek"
|
|
assert "tiers" not in persisted
|
|
|
|
|
|
def test_upsert_llm_provider_preset_id_legacy_writes_recommended_shape():
|
|
# Explicit legacy-nine presetId == today's recommended path: enabled,
|
|
# persisted tier_profile, no inline tiers.
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="deepseek",
|
|
model="deepseek-chat",
|
|
api_key_env="DEEPSEEK_API_KEY",
|
|
preset_id="deepseek",
|
|
)
|
|
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile == "deepseek"
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert persisted["tier_profile"] == "deepseek"
|
|
assert "tiers" not in persisted
|
|
|
|
|
|
def test_upsert_llm_provider_preset_id_applies_default_model_when_model_omitted():
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="deepseek",
|
|
preset_id="deepseek",
|
|
api_key_env="DEEPSEEK_API_KEY",
|
|
)
|
|
|
|
# deepseek preset default_model is deepseek-v4-flash.
|
|
assert res.config.llm.model == "deepseek-v4-flash"
|
|
|
|
|
|
def test_upsert_llm_provider_preset_id_synthesized_writes_custom_shape():
|
|
# A synthesized preset id (== provider id) writes the custom-mode shape:
|
|
# enabled, tier_profile=None, expanded tiers, with empty model slots filled
|
|
# by the effective model.
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="groq",
|
|
model="llama-3.3-70b",
|
|
api_key_env="GROQ_API_KEY",
|
|
preset_id="groq",
|
|
)
|
|
|
|
router = res.config.squilla_router
|
|
assert router.enabled is True
|
|
assert router.tier_profile is None
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
assert router.tiers[tier]["provider"] == "groq"
|
|
assert router.tiers[tier]["model"] == "llama-3.3-70b"
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert "tier_profile" not in persisted
|
|
assert persisted["tiers"]["c0"]["model"] == "llama-3.3-70b"
|
|
|
|
|
|
def test_upsert_llm_provider_curated_synthesized_preset_preserves_ladder():
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="qianfan",
|
|
api_key_env="QIANFAN_API_KEY",
|
|
preset_id="qianfan",
|
|
)
|
|
|
|
router = res.config.squilla_router
|
|
assert router.enabled is True
|
|
assert router.tier_profile is None
|
|
assert res.config.llm.model == "ernie-4.5-turbo-128k"
|
|
assert router.tiers["c0"]["model"] == "ernie-4.5-turbo-128k"
|
|
assert router.tiers["c3"]["model"] == "ernie-4.5-turbo-128k"
|
|
assert router.tiers["image_model"]["model"] == "ernie-4.5-turbo-vl-32k"
|
|
assert router.tiers["image_model"]["image_only"] is True
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert "tier_profile" not in persisted
|
|
assert persisted["tiers"]["c0"]["model"] == "ernie-4.5-turbo-128k"
|
|
|
|
|
|
def test_upsert_llm_provider_preset_id_must_match_provider():
|
|
cfg = GatewayConfig()
|
|
|
|
with pytest.raises(ValueError, match="does not apply to provider"):
|
|
upsert_llm_provider(
|
|
cfg,
|
|
provider_id="deepseek",
|
|
model="deepseek-chat",
|
|
api_key_env="DEEPSEEK_API_KEY",
|
|
preset_id="openai",
|
|
)
|
|
|
|
|
|
def test_upsert_router_recommended_writes_profile_without_expanded_tiers():
|
|
cfg = GatewayConfig(llm={"provider": "deepseek", "model": "deepseek-chat"})
|
|
|
|
res = upsert_router(cfg, mode="recommended")
|
|
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile == "deepseek"
|
|
assert "tiers" not in res.config.to_toml_dict()["squilla_router"]
|
|
assert res.public_payload["mode"] == "recommended"
|
|
|
|
|
|
def test_upsert_router_forces_image_model_role_invariants():
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "z-ai/glm-5.1"})
|
|
|
|
res = upsert_router(
|
|
cfg,
|
|
mode="openrouter-mix",
|
|
tiers={
|
|
"image_model": {
|
|
"provider": "openrouter",
|
|
"model": "anthropic/claude-opus-4.8",
|
|
"supportsImage": False,
|
|
"image_only": False,
|
|
}
|
|
},
|
|
)
|
|
|
|
image_tier = res.config.squilla_router.tiers["image_model"]
|
|
assert image_tier["model"] == "anthropic/claude-opus-4.8"
|
|
assert image_tier["supports_image"] is True
|
|
assert image_tier["image_only"] is True
|
|
|
|
|
|
def test_upsert_router_can_disable():
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "deepseek/x"})
|
|
|
|
res = upsert_router(cfg, mode="disabled")
|
|
|
|
assert res.config.squilla_router.enabled is False
|
|
assert res.config.squilla_router.tier_profile is None
|
|
assert res.public_payload["mode"] == "disabled"
|
|
|
|
|
|
def test_upsert_router_rejects_openrouter_mix_for_direct_provider():
|
|
cfg = GatewayConfig(llm={"provider": "deepseek", "model": "deepseek-chat"})
|
|
|
|
with pytest.raises(ValueError, match="openrouter-mix"):
|
|
upsert_router(cfg, mode="openrouter-mix")
|
|
|
|
|
|
def test_upsert_router_custom_is_accepted_for_any_provider():
|
|
# custom is the provider-agnostic generalization of openrouter-mix: for a
|
|
# legacy-nine provider it seeds tiers from the packaged preset but writes
|
|
# NO tier_profile.
|
|
cfg = GatewayConfig(llm={"provider": "deepseek", "model": "deepseek-chat"})
|
|
|
|
res = upsert_router(cfg, mode="custom")
|
|
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile is None
|
|
assert res.config.squilla_router.tiers["c0"]["provider"] == "deepseek"
|
|
assert res.config.squilla_router.tiers["c0"]["model"] == "deepseek-v4-flash"
|
|
assert res.public_payload["mode"] == "custom"
|
|
assert res.public_payload["tier_profile"] is None
|
|
# With no persisted profile the effective tiers persist expanded inline.
|
|
persisted = res.config.to_toml_dict()["squilla_router"]
|
|
assert "tier_profile" not in persisted
|
|
assert persisted["tiers"]["c2"]["model"] == "deepseek-v4-pro"
|
|
|
|
|
|
def test_upsert_router_custom_merges_provided_tiers_over_preset():
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "model": "z-ai/glm-5.1"})
|
|
|
|
res = upsert_router(
|
|
cfg,
|
|
mode="custom",
|
|
tiers={"c3": {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"}},
|
|
)
|
|
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile is None
|
|
assert res.config.squilla_router.tiers["c3"]["model"] == "anthropic/claude-opus-4.8"
|
|
# Unoverridden tiers keep the openrouter preset values.
|
|
assert res.config.squilla_router.tiers["c0"]["provider"] == "openrouter"
|
|
|
|
|
|
def test_upsert_router_can_enable_cross_provider_tiers():
|
|
cfg = GatewayConfig(llm={"provider": "openai", "model": "gpt-5.4-mini"})
|
|
|
|
res = upsert_router(
|
|
cfg,
|
|
mode="custom",
|
|
cross_provider_tiers=True,
|
|
tier_provider_mismatch="veto",
|
|
tiers={
|
|
"c0": {"provider": "openai", "model": "gpt-5.4-mini"},
|
|
"c1": {"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"},
|
|
"c2": {"provider": "openrouter", "model": "z-ai/glm-5.2"},
|
|
"c3": {"provider": "openai", "model": "gpt-5.5"},
|
|
},
|
|
)
|
|
|
|
router = res.config.squilla_router
|
|
assert router.enabled is True
|
|
assert router.tier_profile is None
|
|
assert router.cross_provider_tiers is True
|
|
assert router.tier_provider_mismatch == "veto"
|
|
assert router.tiers["c1"]["provider"] == "openrouter"
|
|
assert res.public_payload["cross_provider_tiers"] is True
|
|
assert res.public_payload["tier_provider_mismatch"] == "veto"
|
|
|
|
|
|
def test_upsert_router_rejects_unknown_tier_provider_mismatch_policy():
|
|
cfg = GatewayConfig(llm={"provider": "openai", "model": "gpt-5.4-mini"})
|
|
|
|
with pytest.raises(ValueError, match="tierProviderMismatch"):
|
|
upsert_router(
|
|
cfg,
|
|
mode="custom",
|
|
tier_provider_mismatch="drop",
|
|
tiers={
|
|
"c0": {"provider": "openai", "model": "gpt-5.4-mini"},
|
|
"c1": {"provider": "openai", "model": "gpt-5.4"},
|
|
"c2": {"provider": "openai", "model": "gpt-5.5-mini"},
|
|
"c3": {"provider": "openai", "model": "gpt-5.5"},
|
|
},
|
|
)
|
|
|
|
|
|
def test_upsert_router_custom_seeds_current_model_for_synthesized_presets():
|
|
# Non-packaged providers have no curated ladder, so custom mode seeds every
|
|
# tier from the current Chat Model instead of leaving empty routes.
|
|
cfg = GatewayConfig(llm={"provider": "groq", "model": "m"})
|
|
|
|
res = upsert_router(cfg, mode="custom")
|
|
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile is None
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
assert res.config.squilla_router.tiers[tier]["provider"] == "groq"
|
|
assert res.config.squilla_router.tiers[tier]["model"] == "m"
|
|
assert res.public_payload["mode"] == "custom"
|
|
|
|
|
|
def test_upsert_router_custom_accepts_explicit_tiers_for_synthesized_presets():
|
|
cfg = GatewayConfig(llm={"provider": "groq", "model": "m"})
|
|
|
|
res = upsert_router(
|
|
cfg,
|
|
mode="custom",
|
|
tiers={
|
|
"c0": {"model": "groq-fast"},
|
|
"c1": {"model": "groq-mid"},
|
|
"c2": {"model": "groq-strong"},
|
|
"c3": {"model": "groq-max"},
|
|
},
|
|
)
|
|
|
|
assert res.config.squilla_router.enabled is True
|
|
assert res.config.squilla_router.tier_profile is None
|
|
assert res.config.squilla_router.tiers["c1"] == {
|
|
"provider": "groq",
|
|
"model": "groq-mid",
|
|
"description": (
|
|
"groq balanced route (synthesized default; no curated per-tier model ladder)."
|
|
),
|
|
"supports_image": False,
|
|
}
|
|
# The default tier's model syncs into llm.model (same as the other
|
|
# enabled modes).
|
|
assert res.config.llm.model == "groq-mid"
|
|
|
|
|
|
def test_upsert_router_custom_sync_llm_model_from_default_tier():
|
|
cfg = GatewayConfig(llm={"provider": "deepseek", "model": "deepseek-chat"})
|
|
|
|
res = upsert_router(cfg, mode="custom", default_tier="c2")
|
|
|
|
assert res.config.squilla_router.default_tier == "c2"
|
|
assert res.config.llm.model == "deepseek-v4-pro"
|
|
|
|
|
|
def test_upsert_llm_provider_keeps_runtime_secret_marker_when_reusing_key():
|
|
cfg = GatewayConfig()
|
|
cfg.llm.provider = "openrouter"
|
|
cfg.llm.model = "m1"
|
|
cfg.llm.api_key = "from-env"
|
|
cfg.mark_runtime_secret("llm.api_key")
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="m2",
|
|
api_key="",
|
|
)
|
|
|
|
assert res.config.llm.api_key == "from-env"
|
|
assert "llm.api_key" in res.config._runtime_secret_paths
|
|
|
|
|
|
def test_upsert_llm_provider_clears_runtime_secret_marker_for_explicit_key():
|
|
cfg = GatewayConfig()
|
|
cfg.llm.provider = "openrouter"
|
|
cfg.llm.model = "m1"
|
|
cfg.llm.api_key = "from-env"
|
|
cfg.mark_runtime_secret("llm.api_key")
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="m2",
|
|
api_key="sk-written",
|
|
)
|
|
|
|
assert res.config.llm.api_key == "sk-written"
|
|
assert "llm.api_key" not in res.config._runtime_secret_paths
|
|
|
|
|
|
def test_upsert_llm_provider_explicit_key_clears_existing_env_key(monkeypatch):
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "from-env")
|
|
cfg = GatewayConfig(
|
|
llm={
|
|
"provider": "openrouter",
|
|
"model": "m1",
|
|
"api_key": "",
|
|
"api_key_env": "OPENROUTER_API_KEY",
|
|
}
|
|
)
|
|
|
|
res = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="m2",
|
|
api_key="sk-written",
|
|
)
|
|
runtime = resolve_llm_runtime_config(res.config)
|
|
|
|
assert res.config.llm.api_key_env == ""
|
|
assert runtime.api_key == "sk-written"
|
|
assert runtime.api_key_from_env is False
|
|
|
|
|
|
def test_upsert_llm_provider_rejects_ambiguous_key_sources():
|
|
cfg = GatewayConfig()
|
|
|
|
with pytest.raises(ValueError, match="either api_key or api_key_env"):
|
|
upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="m",
|
|
api_key="sk-written",
|
|
api_key_env="OPENROUTER_API_KEY",
|
|
)
|
|
|
|
|
|
def test_upsert_llm_provider_does_not_carry_key_across_providers():
|
|
cfg = GatewayConfig()
|
|
res1 = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
model="m",
|
|
api_key="sk-or",
|
|
)
|
|
# Switching to a different key-required provider must require a new key.
|
|
with pytest.raises(ValueError, match="api_key"):
|
|
upsert_llm_provider(
|
|
res1.config,
|
|
provider_id="openai",
|
|
model="gpt-4o",
|
|
api_key="",
|
|
)
|
|
|
|
|
|
def test_validate_channel_entry_returns_normalized_payload():
|
|
out = validate_channel_entry(
|
|
{"type": "slack", "name": "w", "token": "x", "signing_secret": "ss"}
|
|
)
|
|
assert out["type"] == "slack"
|
|
assert out["enabled"] is True
|
|
assert out["agent_id"] == "main"
|
|
|
|
|
|
def test_upsert_search_provider_configures_brave():
|
|
cfg = GatewayConfig()
|
|
res = upsert_search_provider(
|
|
cfg,
|
|
provider_id="brave",
|
|
api_key="brave-key",
|
|
max_results=7,
|
|
proxy="http://127.0.0.1:7890",
|
|
use_env_proxy=True,
|
|
fallback_policy="network",
|
|
diagnostics=True,
|
|
)
|
|
assert res.config.search_provider == "brave"
|
|
assert res.config.search_api_key == "brave-key"
|
|
assert res.config.search_max_results == 7
|
|
assert res.config.search_proxy == "http://127.0.0.1:7890"
|
|
assert res.config.search_use_env_proxy is True
|
|
assert res.config.search_fallback_policy == "network"
|
|
assert res.config.search_diagnostics is True
|
|
assert res.public_payload["api_key"] == REDACTED_PLACEHOLDER
|
|
|
|
|
|
def test_upsert_search_provider_configures_tavily_with_api_key():
|
|
cfg = GatewayConfig()
|
|
res = upsert_search_provider(
|
|
cfg,
|
|
provider_id="tavily",
|
|
api_key="tavily-key",
|
|
)
|
|
|
|
assert res.config.search_provider == "tavily"
|
|
assert res.config.search_api_key == "tavily-key"
|
|
assert res.config.search_api_key_env == ""
|
|
assert res.public_payload["api_key"] == REDACTED_PLACEHOLDER
|
|
assert res.public_payload["api_key_source"] == "explicit"
|
|
|
|
|
|
def test_upsert_search_provider_strips_trailing_paste_punctuation_from_api_key():
|
|
cfg = GatewayConfig()
|
|
res = upsert_search_provider(cfg, provider_id="brave", api_key="brave-key、")
|
|
|
|
assert res.config.search_api_key == "brave-key"
|
|
|
|
|
|
def test_upsert_search_provider_can_use_env_key_reference():
|
|
cfg = GatewayConfig()
|
|
res = upsert_search_provider(
|
|
cfg,
|
|
provider_id="brave",
|
|
api_key="",
|
|
api_key_env="BRAVE_SEARCH_API_KEY",
|
|
)
|
|
assert res.config.search_provider == "brave"
|
|
assert res.config.search_api_key == ""
|
|
assert res.config.search_api_key_env == "BRAVE_SEARCH_API_KEY"
|
|
assert res.public_payload["api_key_source"] == "env"
|
|
|
|
|
|
def test_upsert_search_provider_can_use_tavily_env_key_reference():
|
|
cfg = GatewayConfig()
|
|
res = upsert_search_provider(
|
|
cfg,
|
|
provider_id="tavily",
|
|
api_key="",
|
|
api_key_env="TAVILY_API_KEY",
|
|
)
|
|
|
|
assert res.config.search_provider == "tavily"
|
|
assert res.config.search_api_key == ""
|
|
assert res.config.search_api_key_env == "TAVILY_API_KEY"
|
|
assert res.public_payload["api_key_source"] == "env"
|
|
|
|
|
|
def test_upsert_search_provider_accepts_webui_string_max_results():
|
|
cfg = GatewayConfig()
|
|
res = upsert_search_provider(
|
|
cfg,
|
|
provider_id="duckduckgo",
|
|
max_results="5",
|
|
)
|
|
|
|
assert res.config.search_provider == "duckduckgo"
|
|
assert res.config.search_max_results == 5
|
|
|
|
|
|
def test_upsert_search_provider_clears_env_key_for_no_key_provider():
|
|
cfg = GatewayConfig(search_provider="brave", search_api_key_env="BRAVE_SEARCH_API_KEY")
|
|
|
|
res = upsert_search_provider(
|
|
cfg,
|
|
provider_id="duckduckgo",
|
|
api_key_env="BRAVE_SEARCH_API_KEY",
|
|
)
|
|
|
|
assert res.config.search_provider == "duckduckgo"
|
|
assert res.config.search_api_key_env == ""
|
|
assert res.public_payload["api_key_source"] == "none"
|
|
|
|
|
|
def test_upsert_image_generation_provider_configures_openrouter(monkeypatch):
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
cfg = GatewayConfig()
|
|
res = upsert_image_generation_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
primary="openrouter/google/gemini-3.1-flash-image-preview",
|
|
api_key="sk-or",
|
|
)
|
|
assert res.config.image_generation.enabled is True
|
|
assert res.config.image_generation.primary == "openrouter/google/gemini-3.1-flash-image-preview"
|
|
assert res.config.image_generation.providers.openrouter.api_key == "sk-or"
|
|
assert res.public_payload["api_key"] == REDACTED_PLACEHOLDER
|
|
assert res.public_payload["api_key_source"] == "explicit"
|
|
|
|
|
|
def test_upsert_image_generation_provider_can_use_matching_llm_key(monkeypatch):
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
cfg = GatewayConfig()
|
|
cfg.llm.provider = "openrouter"
|
|
cfg.llm.api_key = "sk-llm"
|
|
res = upsert_image_generation_provider(cfg, provider_id="openrouter")
|
|
assert res.config.image_generation.enabled is True
|
|
assert res.config.image_generation.providers.openrouter.api_key == ""
|
|
assert res.public_payload["api_key_source"] == "llm_fallback"
|
|
|
|
|
|
def test_upsert_image_generation_provider_can_disable_without_key(monkeypatch):
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
cfg = GatewayConfig()
|
|
|
|
res = upsert_image_generation_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
primary="openrouter/google/gemini-3.1-flash-image-preview",
|
|
enabled=False,
|
|
)
|
|
|
|
assert res.config.image_generation.enabled is False
|
|
assert res.config.image_generation.primary == "openrouter/google/gemini-3.1-flash-image-preview"
|
|
assert res.public_payload["api_key_source"] == "none"
|
|
|
|
|
|
def test_upsert_image_generation_provider_rejects_wrong_primary_provider():
|
|
cfg = GatewayConfig()
|
|
cfg.llm.provider = "openrouter"
|
|
cfg.llm.api_key = "sk-llm"
|
|
with pytest.raises(ValueError, match="provider/model"):
|
|
upsert_image_generation_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
primary="openai/gpt-image-1",
|
|
)
|
|
|
|
|
|
def test_search_provider_requiring_key_can_reuse_existing_key():
|
|
cfg = GatewayConfig(search_provider="brave", search_api_key="old")
|
|
res = upsert_search_provider(cfg, provider_id="brave", api_key="")
|
|
assert res.config.search_api_key == "old"
|
|
|
|
|
|
def test_search_provider_requiring_key_rejects_missing_key():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="api_key"):
|
|
upsert_search_provider(cfg, provider_id="brave", api_key="", api_key_env="")
|
|
|
|
|
|
def test_unsupported_search_provider_rejected():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="not runtime-supported"):
|
|
upsert_search_provider(cfg, provider_id="perplexity", api_key="k")
|
|
|
|
|
|
def test_upsert_channel_preserves_secret_when_blank():
|
|
cfg = GatewayConfig()
|
|
first = upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "w",
|
|
"token": "xoxb-original",
|
|
"signing_secret": "ss-original",
|
|
},
|
|
)
|
|
second = upsert_channel(
|
|
first.config,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "w",
|
|
"token": "", # blank = keep current
|
|
"signing_secret": "",
|
|
"slack_channel_id": "C999",
|
|
},
|
|
)
|
|
raw = [e.model_dump(mode="python") for e in second.config.channels.channels]
|
|
entry = next(e for e in raw if e["name"] == "w")
|
|
assert entry["token"] == "xoxb-original"
|
|
assert entry["signing_secret"] == "ss-original"
|
|
assert entry["slack_channel_id"] == "C999"
|
|
|
|
|
|
def test_upsert_channel_replaces_secret_when_provided():
|
|
cfg = GatewayConfig()
|
|
first = upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "slack",
|
|
"name": "w",
|
|
"token": "xoxb-old",
|
|
"signing_secret": "ss-old",
|
|
},
|
|
)
|
|
second = upsert_channel(
|
|
first.config,
|
|
entry_payload={"type": "slack", "name": "w", "token": "xoxb-new"},
|
|
)
|
|
raw = [e.model_dump(mode="python") for e in second.config.channels.channels]
|
|
entry = next(e for e in raw if e["name"] == "w")
|
|
assert entry["token"] == "xoxb-new"
|
|
|
|
|
|
_IMG_PRIMARY = "openrouter/google/gemini-3.1-flash-image-preview"
|
|
|
|
|
|
def test_upsert_image_generation_applies_size_format_fallbacks():
|
|
cfg = GatewayConfig()
|
|
res = upsert_image_generation_provider(
|
|
cfg,
|
|
provider_id="openrouter",
|
|
primary=_IMG_PRIMARY,
|
|
api_key="sk-img",
|
|
size="1536x1024",
|
|
output_format="webp",
|
|
fallbacks=["openai/gpt-image-1", " openrouter/x ", ""],
|
|
)
|
|
ig = res.config.image_generation
|
|
assert ig.size == "1536x1024"
|
|
assert ig.output_format == "webp"
|
|
assert ig.fallbacks == ["openai/gpt-image-1", "openrouter/x"]
|
|
assert res.public_payload["size"] == "1536x1024"
|
|
assert res.public_payload["fallbacks"] == ["openai/gpt-image-1", "openrouter/x"]
|
|
|
|
|
|
def test_upsert_image_generation_empty_keeps_current():
|
|
cfg = GatewayConfig()
|
|
cfg.image_generation.size = "1024x1536"
|
|
cfg.image_generation.output_format = "jpeg"
|
|
cfg.image_generation.fallbacks = ["openrouter/keep"]
|
|
res = upsert_image_generation_provider(
|
|
cfg, provider_id="openrouter", primary=_IMG_PRIMARY, api_key="sk-img"
|
|
)
|
|
ig = res.config.image_generation
|
|
assert ig.size == "1024x1536"
|
|
assert ig.output_format == "jpeg"
|
|
assert ig.fallbacks == ["openrouter/keep"]
|
|
|
|
|
|
def test_upsert_image_generation_rejects_bad_size():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="image size"):
|
|
upsert_image_generation_provider(
|
|
cfg, provider_id="openrouter", primary=_IMG_PRIMARY, api_key="sk-img", size="999x999"
|
|
)
|
|
|
|
|
|
def test_upsert_image_generation_rejects_bad_fallback_reference():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="fallback"):
|
|
upsert_image_generation_provider(
|
|
cfg, provider_id="openrouter", primary=_IMG_PRIMARY, api_key="sk-img",
|
|
fallbacks=["no-slash-ref"],
|
|
)
|
|
|
|
|
|
def test_validate_channel_entry_error_never_echoes_secret_values():
|
|
secret = "tg-super-secret-token-value-1234567890"
|
|
|
|
with pytest.raises(ValueError, match="webhook_url") as exc_info:
|
|
validate_channel_entry(
|
|
{
|
|
"type": "telegram",
|
|
"name": "t",
|
|
"transport_name": "webhook",
|
|
"token": secret,
|
|
}
|
|
)
|
|
|
|
message = str(exc_info.value)
|
|
assert secret not in message
|
|
assert secret[-12:] not in message
|
|
assert "input_value" not in message
|
|
|
|
|
|
def test_upsert_channel_error_never_echoes_secret_values():
|
|
cfg = GatewayConfig()
|
|
secret = "tg-super-secret-token-value-1234567890"
|
|
|
|
with pytest.raises(ValueError, match="webhook_url") as exc_info:
|
|
upsert_channel(
|
|
cfg,
|
|
entry_payload={
|
|
"type": "telegram",
|
|
"name": "t",
|
|
"transport_name": "webhook",
|
|
"token": secret,
|
|
},
|
|
)
|
|
|
|
message = str(exc_info.value)
|
|
assert secret not in message
|
|
assert secret[-12:] not in message
|
|
|
|
|
|
def test_upsert_channel_rejects_blank_required_secret_for_new_entry():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="token"):
|
|
upsert_channel(cfg, entry_payload={"type": "telegram", "name": "t", "token": ""})
|
|
|
|
|
|
def test_upsert_channel_rejects_whitespace_only_required_secret_for_new_entry():
|
|
cfg = GatewayConfig()
|
|
with pytest.raises(ValueError, match="token"):
|
|
upsert_channel(cfg, entry_payload={"type": "telegram", "name": "t", "token": " "})
|
|
|
|
|
|
def test_validate_channel_entry_rejects_blank_required_secret():
|
|
with pytest.raises(ValueError, match="token"):
|
|
validate_channel_entry({"type": "telegram", "name": "t", "token": ""})
|
|
|
|
|
|
def test_validate_channel_entry_blank_secret_skips_inapplicable_show_when_fields():
|
|
# slack socket mode: signing_secret is required only for webhook mode,
|
|
# so the blank-secret gate must not fire for it here.
|
|
out = validate_channel_entry(
|
|
{
|
|
"type": "slack",
|
|
"name": "w",
|
|
"token": "xoxb-test",
|
|
"connection_mode": "socket",
|
|
"app_token": "xapp-test",
|
|
}
|
|
)
|
|
assert out["connection_mode"] == "socket"
|
|
|
|
|
|
def test_upsert_channel_whitespace_only_secret_keeps_existing_value():
|
|
cfg = GatewayConfig()
|
|
first = upsert_channel(
|
|
cfg,
|
|
entry_payload={"type": "telegram", "name": "t", "token": "tg-original"},
|
|
)
|
|
second = upsert_channel(
|
|
first.config,
|
|
entry_payload={"type": "telegram", "name": "t", "token": " "},
|
|
)
|
|
raw = [e.model_dump(mode="python") for e in second.config.channels.channels]
|
|
entry = next(e for e in raw if e["name"] == "t")
|
|
assert entry["token"] == "tg-original"
|
|
|
|
|
|
def test_router_reconcile_survives_out_of_band_llm_model_change():
|
|
"""R6: a machine-seeded ladder must stay recognizable after llm.model
|
|
changed out-of-band (config.set RPC / TOML hand-edit): a re-save naming
|
|
the new model explicitly must reseed the tiers instead of leaving every
|
|
routed turn pinned to the old model."""
|
|
cfg = GatewayConfig()
|
|
seeded = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="anthropic",
|
|
model="model-alpha",
|
|
api_key="sk-old",
|
|
).config
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
assert seeded.squilla_router.tiers[tier]["model"] == "model-alpha"
|
|
|
|
# Out-of-band hot-apply: llm.model changes without a provider save.
|
|
seeded.llm.model = "model-beta"
|
|
|
|
rotated = upsert_llm_provider(
|
|
seeded,
|
|
provider_id="anthropic",
|
|
model="model-beta",
|
|
api_key="sk-new",
|
|
).config
|
|
assert rotated.llm.model == "model-beta"
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
assert rotated.squilla_router.tiers[tier]["model"] == "model-beta"
|
|
|
|
|
|
def test_router_reconcile_still_preserves_hand_authored_ladder():
|
|
"""The R6 widening must not regress the hand-customized guard: a ladder
|
|
that matches no machine seeding survives a same-provider re-save."""
|
|
cfg = GatewayConfig()
|
|
seeded = upsert_llm_provider(
|
|
cfg,
|
|
provider_id="anthropic",
|
|
model="model-alpha",
|
|
api_key="sk-old",
|
|
).config
|
|
router_payload = seeded.squilla_router.model_dump(mode="python")
|
|
router_payload["tiers"]["c0"]["model"] = "model-cheap"
|
|
router_payload["tiers"]["c3"]["model"] = "model-big"
|
|
from opensquilla.gateway.config import SquillaRouterConfig
|
|
|
|
seeded.squilla_router = SquillaRouterConfig(**router_payload)
|
|
|
|
rotated = upsert_llm_provider(
|
|
seeded,
|
|
provider_id="anthropic",
|
|
model="model-alpha",
|
|
api_key="sk-new",
|
|
).config
|
|
assert rotated.squilla_router.tiers["c0"]["model"] == "model-cheap"
|
|
assert rotated.squilla_router.tiers["c3"]["model"] == "model-big"
|
|
|
|
|
|
def test_upsert_router_non_registry_provider_gets_actionable_error():
|
|
"""X1: a hand-edited non-registry llm.provider used to die on the cryptic
|
|
"router tier 'c0' must be an object"; the error must now name the
|
|
provider and point at the two runnable ways out."""
|
|
cfg = GatewayConfig()
|
|
cfg.llm.provider = "acme-llm"
|
|
cfg.llm.model = "acme-model"
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
upsert_router(cfg, mode="recommended", default_tier="c1")
|
|
|
|
message = str(exc_info.value)
|
|
assert "acme-llm" in message
|
|
assert "opensquilla onboard configure provider --provider" in message
|
|
assert "--router disabled" in message
|
|
assert "must be an object" not in message
|
|
|
|
|
|
def test_upsert_router_non_registry_provider_disabled_still_works():
|
|
cfg = GatewayConfig()
|
|
cfg.llm.provider = "acme-llm"
|
|
cfg.llm.model = "acme-model"
|
|
res = upsert_router(cfg, mode="disabled")
|
|
assert res.config.squilla_router.enabled is False
|
|
|
|
|
|
def test_image_generation_explicit_enabled_decision_is_force_persisted(tmp_path):
|
|
"""R1: an explicit enabled=false on a fresh config equals the model
|
|
default, so the sparse diff alone would drop it — and a later key
|
|
rotation would re-enable image generation via configure-implies-enable.
|
|
The mutation must force the decision into the file."""
|
|
import tomllib as _tomllib
|
|
|
|
from opensquilla.onboarding.config_store import load_config, persist_config
|
|
|
|
target = tmp_path / "config.toml"
|
|
cfg = load_config(target)
|
|
res = upsert_image_generation_provider(
|
|
cfg,
|
|
provider_id="openai",
|
|
primary="openai/gpt-image-1",
|
|
api_key="sk-image-1",
|
|
enabled=False,
|
|
)
|
|
persist_config(res.config, path=target)
|
|
|
|
data = _tomllib.loads(target.read_text())
|
|
assert data["image_generation"]["enabled"] is False
|