Files
2026-07-13 13:12:33 +08:00

551 lines
20 KiB
Python

"""Per-section verifier behaviour and the ``needs_onboarding`` reduction."""
from __future__ import annotations
import pytest
from opensquilla.gateway.config import (
GatewayConfig,
LlmProviderConfig,
MemoryEmbeddingConfig,
SlackChannelEntry,
)
from opensquilla.onboarding.section_status import (
SectionStatus,
channels_section_status,
ensemble_section_status,
image_generation_section_status,
llm_section_status,
memory_embedding_section_status,
needs_onboarding,
router_section_status,
search_section_status,
)
from opensquilla.onboarding.status import get_onboarding_status
@pytest.fixture()
def cfg() -> GatewayConfig:
return GatewayConfig()
# ── llm ─────────────────────────────────────────────────────────────────────
def test_llm_missing_when_provider_unset(cfg):
cfg.llm = LlmProviderConfig(provider="", model="", api_key="")
assert llm_section_status(cfg) is SectionStatus.MISSING
def test_llm_ok_with_explicit_api_key(cfg):
cfg.llm = LlmProviderConfig(
provider="openrouter",
model="m",
api_key="sk-x",
base_url="https://openrouter.ai/api/v1",
)
assert llm_section_status(cfg) is SectionStatus.OK
def test_llm_ok_with_env_key_present(cfg, monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "from-env")
cfg.llm = LlmProviderConfig(
provider="openrouter",
model="m",
api_key="",
api_key_env="OPENROUTER_API_KEY",
base_url="https://openrouter.ai/api/v1",
)
assert llm_section_status(cfg) is SectionStatus.OK
def test_llm_degraded_when_env_key_missing(cfg, monkeypatch):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
cfg.llm = LlmProviderConfig(
provider="openrouter",
model="m",
api_key="",
api_key_env="OPENROUTER_API_KEY",
base_url="https://openrouter.ai/api/v1",
)
assert llm_section_status(cfg) is SectionStatus.DEGRADED
def test_llm_unknown_for_unsupported_provider(cfg):
cfg.llm = LlmProviderConfig(provider="no-such-provider", model="m")
assert llm_section_status(cfg) is SectionStatus.UNKNOWN
def test_llm_ok_with_default_env_var_when_config_omits_key(cfg, monkeypatch):
# No explicit api_key / api_key_env, but the provider's default env var is
# present — mirrors the runtime (resolve_llm_runtime_config) and image-gen.
monkeypatch.setenv("OPENROUTER_API_KEY", "from-env")
cfg.llm = LlmProviderConfig(
provider="openrouter",
model="m",
api_key="",
base_url="https://openrouter.ai/api/v1",
)
assert llm_section_status(cfg) is SectionStatus.OK
def test_llm_missing_without_key_or_default_env_var(cfg, monkeypatch):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
cfg.llm = LlmProviderConfig(
provider="openrouter",
model="m",
api_key="",
base_url="https://openrouter.ai/api/v1",
)
assert llm_section_status(cfg) is SectionStatus.MISSING
# ── router ──────────────────────────────────────────────────────────────────
def test_router_disabled_is_optional(cfg):
cfg.squilla_router.enabled = False
assert router_section_status(cfg) is SectionStatus.OPTIONAL
def test_router_enabled_is_ok(cfg):
cfg.squilla_router.enabled = True
assert router_section_status(cfg) is SectionStatus.OK
# ── ensemble ────────────────────────────────────────────────────────────────
def test_ensemble_enabled_is_ok(cfg):
cfg.llm_ensemble.enabled = True
assert ensemble_section_status(cfg) is SectionStatus.OK
def test_ensemble_disabled_is_optional(cfg):
cfg.llm_ensemble.enabled = False
assert ensemble_section_status(cfg) is SectionStatus.OPTIONAL
def test_ensemble_never_blocks_onboarding(cfg):
# The ensemble reuses the provider credential; there is nothing section-
# local to verify, so it must never produce a blocking/action state.
from opensquilla.onboarding.status import get_onboarding_status
for enabled in (True, False):
cfg.llm_ensemble.enabled = enabled
detail = get_onboarding_status(cfg).section_details["ensemble"]
assert detail["blocking"] is False
assert detail["actionRequired"] is False
assert detail["required"] is False
def test_ensemble_status_reports_candidate_provider_credentials(cfg, monkeypatch):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
# Construct with the llm section so the router ladder follows the provider;
# a post-construction reassignment would leave the built-in default
# provider's ladder in place and add its provider to the credential list.
cfg = GatewayConfig(
llm={
"provider": "deepseek",
"model": "deepseek-v4-flash",
"api_key": "sk-deepseek",
"base_url": "https://api.deepseek.com",
}
)
cfg.llm_ensemble.enabled = True
cfg.llm_ensemble.selection_mode = "router_dynamic"
cfg.llm_ensemble.candidates = [
{
"provider": "openrouter",
"model": "qwen/qwen3.7-max",
"source": "custom",
"enabled": True,
}
]
status = get_onboarding_status(cfg)
assert status.ensemble_credential_status == (
{
"provider": "deepseek",
"available": True,
"source": "explicit",
"envKey": "DEEPSEEK_API_KEY",
},
{
"provider": "openrouter",
"available": False,
"source": "missing_env",
"envKey": "OPENROUTER_API_KEY",
},
)
def test_llm_credential_status_reports_explicit_key(cfg):
cfg.llm = LlmProviderConfig(
provider="deepseek",
model="deepseek-v4-flash",
api_key="sk-deepseek-secret-123456",
api_key_env="",
base_url="https://api.deepseek.com",
)
status = get_onboarding_status(cfg)
cred = status.llm_credential_status
assert cred["provider"] == "deepseek"
assert cred["available"] is True
assert cred["source"] == "explicit"
assert cred["envKey"] == "DEEPSEEK_API_KEY"
assert cred["masked"] != "sk-deepseek-secret-123456"
assert cred["masked"].endswith("3456")
assert cred["revealAllowed"] is False
def test_llm_credential_status_reports_env_key(cfg, monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-deepseek-env-654321")
cfg.llm = LlmProviderConfig(
provider="deepseek",
model="deepseek-v4-flash",
api_key="",
api_key_env="DEEPSEEK_API_KEY",
base_url="https://api.deepseek.com",
)
status = get_onboarding_status(cfg)
cred = status.llm_credential_status
assert cred["provider"] == "deepseek"
assert cred["available"] is True
assert cred["source"] == "env"
assert cred["envKey"] == "DEEPSEEK_API_KEY"
assert cred["masked"] != "sk-deepseek-env-654321"
assert cred["masked"].endswith("4321")
assert cred["revealAllowed"] is False
def test_llm_credential_status_reports_missing_env(cfg, monkeypatch):
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
cfg.llm = LlmProviderConfig(
provider="deepseek",
model="deepseek-v4-flash",
api_key="",
api_key_env="DEEPSEEK_API_KEY",
base_url="https://api.deepseek.com",
)
status = get_onboarding_status(cfg)
assert status.llm_credential_status == {
"provider": "deepseek",
"available": False,
"source": "missing_env",
"envKey": "DEEPSEEK_API_KEY",
"masked": "",
"revealAllowed": False,
}
def test_llm_credential_status_treats_runtime_secret_cache_as_env(cfg, monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-env-current")
cfg.llm = LlmProviderConfig(
provider="deepseek",
model="deepseek-v4-flash",
api_key="sk-runtime-cache",
api_key_env="DEEPSEEK_API_KEY",
base_url="https://api.deepseek.com",
)
cfg.mark_runtime_secret("llm.api_key")
status = get_onboarding_status(cfg)
cred = status.llm_credential_status
assert cred["provider"] == "deepseek"
assert cred["available"] is True
assert cred["source"] == "env"
assert cred["envKey"] == "DEEPSEEK_API_KEY"
assert cred["masked"] != "sk-runtime-cache"
assert cred["masked"].endswith("rent")
assert cred["revealAllowed"] is False
# ── search ──────────────────────────────────────────────────────────────────
def test_search_unset_is_optional(cfg, monkeypatch):
cfg.search_provider = ""
monkeypatch.delenv("BRAVE_API_KEY", raising=False)
assert search_section_status(cfg) is SectionStatus.OPTIONAL
def test_search_duckduckgo_default_is_ok(cfg):
cfg.search_provider = "duckduckgo"
cfg.search_api_key = ""
cfg.search_api_key_env = ""
assert search_section_status(cfg) is SectionStatus.OK
def test_search_brave_with_explicit_key_is_ok(cfg):
cfg.search_provider = "brave"
cfg.search_api_key = "secret"
assert search_section_status(cfg) is SectionStatus.OK
def test_search_brave_without_credentials_is_missing(cfg, monkeypatch):
cfg.search_provider = "brave"
cfg.search_api_key = ""
cfg.search_api_key_env = ""
monkeypatch.delenv("BRAVE_API_KEY", raising=False)
assert search_section_status(cfg) is SectionStatus.MISSING
def test_search_brave_with_default_env_var_is_ok(cfg, monkeypatch):
from opensquilla.onboarding.search_specs import get_search_provider_setup_spec
# No explicit key/env declared, but the provider's default env var resolves.
cfg.search_provider = "brave"
cfg.search_api_key = ""
cfg.search_api_key_env = ""
monkeypatch.setenv(get_search_provider_setup_spec("brave").env_key, "from-env")
assert search_section_status(cfg) is SectionStatus.OK
def test_search_brave_with_env_key_missing_is_degraded(cfg, monkeypatch):
monkeypatch.delenv("BRAVE_API_KEY", raising=False)
cfg.search_provider = "brave"
cfg.search_api_key = ""
cfg.search_api_key_env = "BRAVE_API_KEY"
assert search_section_status(cfg) is SectionStatus.DEGRADED
def test_search_unknown_provider_is_unknown(cfg):
cfg.search_provider = "no-such-search"
assert search_section_status(cfg) is SectionStatus.UNKNOWN
# ── channels ────────────────────────────────────────────────────────────────
def test_channels_empty_is_optional(cfg):
cfg.channels.channels.clear()
assert channels_section_status(cfg) is SectionStatus.OPTIONAL
def test_channels_all_disabled_is_optional(cfg):
cfg.channels.channels.clear()
cfg.channels.channels.append(
SlackChannelEntry(name="work", enabled=False, token="x")
)
assert channels_section_status(cfg) is SectionStatus.OPTIONAL
def test_channels_any_enabled_is_ok(cfg):
cfg.channels.channels.clear()
cfg.channels.channels.append(
SlackChannelEntry(name="work", enabled=True, token="x")
)
assert channels_section_status(cfg) is SectionStatus.OK
# ── image generation ────────────────────────────────────────────────────────
def test_image_generation_disabled_is_optional(cfg):
cfg.image_generation.enabled = False
assert image_generation_section_status(cfg) is SectionStatus.OPTIONAL
def test_image_generation_enabled_without_credentials_is_missing(cfg, monkeypatch):
cfg.image_generation.enabled = True
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
# No provider credentials anywhere, LLM provider is not the image provider.
cfg.llm = LlmProviderConfig(provider="openrouter", model="m", api_key="")
assert image_generation_section_status(cfg) is SectionStatus.MISSING
def test_image_generation_unknown_provider_reference_is_unknown(cfg, monkeypatch):
cfg.image_generation.enabled = True
cfg.image_generation.primary = "no-such-provider/no-such-model"
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
assert image_generation_section_status(cfg) is SectionStatus.UNKNOWN
def test_image_generation_env_key_reference_missing_is_degraded(cfg, monkeypatch):
cfg.image_generation.enabled = True
cfg.image_generation.primary = "openai/gpt-image-1"
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("CUSTOM_IMAGE_KEY", raising=False)
cfg.llm = LlmProviderConfig(provider="openrouter", model="m", api_key="")
# Wire an explicit env reference to a variable that is not set.
openai_provider = cfg.image_generation.providers.openai
openai_provider.api_key = ""
openai_provider.api_key_env = "CUSTOM_IMAGE_KEY"
assert image_generation_section_status(cfg) is SectionStatus.DEGRADED
def test_image_generation_missing_custom_env_is_not_masked_by_default_env(
cfg,
monkeypatch,
):
cfg.image_generation.enabled = True
cfg.image_generation.primary = "openai/gpt-image-1"
cfg.llm = LlmProviderConfig(provider="openrouter", model="m", api_key="")
openai_provider = cfg.image_generation.providers.openai
openai_provider.api_key = ""
openai_provider.api_key_env = "CUSTOM_IMAGE_KEY"
monkeypatch.delenv("CUSTOM_IMAGE_KEY", raising=False)
monkeypatch.setenv("OPENAI_API_KEY", "default-env-key")
assert image_generation_section_status(cfg) is SectionStatus.DEGRADED
def test_image_generation_custom_default_primary_is_not_masked_by_other_provider(
cfg,
monkeypatch,
):
cfg.image_generation.enabled = True
cfg.image_generation.primary = "openai/gpt-image-1"
cfg.llm = LlmProviderConfig(provider="openrouter", model="m", api_key="sk-or")
openai_provider = cfg.image_generation.providers.openai
openai_provider.api_key = ""
openai_provider.api_key_env = "CUSTOM_IMAGE_KEY"
monkeypatch.delenv("CUSTOM_IMAGE_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
assert image_generation_section_status(cfg) is SectionStatus.DEGRADED
# ── memory embedding ────────────────────────────────────────────────────────
def test_memory_embedding_auto_is_ok(cfg):
cfg.memory.embedding = MemoryEmbeddingConfig(provider="auto")
assert memory_embedding_section_status(cfg) is SectionStatus.OK
def test_memory_embedding_none_is_optional(cfg):
cfg.memory.embedding = MemoryEmbeddingConfig(provider="none")
assert memory_embedding_section_status(cfg) is SectionStatus.OPTIONAL
def test_memory_embedding_remote_without_key_is_missing(cfg):
cfg.memory.embedding = MemoryEmbeddingConfig(provider="openai")
assert memory_embedding_section_status(cfg) is SectionStatus.MISSING
def test_memory_embedding_remote_with_missing_env_key_is_degraded(cfg, monkeypatch):
monkeypatch.delenv("OPENAI_EMBEDDINGS_API_KEY", raising=False)
cfg.memory.embedding = MemoryEmbeddingConfig(
provider="openai",
remote={"api_key_env": "OPENAI_EMBEDDINGS_API_KEY"},
)
assert memory_embedding_section_status(cfg) is SectionStatus.DEGRADED
def test_memory_embedding_remote_with_env_key_is_ok(cfg, monkeypatch):
monkeypatch.setenv("OPENAI_EMBEDDINGS_API_KEY", "mem-env-key")
cfg.memory.embedding = MemoryEmbeddingConfig(
provider="openai",
remote={"api_key_env": "OPENAI_EMBEDDINGS_API_KEY"},
)
assert memory_embedding_section_status(cfg) is SectionStatus.OK
def test_memory_embedding_remote_with_key_is_ok(cfg):
cfg.memory.embedding = MemoryEmbeddingConfig(
provider="openai",
remote={"api_key": "sk-embedding"},
)
assert memory_embedding_section_status(cfg) is SectionStatus.OK
# ── needs_onboarding reduction ───────────────────────────────────────────────
def test_needs_onboarding_false_when_all_ok_or_optional():
sections = {
"llm": SectionStatus.OK,
"router": SectionStatus.OPTIONAL,
"search": SectionStatus.OK,
"channels": SectionStatus.OPTIONAL,
"image_generation": SectionStatus.OPTIONAL,
"memory_embedding": SectionStatus.OK,
}
assert needs_onboarding(sections) is False
def test_needs_onboarding_false_when_optional_section_missing():
sections = {
"llm": SectionStatus.OK,
"router": SectionStatus.OPTIONAL,
"search": SectionStatus.MISSING,
"channels": SectionStatus.OPTIONAL,
"image_generation": SectionStatus.OPTIONAL,
"memory_embedding": SectionStatus.OK,
}
assert needs_onboarding(sections) is False
def test_needs_onboarding_true_when_required_section_degraded():
sections = {
"llm": SectionStatus.DEGRADED,
"router": SectionStatus.OK,
"search": SectionStatus.OK,
"channels": SectionStatus.OPTIONAL,
"image_generation": SectionStatus.OPTIONAL,
"memory_embedding": SectionStatus.OK,
}
assert needs_onboarding(sections) is True
def test_needs_onboarding_false_when_optional_section_unknown():
sections = {
"llm": SectionStatus.OK,
"router": SectionStatus.UNKNOWN,
"search": SectionStatus.OK,
"channels": SectionStatus.OPTIONAL,
"image_generation": SectionStatus.OPTIONAL,
"memory_embedding": SectionStatus.OK,
}
assert needs_onboarding(sections) is False
# ── shared image-generation provider resolution ─────────────────────────────
def test_image_generation_provider_resolution_has_a_single_implementation():
"""status.py must reuse section_status's provider resolution verbatim.
The two modules once carried textually diverging copies of this ~50-line
helper; pin the import so any future re-fork fails loudly.
"""
from opensquilla.onboarding import section_status, status
assert (
status._configured_image_generation_provider_ids
is section_status._configured_image_generation_provider_ids
)
def test_default_image_generation_primary_matches_config_default():
from opensquilla.gateway.config import ImageGenerationConfig
from opensquilla.onboarding.section_status import DEFAULT_IMAGE_GENERATION_PRIMARY
assert (
DEFAULT_IMAGE_GENERATION_PRIMARY
== ImageGenerationConfig.model_fields["primary"].default
)
assert DEFAULT_IMAGE_GENERATION_PRIMARY == GatewayConfig().image_generation.primary
def test_both_call_sites_resolve_the_same_providers(cfg, monkeypatch):
"""Verifier and status annotations must agree on the resolved providers."""
from opensquilla.onboarding import section_status
from opensquilla.onboarding.status import get_onboarding_status
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
cfg.image_generation.enabled = True
cfg.image_generation.primary = "openrouter/google/gemini-3.1-flash-image-preview"
cfg.image_generation.providers.openrouter.api_key = "sk-dummy-image"
resolved = section_status._configured_image_generation_provider_ids(cfg)
s = get_onboarding_status(cfg)
assert resolved == ["openrouter"]
assert s.image_generation_provider == "openrouter"
assert image_generation_section_status(cfg) is SectionStatus.OK
assert s.image_generation_configured is True