Assistant IA : liste deroulante de fournisseurs (dont Ollama, LM Studio)
Retour utilisateur : le champ "Fournisseur" en simple texte libre ne donnait aucune indication de ce qui est possible, notamment pour l'IA locale (Ollama, LM Studio), alors que d'autres preferent un service cloud. Remplace par une QComboBox editable : une liste de raccourcis usuels (OpenAI, OpenRouter, Mistral AI, Ollama local, LM Studio local, Personnalise...) qui pre-remplit l'URL de base au choix - sans jamais empecher de saisir librement un autre fournisseur/proxy compatible OpenAI, conformement a l'exigence d'origine de ne rien figer en dur. Le modele reste toujours en saisie libre, chaque installation Ollama/LM Studio ayant ses propres modeles telecharges. Bonus cible : choisir un prereglage local (Ollama/LM Studio, detecte via localhost/127.0.0.1 dans l'URL) met a jour l'indication du champ cle API pour signaler qu'elle n'est generalement pas necessaire. tests/test_ai_settings_dialog.py (8 verifications, nouveau fichier - aucun test ne couvrait encore AISettingsDialog elle-meme) : presence d'Ollama/LM Studio dans la liste, pre-remplissage de l'URL par prereglage, "Personnalise" qui ne touche pas une URL deja saisie, persistance d'un nom de fournisseur personnalise a travers un enregistrement puis une reouverture. Suite complete revalidee (34/34) et ruff toujours propre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
127
tests/test_ai_settings_dialog.py
Normal file
127
tests/test_ai_settings_dialog.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user