Files
pyssh-client/tests/test_ai_command_dialog.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

66 lines
2.4 KiB
Python

"""AICommandDialog : extraction de la commande depuis la reponse de l'IA
(bloc de code s'il y en a un), et les 3 actions possibles (Copier / Inserer
dans le terminal / Annuler) - jamais d'execution automatique."""
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
from PySide6.QtWidgets import QApplication
from app.dialogs.ai_command_dialog import AICommandDialog, extract_command
app = QApplication.instance() or QApplication(sys.argv)
print("=== 1. extract_command() : bloc de code present ===")
response = "Pour lister les gros fichiers :\n\n```bash\nfind / -size +100M\n```\n\nCela peut prendre du temps."
assert extract_command(response) == "find / -size +100M"
print(" OK")
print()
print("=== 2. extract_command() : bloc de code sans langage indique ===")
response = "```\ndf -h\n```"
assert extract_command(response) == "df -h"
print(" OK")
print()
print("=== 3. extract_command() : pas de bloc de code -> reponse entiere ===")
response = "Utilisez simplement : uptime"
assert extract_command(response) == "Utilisez simplement : uptime"
print(" OK")
print()
print("=== 4. Avertissement affiche seulement si la commande est dangereuse ===")
dialog_safe = AICommandDialog("```\ndf -h\n```")
assert dialog_safe.command == "df -h"
dialog_dangerous = AICommandDialog("```\nrm -rf /tmp/x\n```", dangerous_labels=["rm -rf (suppression recursive)"])
assert dialog_dangerous.command == "rm -rf /tmp/x"
print(" OK (pas de verification visuelle automatisee, juste la construction sans exception)")
print()
print("=== 5. 'Copier' place la commande (eventuellement corrigee) dans le presse-papiers ===")
dialog = AICommandDialog("```\necho bonjour\n```")
dialog.command_view.setPlainText("echo modifie")
dialog._copy()
assert QApplication.clipboard().text() == "echo modifie"
print(" OK")
print()
print("=== 6. 'Inserer dans le terminal' : accepte le dialogue, ne l'execute jamais ===")
dialog = AICommandDialog("```\nls -la\n```")
assert dialog.insert_requested is False
dialog._insert()
assert dialog.insert_requested is True
assert dialog.command == "ls -la"
assert dialog.result() == 1 # QDialog.Accepted
print(" OK")
print()
print("=== 7. 'Annuler' : rejette le dialogue, insert_requested reste False ===")
dialog = AICommandDialog("```\nls -la\n```")
dialog.cancel_btn.click()
assert dialog.insert_requested is False
assert dialog.result() == 0 # QDialog.Rejected
print(" OK")
print()
print("AI COMMAND DIALOG : TOUS LES TESTS OK")