"""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