| 1 | from pathlib import Path |
| 2 | |
| 3 | |
| 4 | PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| 5 | |
| 6 | |
| 7 | def read(*parts: str) -> str: |
| 8 | return PROJECT_ROOT.joinpath(*parts).read_text(encoding="utf-8") |
| 9 | |
| 10 | |
| 11 | def test_model_config_text_buttons_have_shared_primitive() -> None: |
| 12 | buttons_css = read("webui", "css", "buttons.css") |
| 13 | preset_modal = read("plugins", "_model_config", "webui", "main.html") |
| 14 | overview = read("plugins", "_model_config", "webui", "preset-overview.html") |
| 15 | |
| 16 | assert ".text-button {" in buttons_css |
| 17 | assert "appearance: none;" in buttons_css |
| 18 | assert ".text-button:hover:not(:disabled)" in buttons_css |
| 19 | assert ".text-button .material-symbols-outlined" in buttons_css |
| 20 | assert 'class="text-button"' in preset_modal |
| 21 | assert 'class="model-preset-actions"' in overview |
| 22 | |
| 23 | |
| 24 | def test_model_preset_rows_keep_stable_identity_after_middle_delete() -> None: |
| 25 | preset_modal = read("plugins", "_model_config", "webui", "main.html") |
| 26 | preset_store = read("plugins", "_model_config", "webui", "model-config-store.js") |
| 27 | |
| 28 | assert 'x-data="$store.modelConfig.createPresetEditor($store.modelConfig.presetEditorInitialName)"' in preset_modal |
| 29 | assert ':key="preset._key"' in preset_modal |
| 30 | assert 'x-model.number="selectedKey"' in preset_modal |
| 31 | assert "createPresetEditor(initialName = '')" in preset_store |
| 32 | assert "preparePresetEditor($store.chats?.selected || '')" in preset_modal |
| 33 | assert "_key: nextPresetKey++" in preset_store |
| 34 | assert "removeSelectedPreset()" in preset_store |
| 35 | assert "selectedKey" in preset_store |
| 36 | assert ':key="idx"' not in preset_modal |
| 37 | assert "JSON.parse(JSON.stringify" not in preset_modal |
| 38 | assert "presets.splice" not in preset_store |
| 39 | |
| 40 | |
| 41 | def test_model_preset_editor_can_reset_to_bundled_defaults() -> None: |
| 42 | preset_modal = read("plugins", "_model_config", "webui", "main.html") |
| 43 | preset_store = read("plugins", "_model_config", "webui", "model-config-store.js") |
| 44 | |
| 45 | assert '$confirmClick($event, () => resetPresets())' in preset_modal |
| 46 | assert "Restore bundled presets" in preset_modal |
| 47 | assert "async resetPresets()" in preset_store |
| 48 | assert "if (!await store.resetGlobalPresets()) return;" in preset_store |
| 49 | assert "this.refreshPresets();" in preset_store |
| 50 | |
| 51 | |
| 52 | def test_default_preset_is_locked_and_shared_overview_is_reused() -> None: |
| 53 | preset_modal = read("plugins", "_model_config", "webui", "main.html") |
| 54 | config_modal = read("plugins", "_model_config", "webui", "config.html") |
| 55 | settings_summary = read("plugins", "_model_config", "webui", "models-summary.html") |
| 56 | helper = read("plugins", "_model_config", "helpers", "model_config.py") |
| 57 | |
| 58 | assert '<template x-if="canRenameSelected">' in preset_modal |
| 59 | assert "get canRenameSelected()" in read( |
| 60 | "plugins", "_model_config", "webui", "model-config-store.js" |
| 61 | ) |
| 62 | assert "Default cannot be renamed or deleted." not in preset_modal |
| 63 | assert ":readonly=" not in preset_modal |
| 64 | assert "The Default preset cannot be deleted or renamed." in helper |
| 65 | assert "preserveImplicitDefaults" in read( |
| 66 | "plugins", "_model_config", "webui", "model-config-store.js" |
| 67 | ) |
| 68 | component_path = "/plugins/_model_config/webui/preset-overview.html" |
| 69 | assert component_path in config_modal |
| 70 | assert component_path in settings_summary |
| 71 | assert "Per-project / agent" in read( |
| 72 | "plugins", "_model_config", "webui", "preset-overview.html" |
| 73 | ) |
| 74 | |
| 75 | |
| 76 | def test_preset_editor_uses_standard_modal_footer_buttons() -> None: |
| 77 | preset_modal = read("plugins", "_model_config", "webui", "main.html") |
| 78 | api_keys_modal = read("plugins", "_model_config", "webui", "api-keys.html") |
| 79 | |
| 80 | for content in (preset_modal, api_keys_modal): |
| 81 | assert '<div class="modal-footer" data-modal-footer>' in content |
| 82 | assert 'class="btn btn-ok"' in content |
| 83 | assert 'class="btn btn-cancel"' in content |
| 84 | |
| 85 | assert "preset-editor-footer" not in preset_modal |
| 86 | |
| 87 | |
| 88 | def test_preset_editor_nests_one_conditional_vision_selector_in_main() -> None: |
| 89 | preset_modal = read("plugins", "_model_config", "webui", "main.html") |
| 90 | model_field = read("plugins", "_model_config", "webui", "model-field.html") |
| 91 | preset_overview = read("plugins", "_model_config", "webui", "preset-overview.html") |
| 92 | preset_store = read("plugins", "_model_config", "webui", "model-config-store.js") |
| 93 | |
| 94 | main_start = preset_modal.index('<div class="section-title">Main Model</div>') |
| 95 | utility_start = preset_modal.index('<div class="section-title">Utility Model</div>') |
| 96 | selector_start = preset_modal.index('class="vision-sidecar-selector"') |
| 97 | supports_start = model_field.index('<div class="field-title">Supports Vision</div>') |
| 98 | override_start = model_field.index('<div class="field-title">Use separate Vision Model</div>') |
| 99 | context_start = model_field.index('<div class="field-title">Context window size</div>') |
| 100 | advanced_start = model_field.index('<!-- Advanced Settings (collapsed by default) -->') |
| 101 | vision_advanced_start = model_field.index('<template x-if="modelType === \'vision\'">') |
| 102 | vision_advanced_end = model_field.index('<!-- Utility-specific: ctx_input slider -->') |
| 103 | vision_advanced = model_field[vision_advanced_start:vision_advanced_end] |
| 104 | |
| 105 | assert '<div class="section-title">Vision Model</div>' not in preset_modal |
| 106 | assert preset_modal.count('class="vision-sidecar-selector"') == 1 |
| 107 | assert main_start < selector_start < utility_start |
| 108 | assert '!selectedPreset.chat.vision || selectedPreset.vision.override_main' in preset_modal |
| 109 | assert "get visionModel() { return selectedPreset.vision; }" in preset_modal |
| 110 | assert "modelType: 'vision'" in preset_modal |
| 111 | assert preset_modal.count("apiKeyMode: 'store'") == 4 |
| 112 | assert "margin: 0.75rem 0 0;" in preset_modal |
| 113 | assert "padding: 0.25rem 0 0;" in preset_modal |
| 114 | assert "border-left: 2px solid var(--color-border);" not in preset_modal |
| 115 | assert "Use separate Vision Model" in model_field |
| 116 | assert supports_start < override_start < context_start < advanced_start |
| 117 | assert "When disabled, vision_load uses this model's native vision." in model_field |
| 118 | assert "When enabled, vision_load uses the preset's Vision Model" not in model_field |
| 119 | assert 'x-model="visionModel.override_main"' in model_field |
| 120 | assert '<div class="advanced-section" x-data="{ advOpen: false }">' in model_field |
| 121 | assert advanced_start < vision_advanced_start |
| 122 | assert model_field.count('<div class="field-title">Timeout (seconds)</div>') == 1 |
| 123 | assert model_field.count('<div class="field-title">Max tokens</div>') == 1 |
| 124 | assert 'x-model.number="model.timeout"' in vision_advanced |
| 125 | assert 'x-model.number="model.max_tokens"' in vision_advanced |
| 126 | assert "How long to wait for the Vision Model." in vision_advanced |
| 127 | assert "Maximum output tokens for each delegated vision call." in vision_advanced |
| 128 | assert "Agent Editor > Advanced > Prompt files" in vision_advanced |
| 129 | assert "fw.vision_load.md" in vision_advanced |
| 130 | assert "delegated_system" not in model_field |
| 131 | assert "vision prompt" not in model_field[:vision_advanced_start].lower() |
| 132 | assert "timeout: 300" in preset_store |
| 133 | assert "max_tokens: 2000" in preset_store |
| 134 | assert "value?.timeout ?? kwargs.timeout ?? 300" in preset_store |
| 135 | assert "value?.max_tokens ?? kwargs.max_tokens ?? 2000" in preset_store |
| 136 | assert "delete kwargs.timeout;" in preset_store |
| 137 | assert "delete kwargs.max_tokens;" in preset_store |
| 138 | assert "_kwargs_text: kwargsToText(vision.kwargs)" in preset_store |
| 139 | assert "model-preset-row-nested" in preset_overview |
| 140 | assert "model.title === 'Vision'" in preset_overview |
| 141 | assert preset_overview.count("model.title !== 'Vision'") == 2 |
| 142 | assert '<span class="model-preset-role">Vision override</span>' in preset_overview |
| 143 | assert "'model-preset-identity-vision': model.title === 'Vision'" in preset_overview |
| 144 | assert "grid-column: 3;" in preset_overview |
| 145 | assert "padding-top: var(--spacing-xs);" in preset_overview |
| 146 | assert "margin-left: 2rem;" not in preset_overview |
| 147 | assert "border-left: 1px solid var(--color-border);" not in preset_overview |
| 148 | assert "if (slotKey === 'vision') config[sectionKey] = {};" in preset_store |
| 149 | assert "['chat', 'vision', 'utility']" in preset_store |
| 150 | |
| 151 | |
| 152 | def test_plugin_settings_reset_is_explicit_and_does_not_capture_toast_early() -> None: |
| 153 | settings_modal = read("webui", "components", "plugins", "plugin-settings.html") |
| 154 | settings_store = read("webui", "components", "plugins", "plugin-settings-store.js") |
| 155 | |
| 156 | assert "Reset to default" in settings_modal |
| 157 | assert "const justToast = globalThis.justToast" not in settings_store |
| 158 | assert 'globalThis.justToast?.("Settings reset to default.", "info")' in settings_store |