Files
pyssh-client/tests/test_ai_settings.py
Florian 0b2ff38bfd Assistant IA optionnel (analyse de sortie, diagnostic, generation de commande)
Nouvelle couche app/ai/ (provider OpenAI-compatible via urllib, redaction de
secrets, detection de commandes dangereuses, parametres, worker QThread) et son
integration dans chaque onglet terminal via un bouton "Assistant IA" (memes
conventions que le bouton Snippets existant) :

- Expliquer cette sortie / Diagnostiquer cette erreur / Proposer une solution /
  Poser une question... : utilisent la selection courante du terminal, affichees
  dans un panneau lateral (memoire uniquement, jamais journalise) reutilisant le
  splitter comme le panneau SFTP.
- Generer une commande : ne s'execute jamais automatiquement (Copier / Inserer
  dans le terminal sans Entree / Annuler), avec avertissement si la commande
  proposee correspond a un motif destructeur connu.
- Avant tout envoi : avis unique (persist) puis detection de secrets evidents
  (mot de passe, cle, jeton) avec choix de masquer, envoyer tel quel ou annuler.

Desactive par defaut, sans liste de fournisseur figee (URL + modele libres,
compatible OpenAI/OpenRouter/LLM local). La cle API passe exclusivement par
CredentialStore (jamais dans settings.json ni journalisee). Parametrable via
le nouveau menu "Assistant IA > Parametres..." (bouton "Tester la connexion").

Bug trouve par les tests avant commit : un panneau nouvellement insere dans un
QSplitter deja visible est affiche automatiquement par Qt, ce qui inversait le
premier clic sur "Afficher le panneau IA" ; corrige en le masquant explicitement
a sa creation.

9 nouveaux fichiers de test (provider, redaction, dangers, parametres, worker,
confirmations de securite, dialogue de commande, panneau, integration onglet) :
39 verifications, toutes au vert. Suite complete (31 fichiers) executee sans
regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 05:48:13 +02:00

100 lines
3.3 KiB
Python

"""Configuration de l'assistant IA : parametres non sensibles persistes via
app_settings (redirige vers un fichier temporaire pour ne jamais toucher les
vrais reglages), cle API exclusivement via CredentialStore, et construction du
fournisseur a partir des deux."""
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
import tempfile
from pathlib import Path
from unittest.mock import patch
from app.credentials import CredentialStore
from app.ai.settings import (
AISettings, load_ai_settings, save_ai_settings, load_api_key, save_api_key,
build_provider_from_settings, AI_API_KEY_CREDENTIAL, DEFAULT_BASE_URL,
)
from app.ai.provider import OpenAICompatibleProvider
import app.app_settings as app_settings
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)
tmp = Path(tempfile.mkdtemp())
settings_redirect = patch.object(app_settings, "SETTINGS_FILE", tmp / "settings.json")
settings_redirect.start()
print("=== 1. Valeurs par defaut : desactive, pas de secret expose ===")
default = load_ai_settings()
print(" ", default)
assert default.enabled is False
assert default.base_url == DEFAULT_BASE_URL
print(" OK")
print()
print("=== 2. Sauvegarde/rechargement des parametres non sensibles ===")
settings = AISettings(enabled=True, provider_name="OpenRouter", base_url="https://openrouter.ai/api/v1",
model="anthropic/claude", timeout=45.0, temperature=0.7)
save_ai_settings(settings)
reloaded = load_ai_settings()
print(" ", reloaded)
assert reloaded == settings
print(" OK")
print()
print("=== 3. La cle API ne transite jamais par app_settings (verification du fichier disque) ===")
raw = (tmp / "settings.json").read_text(encoding="utf-8")
assert "sk-" not in raw
assert "api_key" not in raw.lower() or "ai_api" not in raw.lower() # aucune cle presente, seul le nom du champ pourrait
print(" contenu persiste:", raw)
print(" OK")
print()
print("=== 4. Cle API : lecture/ecriture via CredentialStore uniquement ===")
store = FakeCredentialStore()
assert load_api_key(store) == ""
save_api_key(store, "sk-test-secret")
assert load_api_key(store) == "sk-test-secret"
assert store.get_secret(AI_API_KEY_CREDENTIAL) == "sk-test-secret"
save_api_key(store, "") # vider la cle -> supprime l'entree
assert load_api_key(store) == ""
print(" OK")
print()
print("=== 5. build_provider_from_settings() : None si desactive ===")
save_ai_settings(AISettings(enabled=False))
assert build_provider_from_settings(store) is None
print(" OK")
print()
print("=== 6. build_provider_from_settings() : fournisseur pret si active ===")
save_ai_settings(AISettings(enabled=True, base_url="https://api.example.com/v1", model="gpt-x"))
save_api_key(store, "sk-live")
provider = build_provider_from_settings(store)
assert isinstance(provider, OpenAICompatibleProvider)
assert provider.base_url == "https://api.example.com/v1"
assert provider.model == "gpt-x"
assert provider.api_key == "sk-live"
print(" OK")
settings_redirect.stop()
print()
print("AI SETTINGS : TOUS LES TESTS OK")