agent-zero/tests/test_model_config_api_keys.py
Alessandro 1304195d4c ux: onboarding flow prototype
reuse /banners for composer missing-key status

polish onboarding ui; add chat composer API key banner

drop dedicated status backend endpoint
2026-03-27 18:46:09 +01:00

107 lines
4 KiB
Python

import sys
import threading
import types
from pathlib import Path
import pytest
from flask import Flask
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
sys.modules["giturlparse"] = types.SimpleNamespace(parse=lambda *args, **kwargs: None)
class _DummyObserver:
def __init__(self):
self._alive = False
def is_alive(self):
return self._alive
def start(self):
self._alive = True
def stop(self):
self._alive = False
def join(self, *args, **kwargs):
return None
def unschedule_all(self):
return None
def schedule(self, *args, **kwargs):
return None
watchdog = types.ModuleType("watchdog")
watchdog.observers = types.SimpleNamespace(Observer=_DummyObserver)
watchdog.events = types.SimpleNamespace(FileSystemEventHandler=object)
sys.modules["watchdog"] = watchdog
sys.modules["watchdog.observers"] = watchdog.observers
sys.modules["watchdog.events"] = watchdog.events
from plugins._model_config.api.api_keys import ApiKeys
from plugins._model_config.extensions.python.banners import _20_missing_api_key as missing_key_banner
import models
def test_model_config_api_keys_can_be_cleared_via_backend(monkeypatch, tmp_path):
from helpers import dotenv
env_file = tmp_path / ".env"
monkeypatch.setattr(dotenv, "get_dotenv_file_path", lambda: str(env_file))
for key in ("API_KEY_OPENROUTER", "OPENROUTER_API_KEY", "OPENROUTER_API_TOKEN"):
monkeypatch.delenv(key, raising=False)
handler = ApiKeys(Flask(__name__), threading.Lock())
assert handler._set_keys({"keys": {"openrouter": "sk-test-openrouter"}}) == {"ok": True}
assert models.get_api_key("openrouter") == "sk-test-openrouter"
assert handler._set_keys({"keys": {"openrouter": ""}}) == {"ok": True}
assert models.get_api_key("openrouter") == "None"
assert handler._reveal_key({"provider": "openrouter"}) == {"ok": True, "value": ""}
@pytest.mark.asyncio
async def test_missing_api_key_banner_exposes_missing_providers(monkeypatch):
from plugins._model_config.helpers import model_config
fake = [{"model_type": "Chat Model", "provider": "openai"}]
monkeypatch.setattr(model_config, "get_missing_api_key_providers", lambda: fake)
banners = []
await missing_key_banner.MissingApiKeyCheck(agent=None).execute(
banners=banners, frontend_context={}
)
row = next(b for b in banners if b.get("id") == "missing-api-key")
assert row.get("missing_providers") == fake
def test_model_config_frontend_tracks_inline_api_key_edits():
store_path = PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "model-config-store.js"
composer_store_path = PROJECT_ROOT / "webui" / "components" / "chat" / "input" / "composer-banner-store.js"
config_path = PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "config.html"
modal_path = PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "api-keys.html"
store_content = store_path.read_text(encoding="utf-8")
composer_store_content = composer_store_path.read_text(encoding="utf-8")
config_content = config_path.read_text(encoding="utf-8")
modal_content = modal_path.read_text(encoding="utf-8")
assert "apiKeyDirty" in store_content
assert "resetApiKeyDrafts()" in store_content
assert "!provider || seen.has(provider) || !this.apiKeyDirty[provider]" in store_content
assert "normalized[provider] = value.trim() ? value : '';" in store_content
assert '"missing-api-key"' in composer_store_content
assert 'callJsonApi("/banners"' in composer_store_content
assert "/plugins/_model_config/missing_api_key_status" not in composer_store_content
assert "$store.modelConfig.resetApiKeyDrafts();" in config_content
assert '@input="$store.modelConfig.touchApiKey(config[section.key].provider)"' in config_content
assert "updates[provider] = this.keys[provider] || '';" in modal_content
assert "$store.modelConfig.resetApiKeyDrafts();" in modal_content