"""AISettingsDialog : liste deroulante de fournisseurs (raccourcis usuels, dont des LLM locaux comme Ollama/LM Studio) qui pre-remplit l'URL de base sans jamais empecher de saisir un fournisseur personnalise.""" import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) import tempfile from pathlib import Path from unittest.mock import patch from PySide6.QtWidgets import QApplication import app.app_settings as app_settings from app.ai.settings import load_ai_settings from app.credentials import CredentialStore from app.dialogs.ai_settings_dialog import PROVIDER_PRESETS, AISettingsDialog app = QApplication.instance() or QApplication(sys.argv) class FakeCredentialStore(CredentialStore): def __init__(self): self._data = {} def is_available(self): return True def set_secret(self, key, secret): self._data[key] = secret return True def get_secret(self, key): return self._data.get(key) def delete_secret(self, key): self._data.pop(key, None) def select_preset(dialog, label: str) -> int: """Simule un choix reel dans la liste (pas une saisie libre) : Qt met a jour l'index courant ET emet `activated` lors d'un clic - setCurrentIndex() seul n'emet pas `activated`, il faut donc reproduire les deux.""" index = dialog.provider_combo.findText(label) assert index >= 0, f"prereglage introuvable : {label}" dialog.provider_combo.setCurrentIndex(index) dialog.provider_combo.activated.emit(index) return index tmp = Path(tempfile.mkdtemp()) settings_redirect = patch.object(app_settings, "SETTINGS_FILE", tmp / "settings.json") settings_redirect.start() print("=== 1. La liste propose des fournisseurs cloud ET des LLM locaux ===") store = FakeCredentialStore() dialog = AISettingsDialog(credential_store=store) labels = [dialog.provider_combo.itemText(i) for i in range(dialog.provider_combo.count())] print(" fournisseurs proposes:", labels) assert "OpenAI" in labels assert "Ollama (local)" in labels assert "LM Studio (local)" in labels assert "Personnalise..." in labels print(" OK") print() print("=== 2. Choisir 'Ollama (local)' pre-remplit l'URL de base ===") dialog.base_url_edit.setText("") select_preset(dialog, "Ollama (local)") print(" URL de base:", dialog.base_url_edit.text()) assert dialog.base_url_edit.text() == "http://localhost:11434/v1" print(" OK") print() print("=== 3. ... et signale que la cle API n'est generalement pas necessaire ===") print(" placeholder cle API:", dialog.api_key_edit.placeholderText()) assert "inutile" in dialog.api_key_edit.placeholderText().lower() print(" OK") print() print("=== 4. Choisir un fournisseur cloud restaure le message par defaut pour la cle API ===") select_preset(dialog, "OpenAI") assert dialog.base_url_edit.text() == "https://api.openai.com/v1" print(" placeholder cle API:", dialog.api_key_edit.placeholderText()) assert "inutile" not in dialog.api_key_edit.placeholderText().lower() print(" OK") print() print("=== 5. 'Personnalise...' ne touche pas a une URL de base deja saisie ===") dialog.base_url_edit.setText("https://mon-proxy-interne.example.com/v1") select_preset(dialog, "Personnalise...") print(" URL de base:", dialog.base_url_edit.text()) assert dialog.base_url_edit.text() == "https://mon-proxy-interne.example.com/v1" print(" OK") print() print("=== 6. Le champ reste editable : un nom personnalise est bien conserve a l'enregistrement ===") dialog.provider_combo.setCurrentText("Mon LLM interne") dialog.model_edit.setText("mon-modele") dialog._on_accept() reloaded = load_ai_settings() print(" fournisseur enregistre:", reloaded.provider_name) assert reloaded.provider_name == "Mon LLM interne" assert reloaded.base_url == "https://mon-proxy-interne.example.com/v1" print(" OK") print() print("=== 7. Rouvrir le dialogue recharge le fournisseur personnalise precedemment saisi ===") dialog2 = AISettingsDialog(credential_store=store) print(" fournisseur affiche:", dialog2.provider_combo.currentText()) assert dialog2.provider_combo.currentText() == "Mon LLM interne" print(" OK") print() print("=== 8. Les prereglages restent coherents avec la liste affichee (pas de doublon, tous non vides) ===") seen = set() for label, _url in PROVIDER_PRESETS: assert label not in seen, f"doublon dans PROVIDER_PRESETS : {label}" seen.add(label) assert label.strip(), "un prereglage ne doit jamais avoir un nom vide" print(" OK") settings_redirect.stop() print() print("AI SETTINGS DIALOG : TOUS LES TESTS OK")