Rework AI-Typewriter en application d'arrière-plan multi-profils
- 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).
This commit is contained in:
+67
-38
@@ -1,51 +1,80 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from ai_typewriter.config import load_config
|
||||
from ai_typewriter.config import (
|
||||
ConfigError,
|
||||
ConfigStore,
|
||||
Profile,
|
||||
load_config,
|
||||
math_latex_profile,
|
||||
)
|
||||
|
||||
|
||||
def test_load_default_config(tmp_path):
|
||||
def test_store_creates_defaults_when_missing(tmp_path):
|
||||
path = tmp_path / "nested" / "config.json"
|
||||
store = ConfigStore(path)
|
||||
store.ensure_defaults()
|
||||
|
||||
assert path.exists()
|
||||
assert len(store.profiles) == 2
|
||||
names = [p.name for p in store.profiles]
|
||||
assert "Général" in names
|
||||
assert math_latex_profile().name in names
|
||||
store.load()
|
||||
assert store.active_name in names
|
||||
|
||||
|
||||
def test_default_math_profile_enables_equation_markers():
|
||||
p = math_latex_profile()
|
||||
assert p.equation_enabled is True
|
||||
assert p.eq_start_marker == "[EQ]"
|
||||
assert p.eq_end_marker == "[/EQ]"
|
||||
assert p.eq_start_key == "alt+="
|
||||
assert p.eq_end_key == "right"
|
||||
assert "[EQ]" in p.effective_prompt()
|
||||
|
||||
|
||||
def test_crud_upsert_set_active_and_remove(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "ollama"}), encoding="utf-8")
|
||||
store = ConfigStore(path)
|
||||
store.ensure_defaults()
|
||||
store.load()
|
||||
|
||||
cfg = load_config(path)
|
||||
names_before = len(store.get_all())
|
||||
prof = math_latex_profile(name="MaesProfil")
|
||||
store.upsert(prof)
|
||||
store.set_active("MaesProfil")
|
||||
assert store.active().name == "MaesProfil"
|
||||
assert len(store.get_all()) == names_before + 1
|
||||
|
||||
assert cfg.provider == "ollama"
|
||||
assert cfg.hotkey == "ctrl+alt+a"
|
||||
assert cfg.math_text_format == "plain"
|
||||
assert "Réponds directement" in cfg.system_prompt
|
||||
prof2 = Profile(name="MaesProfil", provider="openai", model="gpt-4o-mini")
|
||||
store.upsert(prof2)
|
||||
assert store.get("MaesProfil").provider == "openai"
|
||||
|
||||
store.remove("MaesProfil")
|
||||
with pytest.raises(KeyError):
|
||||
store.get("MaesProfil")
|
||||
|
||||
|
||||
def test_accept_unicode_format(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "ollama", "math_text_format": "unicode"}), encoding="utf-8")
|
||||
|
||||
cfg = load_config(path)
|
||||
|
||||
assert cfg.math_text_format == "unicode"
|
||||
def test_remove_last_profile_blocked(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.json")
|
||||
store.ensure_defaults()
|
||||
store.load()
|
||||
# Il y a 2 profils par défaut : le premier retrait réussit…
|
||||
store.remove(store.get_all()[0].name)
|
||||
assert len(store.get_all()) == 1
|
||||
# …mais retirer le dernier est interdit.
|
||||
with pytest.raises(ConfigError):
|
||||
store.remove(store.get_all()[0].name)
|
||||
|
||||
|
||||
def test_timeout_zero_disables_timeout(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "ollama", "request_timeout_seconds": 0}), encoding="utf-8")
|
||||
|
||||
cfg = load_config(path)
|
||||
|
||||
assert cfg.request_timeout_seconds is None
|
||||
def test_set_active_unknown_raises(tmp_path):
|
||||
store = ConfigStore(tmp_path / "config.json")
|
||||
store.ensure_defaults()
|
||||
store.load()
|
||||
with pytest.raises(KeyError):
|
||||
store.set_active("inexistant")
|
||||
|
||||
|
||||
def test_reject_invalid_math_text_format(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "ollama", "math_text_format": "bad"}), encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
load_config(path)
|
||||
|
||||
|
||||
def test_reject_invalid_provider(tmp_path):
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(json.dumps({"provider": "bad"}), encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
load_config(path)
|
||||
def test_load_config_return_store(tmp_path):
|
||||
store = load_config(str(tmp_path / "config.json"))
|
||||
assert isinstance(store, ConfigStore)
|
||||
Reference in New Issue
Block a user