"""Integration de l'assistant IA dans un onglet terminal : menu (active/desactive selon la selection), panneau lateral cree a la demande dans le splitter (meme mecanisme que le panneau SFTP), et generation de commande (jamais executee automatiquement, avec avertissement si dangereuse). LocalTerminalConnection.start() est neutralisee comme dans test_snippets.py : aucun sous-processus reel, send() depose seulement les octets dans une file.""" import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) from unittest.mock import patch from PySide6.QtCore import QCoreApplication from PySide6.QtWidgets import QApplication from app.ai.provider import AIProvider from app.dialogs.ai_prompts import SEND from app.local_terminal import LocalTerminalConnection from app.ui.ai_panel import AIPanel from app.ui.tabs import LocalTerminalTabWidget app = QApplication.instance() or QApplication(sys.argv) NOTICE_OK = patch("app.ui.ai_panel.ensure_ai_notice_acknowledged", return_value=True) SEND_ASIS = patch("app.ui.ai_panel.confirm_sensitive_content", return_value=SEND) PROMPTS_OK_IN_TABS = patch("app.ui.tabs.ensure_ai_notice_acknowledged", return_value=True) SEND_ASIS_IN_TABS = patch("app.ui.tabs.confirm_sensitive_content", return_value=SEND) class FakeProvider(AIProvider): def __init__(self, reply="reponse"): self.reply = reply def chat(self, messages, *, temperature=None): return self.reply def pump(ms=3000): import time deadline = time.time() + ms / 1000 while time.time() < deadline: QCoreApplication.processEvents() time.sleep(0.01) def make_tab(ai_provider_factory=None): return LocalTerminalTabWidget(ai_provider_factory=ai_provider_factory) print("=== 1. Menu IA : actions desactivees sur un terminal vraiment vide (rien affiche, rien selectionne) ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): tab = make_tab() assert not tab.terminal.has_selection() assert tab.terminal.visible_text() == "" menu = tab._build_ai_menu() labels = [a.text() for a in menu.actions() if a.text()] print(" actions:", labels) assert "Expliquer cette sortie" in labels assert "Generer une commande..." in labels for text in ("Expliquer cette sortie", "Diagnostiquer cette erreur", "Proposer une solution"): action = next(a for a in menu.actions() if a.text() == text) assert not action.isEnabled(), f"{text} devrait etre desactive : rien a analyser" # "Poser une question" reste utilisable sans rien : le contexte est optionnel ask_action = next(a for a in menu.actions() if a.text() == "Poser une question...") assert ask_action.isEnabled() generate_action = next(a for a in menu.actions() if a.text() == "Generer une commande...") assert generate_action.isEnabled(), "Generer une commande ne necessite pas de selection" tab.close_connection() print(" OK") print() print("=== 2. Menu IA : actions activees des qu'il y a du texte visible, meme sans selection ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): tab = make_tab() tab.terminal.feed(b"erreur : connection refused\r\n") assert not tab.terminal.has_selection() assert tab.terminal.visible_text().strip() menu = tab._build_ai_menu() for text in ("Expliquer cette sortie", "Diagnostiquer cette erreur", "Proposer une solution", "Poser une question..."): action = next(a for a in menu.actions() if a.text() == text) assert action.isEnabled(), f"{text} devrait etre active : l'ecran visible sert de repli" tab.close_connection() print(" OK") print() print("=== 2bis. 'Expliquer cette sortie' sans selection : se rabat sur l'ecran visible ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): provider = FakeProvider(reply="Ceci indique une connexion refusee.") tab = make_tab(ai_provider_factory=lambda: provider) tab.terminal.feed(b"erreur : connection refused\r\n") assert not tab.terminal.has_selection(), "ce test verifie precisement le cas SANS selection" with NOTICE_OK, SEND_ASIS: tab._ai_explain() pump() assert isinstance(tab.ai_panel, AIPanel) conversation = tab.ai_panel.conversation.toPlainText() print(" conversation:", conversation) assert "connection refused" in conversation.lower() # le texte envoye est bien celui de l'ecran assert "connexion refusee" in conversation.lower() # et la reponse simulee est bien affichee tab.close_connection() print(" OK") print() print("=== 2ter. Une selection existante reste prioritaire sur l'ecran visible ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): provider = FakeProvider(reply="reponse") tab = make_tab(ai_provider_factory=lambda: provider) tab.terminal.feed(b"ligne 1 sans rapport\r\nerreur precise\r\n") tab.terminal._sel_anchor, tab.terminal._sel_focus = (0, 1), (13, 1) assert tab.terminal.selected_text() == "erreur precise" with NOTICE_OK, SEND_ASIS: tab._ai_explain() pump() conversation = tab.ai_panel.conversation.toPlainText() print(" conversation:", conversation) assert "erreur precise" in conversation.lower() assert "ligne 1 sans rapport" not in conversation.lower() tab.close_connection() print(" OK") print() print("=== 3. 'Expliquer cette sortie' : cree le panneau IA dans le splitter et lance la requete ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): provider = FakeProvider(reply="Ceci indique une connexion refusee.") tab = make_tab(ai_provider_factory=lambda: provider) tab.terminal.feed(b"erreur : connection refused\r\n") tab.terminal._sel_anchor, tab.terminal._sel_focus = (0, 0), (10, 0) assert tab.ai_panel is None with NOTICE_OK, SEND_ASIS: tab._ai_explain() pump() assert isinstance(tab.ai_panel, AIPanel) assert tab.splitter.indexOf(tab.ai_panel) >= 0 assert "connexion refusee" in tab.ai_panel.conversation.toPlainText().lower() tab.close_connection() print(" OK") print() print("=== 4. Basculer l'affichage du panneau IA (menu) ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): tab = make_tab(ai_provider_factory=lambda: FakeProvider()) tab.show() # isVisible() se compose avec les ancetres : il en faut un reellement affiche tab._toggle_ai_panel() # premiere fois : cree + affiche assert tab.ai_panel is not None and tab.ai_panel.isVisible() tab._toggle_ai_panel() # deuxieme fois : masque assert not tab.ai_panel.isVisible() tab.close_connection() print(" OK") print() print("=== 5. Generer une commande : jamais auto-execute, insertion sans Entree ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): provider = FakeProvider(reply="Pour lister les gros fichiers :\n\n```bash\nfind / -size +100M\n```") tab = make_tab(ai_provider_factory=lambda: provider) with PROMPTS_OK_IN_TABS, SEND_ASIS_IN_TABS, \ patch("app.ui.tabs.QInputDialog.getMultiLineText", return_value=("trouver les gros fichiers", True)), \ patch("app.ui.tabs.AICommandDialog.exec", lambda self: (setattr(self, "insert_requested", True), 1)[1]): tab._ai_generate_command() pump() sent = tab.connection._out_queue.get_nowait() print(" commande inseree:", sent) assert sent == b"find / -size +100M" assert not sent.endswith(b"\r") and not sent.endswith(b"\n") assert tab.ai_btn.isEnabled() and tab.ai_btn.text() == "🤖 Assistant IA" tab.close_connection() print(" OK") print() print("=== 6. Generer une commande : annulee par l'utilisateur -> rien envoye ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): provider = FakeProvider(reply="```bash\nrm -rf /\n```") tab = make_tab(ai_provider_factory=lambda: provider) with patch("app.ui.tabs.QInputDialog.getMultiLineText", return_value=("", False)): tab._ai_generate_command() assert tab.connection._out_queue.empty() tab.close_connection() print(" OK") print() print("=== 7. Sans fournisseur configure : message clair, pas de boite de saisie ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): tab = make_tab(ai_provider_factory=lambda: None) with patch("app.ui.tabs.QInputDialog.getMultiLineText") as mocked_input, \ patch("app.ui.tabs.QMessageBox.information") as mocked_info: tab._ai_generate_command() assert not mocked_input.called assert mocked_info.called tab.close_connection() print(" OK") print() print("=== 8. Fermeture de l'onglet : le panneau IA est arrete proprement ===") with patch.object(LocalTerminalConnection, "start", lambda self: None): tab = make_tab(ai_provider_factory=lambda: FakeProvider()) tab._toggle_ai_panel() tab.close_connection() # ne doit pas lever, ni bloquer indefiniment print(" OK") print() print("AI TAB INTEGRATION : TOUS LES TESTS OK")