- Rollback gestion d'équations : écriture littérale caractère par caractère (suppression de math_format.py et de la normalisation Unicode). - Nouveau profil par défaut 'Mathématiques (LaTeX)' : le modèle émet du LaTeX encadré [EQ]...[/EQ], intercepté par KeyStepper qui déclenche Alt+= en début d'équation et -> en fin. - Config multi-profils dans %APPDATA%/ai-typewriter/config.json (ConfigStore + profils par défaut auto-créés). - Icône de zone de notification (pystray) : Ouvrir les logs (temps réel), Ajouter/Modifier un profil, Gérer l'authentification, Quitter. - Sélecteur de modèles : modèles locaux Ollama + recherche/téléchargement depuis la bibliothèque publique ; fournisseurs tiers (OpenAI, OpenRouter, Gemini, custom). - Clés d'API stockées de façon sécurisée dans le Gestionnaire d'identifiants Windows via keyring (jamais en clair dans config.json). - Tests : 44 tests verts (stepper, profils, clients IA, credentials, catalogue de modèles, moteur).
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""Tests du moteur : capture -> IA -> dactylographie (tout mocké)."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from ai_typewriter.config import ConfigStore
|
|
from ai_typewriter.engine import AITypewriterEngine
|
|
|
|
|
|
def _make_store(tmp_path) -> ConfigStore:
|
|
store = ConfigStore(tmp_path / "config.json")
|
|
store.ensure_defaults()
|
|
store.load()
|
|
return store
|
|
|
|
|
|
def _fake_keyboard(monkeypatch, typed, sent):
|
|
hook = {}
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.key_stepper.keyboard.write",
|
|
lambda char, delay=0, exact=True: typed.append(char),
|
|
)
|
|
monkeypatch.setattr("ai_typewriter.key_stepper.keyboard.send", lambda v: sent.append(v))
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.key_stepper.keyboard.hook",
|
|
lambda callback, suppress=True: hook.update({"callback": callback}) or "hook",
|
|
)
|
|
monkeypatch.setattr("ai_typewriter.key_stepper.keyboard.unhook", lambda v: None)
|
|
return hook
|
|
|
|
|
|
def test_capture_ask_and_step_types_response(monkeypatch, tmp_path):
|
|
store = _make_store(tmp_path)
|
|
engine = AITypewriterEngine(store)
|
|
typed, sent = [], []
|
|
hook = _fake_keyboard(monkeypatch, typed, sent)
|
|
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.engine.capture_clipboard", lambda: "Question de test"
|
|
)
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.engine.ask_ai", lambda prompt, profile, store: "Réponse IA"
|
|
)
|
|
|
|
engine.capture_ask_and_step()
|
|
assert engine._stepper is not None
|
|
hook["callback"](SimpleNamespace(event_type="down"))
|
|
hook["callback"](SimpleNamespace(event_type="down"))
|
|
assert typed == ["R", "é"]
|
|
|
|
|
|
def test_capture_ask_and_step_with_equations(monkeypatch, tmp_path):
|
|
store = _make_store(tmp_path)
|
|
store.set_active("Mathématiques (LaTeX)")
|
|
engine = AITypewriterEngine(store)
|
|
typed, sent = [], []
|
|
hook = _fake_keyboard(monkeypatch, typed, sent)
|
|
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.engine.capture_clipboard", lambda: "Calcule"
|
|
)
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.engine.ask_ai", lambda prompt, profile, store: r"[EQ]a=x[/EQ]"
|
|
)
|
|
|
|
engine.capture_ask_and_step()
|
|
# 1:[EQ]->alt+= ; a: char ; =: char ; x: char ; [/EQ]->right => 5 actions
|
|
for _ in range(5):
|
|
hook["callback"](SimpleNamespace(event_type="down"))
|
|
|
|
assert sent == ["alt+=", "right"]
|
|
assert typed == ["a", "=", "x"]
|
|
|
|
|
|
def test_empty_clipboard_does_not_ask(monkeypatch, tmp_path):
|
|
store = _make_store(tmp_path)
|
|
engine = AITypewriterEngine(store)
|
|
called = []
|
|
monkeypatch.setattr("ai_typewriter.engine.capture_clipboard", lambda: " ")
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.engine.ask_ai",
|
|
lambda prompt, profile, store: called.append(prompt) or "x",
|
|
)
|
|
engine.capture_ask_and_step()
|
|
assert called == []
|
|
|
|
|
|
def test_ask_only_returns_answer(monkeypatch, tmp_path):
|
|
store = _make_store(tmp_path)
|
|
engine = AITypewriterEngine(store)
|
|
monkeypatch.setattr(
|
|
"ai_typewriter.engine.ask_ai",
|
|
lambda prompt, profile, store: "reponse-brute",
|
|
)
|
|
assert engine.ask_only("bonjour") == "reponse-brute"
|
|
|
|
|
|
def test_concurrent_hotkey_ignored_while_busy(monkeypatch, tmp_path):
|
|
store = _make_store(tmp_path)
|
|
engine = AITypewriterEngine(store)
|
|
|
|
# Occupe le verrou
|
|
assert engine._busy.acquire(blocking=False) is True
|
|
calls = []
|
|
engine.handle_hotkey() # doit être ignoré (busy)
|
|
assert calls == []
|
|
engine._busy.release() |