"""Tests for auxiliary models settings UI — panels.js + index.html + i18n.js. Verifies that the auxiliary models card is present in the settings HTML, that the JS loading/saving logic is wired up, and that all locales have the required i18n keys. """ from pathlib import Path ROOT = Path(__file__).parent.parent PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8") INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8") I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8") class TestAuxiliaryModelsHTML: """The auxiliary models card must be present in the settings preferences pane.""" def test_aux_models_container_exists(self): """The #auxModelsContainer div must exist in the preferences pane.""" assert 'id="auxModelsContainer"' in INDEX_HTML, ( "Missing #auxModelsContainer in index.html — auxiliary models card not rendered" ) def test_reset_button_exists(self): assert 'id="btnResetAuxModels"' in INDEX_HTML, ( "Missing #btnResetAuxModels button in index.html" ) def test_apply_button_exists(self): assert 'id="btnApplyAuxModels"' in INDEX_HTML, ( "Missing #btnApplyAuxModels button in index.html" ) def test_aux_card_after_default_model(self): """Auxiliary Models card should come after the Default Model card in the DOM.""" model_idx = INDEX_HTML.find('id="settingsModel"') aux_idx = INDEX_HTML.find('id="auxModelsContainer"') assert model_idx >= 0, "Default Model select not found in index.html" assert aux_idx >= 0, "Auxiliary Models container not found in index.html" assert aux_idx > model_idx, ( "Auxiliary Models container must appear after Default Model in the DOM" ) def test_i18n_label_on_aux_card(self): """The auxiliary models card label must use data-i18n attribute.""" assert 'data-i18n="settings_label_auxiliary_models"' in INDEX_HTML, ( "Missing data-i18n='settings_label_auxiliary_models' on auxiliary card label" ) class TestAuxiliaryModelsJS: """The JS logic for loading and saving auxiliary models must be in panels.js.""" def test_load_function_exists(self): assert "async function _loadAuxiliaryModels" in PANELS_JS, ( "Missing _loadAuxiliaryModels() in panels.js" ) def test_apply_function_exists(self): assert "async function _applyAuxModels" in PANELS_JS, ( "Missing _applyAuxModels() in panels.js" ) def test_aux_task_slots_defined(self): """_AUX_TASK_SLOTS must list the 9 canonical task slots.""" assert "_AUX_TASK_SLOTS" in PANELS_JS, ( "Missing _AUX_TASK_SLOTS constant in panels.js" ) # Verify all 9 tasks are present for key in ("vision", "compression", "web_extract", "session_search", "approval", "mcp", "title_generation", "skills_hub", "curator"): assert f"key:'{key}'" in PANELS_JS, ( f"Missing auxiliary task slot '{key}' in _AUX_TASK_SLOTS" ) def test_calls_model_auxiliary_api(self): """_loadAuxiliaryModels must call /api/model/auxiliary.""" assert "/api/model/auxiliary" in PANELS_JS, ( "panels.js must call /api/model/auxiliary to fetch current config" ) def test_calls_model_set_api(self): """_applyAuxModels must call /api/model/set to save changes.""" assert "/api/model/set" in PANELS_JS, ( "panels.js must call /api/model/set to save auxiliary model changes" ) def test_provider_cascade(self): """Changing provider must rebuild model dropdown.""" assert "_onAuxProviderChange" in PANELS_JS, ( "Missing _onAuxProviderChange() for provider→model cascade" ) assert "_buildAuxModelOptions" in PANELS_JS, ( "Missing _buildAuxModelOptions() for model dropdown rebuild" ) def test_custom_model_prompt(self): """Selecting 'Custom model…' must prompt for model ID.""" assert "__custom__" in PANELS_JS, ( "Missing __custom__ sentinel option for custom model input" ) def test_reset_calls_api_with_reset_task(self): """Reset button must call /api/model/set with task='__reset__'.""" idx = PANELS_JS.find("btnResetAuxModels") assert idx >= 0, "btnResetAuxModels not found in panels.js" # Check that __reset__ is sent in the reset handler body_after = PANELS_JS[idx:idx + 2000] assert "__reset__" in body_after, ( "Reset handler must send task='__reset__' to /api/model/set" ) def test_load_called_from_loadSettingsPanel(self): """_loadAuxiliaryModels must be called from loadSettingsPanel.""" assert "_loadAuxiliaryModels()" in PANELS_JS, ( "_loadAuxiliaryModels() is not called from loadSettingsPanel" ) def test_dirty_flag_marking(self): """Changing an auxiliary dropdown must mark settings dirty.""" assert "_markAuxDirty" in PANELS_JS, ( "Missing _markAuxDirty() for dirty detection" ) # _markAuxDirty should call _markSettingsDirty idx = PANELS_JS.find("function _markAuxDirty") body = PANELS_JS[idx:idx + 200] assert "_markSettingsDirty" in body, ( "_markAuxDirty must call _markSettingsDirty" ) class TestAuxiliaryModelsI18n: """All locales must have the auxiliary model i18n keys.""" REQUIRED_KEYS = [ "settings_label_auxiliary_models", "settings_desc_auxiliary_models", "settings_btn_reset_aux_models", "settings_btn_apply_aux_models", "settings_aux_provider_auto", "settings_aux_model_auto", "settings_aux_model_custom", "settings_aux_model_custom_prompt", "settings_aux_loading", "settings_aux_load_failed", "settings_aux_reset_confirm_title", "settings_aux_reset_confirm_msg", "settings_aux_reset_done", "settings_aux_save_failed", "settings_aux_saved", "settings_aux_no_changes", ] def test_all_i18n_keys_present(self): """Every required key must exist in i18n.js at least once.""" for key in self.REQUIRED_KEYS: assert key in I18N_JS, ( f"Missing i18n key '{key}' in i18n.js" ) def test_all_locales_have_auxiliary_keys(self): """Count of each key should equal the number of locales (12 with Turkish).""" for key in self.REQUIRED_KEYS: count = I18N_JS.count(f"{key}:") assert count == 12, ( f"i18n key '{key}' found {count} times — expected 12 (one per locale)" ) class TestAuxiliaryModelsBackend: """WebUI backend must expose /api/model/auxiliary and /api/model/set.""" ROUTES_PY = (ROOT / "api" / "routes.py").read_text(encoding="utf-8") CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8") def test_model_auxiliary_route_exists(self): """/api/model/auxiliary route must be registered in routes.py.""" assert '"/api/model/auxiliary"' in self.ROUTES_PY, ( "Missing /api/model/auxiliary route in routes.py" ) def test_model_set_route_exists(self): """/api/model/set route must be registered in routes.py.""" assert '"/api/model/set"' in self.ROUTES_PY, ( "Missing /api/model/set route in routes.py" ) def test_get_auxiliary_models_function_exists(self): """get_auxiliary_models() must exist in api/config.py.""" assert "def get_auxiliary_models" in self.CONFIG_PY, ( "Missing get_auxiliary_models() in api/config.py" ) def test_set_auxiliary_model_function_exists(self): """set_auxiliary_model() must exist in api/config.py.""" assert "def set_auxiliary_model" in self.CONFIG_PY, ( "Missing set_auxiliary_model() in api/config.py" ) def test_aux_task_slots_constant_exists(self): """AUX_TASK_SLOTS must be defined in api/config.py.""" assert "AUX_TASK_SLOTS" in self.CONFIG_PY, ( "Missing AUX_TASK_SLOTS constant in api/config.py" ) def test_js_uses_models_endpoint_not_options(self): """Frontend must use /api/models (WebUI's own API) not /api/model/options (agent API).""" # _loadAuxiliaryModels should call /api/models, not /api/model/options idx = PANELS_JS.find("async function _loadAuxiliaryModels") assert idx >= 0, "_loadAuxiliaryModels not found" body = PANELS_JS[idx:idx + 800] assert "/api/models" in body, ( "_loadAuxiliaryModels must call /api/models for provider/model lists" ) assert "/api/model/options" not in body, ( "_loadAuxiliaryModels must NOT call /api/model/options (agent-only endpoint)" ) def test_set_auxiliary_model_rejects_unknown_task(self, monkeypatch, tmp_path): """Unknown auxiliary task names must not pollute config.yaml.""" from api import config config_path = tmp_path / "config.yaml" config_path.write_text("auxiliary: {}\n", encoding="utf-8") monkeypatch.setattr(config, "_get_config_path", lambda: config_path) try: config.set_auxiliary_model("arbitrary_key", "openai", "gpt-5.5") except ValueError as exc: assert "Unknown auxiliary task slot" in str(exc) assert "vision" in str(exc) else: raise AssertionError("set_auxiliary_model accepted an unknown task") assert "arbitrary_key" not in config_path.read_text(encoding="utf-8") def test_model_set_route_returns_400_for_unknown_auxiliary_task(self, monkeypatch): """The route should surface invalid auxiliary task names as a client error.""" from types import SimpleNamespace from api import routes monkeypatch.setattr(routes, "_check_csrf", lambda _handler: True) monkeypatch.setattr(routes, "read_body", lambda _handler: { "scope": "auxiliary", "task": "arbitrary_key", "provider": "openai", "model": "gpt-5.5", }) monkeypatch.setattr( routes, "bad", lambda _handler, msg, status=400: {"ok": False, "error": msg, "status": status}, ) result = routes.handle_post(object(), SimpleNamespace(path="/api/model/set")) assert result["status"] == 400 assert "Unknown auxiliary task slot" in result["error"]