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>
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""Confirmations de securite avant envoi a l'assistant IA : avis de principe
|
|
affiche une seule fois (persist dans app_settings, redirige vers un fichier
|
|
temporaire), et verification des secrets a chaque envoi. QMessageBox est
|
|
simule (exec()/clickedButton() patches) : aucune boite modale reelle ne doit
|
|
bloquer les tests."""
|
|
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 PySide6.QtWidgets import QApplication, QMessageBox
|
|
|
|
import app.app_settings as app_settings
|
|
from app.dialogs.ai_prompts import ensure_ai_notice_acknowledged, confirm_sensitive_content, MASK, SEND, CANCEL
|
|
|
|
app = QApplication.instance() or QApplication(sys.argv)
|
|
|
|
tmp = Path(tempfile.mkdtemp())
|
|
settings_redirect = patch.object(app_settings, "SETTINGS_FILE", tmp / "settings.json")
|
|
settings_redirect.start()
|
|
|
|
|
|
def click(button_text):
|
|
"""Simule un clic sur le bouton `button_text` d'une QMessageBox construite
|
|
avec addButton(), sans faire tourner de boucle d'evenements reelle."""
|
|
def fake_clicked(self):
|
|
for b in self.buttons():
|
|
if b.text() == button_text:
|
|
return b
|
|
return None
|
|
return fake_clicked
|
|
|
|
|
|
print("=== 1. Avis de principe : affiche la premiere fois, refus -> False ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec", return_value=QMessageBox.Cancel) as mocked_exec:
|
|
result = ensure_ai_notice_acknowledged(None)
|
|
assert mocked_exec.called
|
|
assert result is False
|
|
assert app_settings.get_setting("ai_notice_acknowledged", False) is False
|
|
print(" OK")
|
|
|
|
print()
|
|
print("=== 2. Avis de principe : acceptation -> True, persiste ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec", return_value=QMessageBox.Ok) as mocked_exec:
|
|
result = ensure_ai_notice_acknowledged(None)
|
|
assert mocked_exec.called
|
|
assert result is True
|
|
assert app_settings.get_setting("ai_notice_acknowledged", False) is True
|
|
print(" OK")
|
|
|
|
print()
|
|
print("=== 3. Avis de principe : deja accepte -> plus jamais affiche ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec") as mocked_exec:
|
|
result = ensure_ai_notice_acknowledged(None)
|
|
assert not mocked_exec.called, "la boite ne doit plus s'afficher une fois l'accord persiste"
|
|
assert result is True
|
|
print(" OK")
|
|
|
|
print()
|
|
print("=== 4. Aucun secret detecte : SEND direct, aucune boite affichee ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec") as mocked_exec:
|
|
result = confirm_sensitive_content(None, "systemctl status nginx")
|
|
assert not mocked_exec.called
|
|
assert result == SEND
|
|
print(" OK")
|
|
|
|
print()
|
|
print("=== 5. Secret detecte + 'Masquer automatiquement' -> MASK ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec", return_value=0), \
|
|
patch("app.dialogs.ai_prompts.QMessageBox.clickedButton", click("Masquer automatiquement")):
|
|
result = confirm_sensitive_content(None, "password=Sup3rSecret!")
|
|
assert result == MASK
|
|
print(" OK")
|
|
|
|
print()
|
|
print("=== 6. Secret detecte + 'Voir le contenu' -> SEND (envoi tel quel) ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec", return_value=0), \
|
|
patch("app.dialogs.ai_prompts.QMessageBox.clickedButton", click("Voir le contenu")):
|
|
result = confirm_sensitive_content(None, "password=Sup3rSecret!")
|
|
assert result == SEND
|
|
print(" OK")
|
|
|
|
print()
|
|
print("=== 7. Secret detecte + 'Annuler' -> CANCEL ===")
|
|
with patch("app.dialogs.ai_prompts.QMessageBox.exec", return_value=0), \
|
|
patch("app.dialogs.ai_prompts.QMessageBox.clickedButton", click("Annuler")):
|
|
result = confirm_sensitive_content(None, "password=Sup3rSecret!")
|
|
assert result == CANCEL
|
|
print(" OK")
|
|
|
|
settings_redirect.stop()
|
|
|
|
print()
|
|
print("AI PROMPTS : TOUS LES TESTS OK")
|