All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
- Gamme officielle : Essentielle (Pro) = deepseek-v4-flash, glm-5.3-flash, minimax-m3 ; Premium (Business) = claude-sonnet-5, deepseek-v4-pro, glm-5.3 - Routage reel : le modele employe suit le palier IA du forfait (reglages admin > defaut du plan), garde anti-croisement de palier - Nouvelle page admin « Modeles & abonnements » (matrice forfaits/paliers, modele par defaut, catalogue OpenRouter) - Statistiques enrichies : revenus (30 j + total), MRR estime, credits, liste d'attente, paliers utilises - Nouvelle page admin « Marketing » : audiences avec compteurs, apercu, envoi test obligatoire, historique, desabonnement public + en-tetes List-Unsubscribe ; .gitignore pour les donnees d'execution - Interface (accueil + tarifs) alignee sur la gamme, 13 langues completes - Tests : routage par palier (fonction + tache), marketing, revenus en base
153 lines
5.7 KiB
Python
153 lines
5.7 KiB
Python
"""
|
|
Intégration : ``_run_translation_job`` route bien le modèle par palier.
|
|
|
|
Un faux fournisseur OpenAI-compatible capture le paramètre ``model`` reçu du
|
|
worker ; ``load_settings`` (réglages admin) est substitué. On vérifie que :
|
|
- provider="openrouter" → modèle Essentielle (réglages > plan) ;
|
|
- provider="openrouter_premium" → modèle Premium ;
|
|
- un ``ai_tiers.essential`` personnalisé est honoré sans redéploiement ;
|
|
- un ``ai_tiers.essential`` contenant un modèle Premium est corrigé à l'exécution.
|
|
"""
|
|
|
|
import asyncio
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
import routes.translate_routes as tr
|
|
|
|
|
|
@pytest.fixture
|
|
def minimal_xlsx(tmp_path):
|
|
try:
|
|
import openpyxl
|
|
|
|
wb = openpyxl.Workbook()
|
|
wb.active["A1"] = "Hello"
|
|
p = tmp_path / "minimal.xlsx"
|
|
wb.save(p)
|
|
return p
|
|
except ImportError:
|
|
pytest.skip("openpyxl required")
|
|
|
|
|
|
def _settings(ai_tiers: dict, api_key: str = "sk-or-test"):
|
|
from routes.admin_routes import SettingsConfig
|
|
|
|
return SettingsConfig.model_validate(
|
|
{
|
|
"openrouter": {"api_key": api_key},
|
|
"openrouter_premium": {"api_key": api_key},
|
|
"ai_tiers": ai_tiers,
|
|
}
|
|
)
|
|
|
|
|
|
def _register_job(job_id: str) -> None:
|
|
tr._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "processing",
|
|
"progress_percent": 0,
|
|
"current_step": "",
|
|
"provider": "openrouter",
|
|
"created_at": "2026-09-05T00:00:00+00:00",
|
|
}
|
|
|
|
|
|
def _run_job(monkeypatch, tmp_path, minimal_xlsx, admin_settings, provider: str,
|
|
user_plan: str) -> list:
|
|
"""Lance _run_translation_job avec des faux traducteurs/fournisseurs et
|
|
renvoie la liste des modèles capturés par le faux fournisseur."""
|
|
import routes.admin_routes as admin_routes_mod
|
|
import services.providers.openai_provider as openai_provider_mod
|
|
|
|
captured: list = []
|
|
|
|
class FakeProvider:
|
|
def __init__(self, api_key=None, model=None, base_url=None, timeout=60, **kwargs):
|
|
self.model = model
|
|
self._model = model
|
|
captured.append(model)
|
|
|
|
class FakeExcelTranslator:
|
|
def __init__(self, provider=None):
|
|
self.provider = provider
|
|
|
|
def translate_file(self, input_path, output_path, target_lang,
|
|
source_lang="auto", progress_callback=None,
|
|
translate_images=False, **kwargs):
|
|
out = Path(output_path)
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_bytes(minimal_xlsx.read_bytes())
|
|
|
|
def get_translation_stats(self):
|
|
return {"attempted": 1, "changed": 1}
|
|
|
|
monkeypatch.setattr(openai_provider_mod, "OpenAITranslationProvider", FakeProvider)
|
|
monkeypatch.setattr(tr, "ExcelTranslator", FakeExcelTranslator)
|
|
monkeypatch.setattr(
|
|
admin_routes_mod, "load_settings", lambda: admin_settings, raising=True
|
|
)
|
|
|
|
_register_job("job-test")
|
|
try:
|
|
asyncio.run(
|
|
tr._run_translation_job(
|
|
job_id="job-test",
|
|
input_path=Path(minimal_xlsx),
|
|
file_extension=".xlsx",
|
|
target_lang="fr",
|
|
source_lang="en",
|
|
provider=provider,
|
|
user_id=None,
|
|
custom_prompt=None,
|
|
glossary_id=None,
|
|
prompt_id=None,
|
|
webhook_url=None,
|
|
user_plan=user_plan,
|
|
)
|
|
)
|
|
finally:
|
|
tr._translation_jobs.pop("job-test", None)
|
|
return captured
|
|
|
|
|
|
def test_openrouter_pro_uses_essential_tier_model(monkeypatch, tmp_path, minimal_xlsx):
|
|
settings = _settings({"essential": {"models": [], "default_model": ""},
|
|
"premium": {"models": [], "default_model": ""}})
|
|
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
|
provider="openrouter", user_plan="PlanType.PRO")
|
|
assert captured, "le faux fournisseur n'a pas été instancié"
|
|
assert captured[0] == "deepseek/deepseek-v4-flash"
|
|
assert captured[0] not in (
|
|
"anthropic/claude-sonnet-5",
|
|
"deepseek/deepseek-v4-pro",
|
|
"z-ai/glm-5.3",
|
|
)
|
|
|
|
|
|
def test_openrouter_premium_uses_premium_tier_model(monkeypatch, tmp_path, minimal_xlsx):
|
|
settings = _settings({"essential": {"models": [], "default_model": ""},
|
|
"premium": {"models": [], "default_model": ""}})
|
|
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
|
provider="openrouter_premium", user_plan="PlanType.BUSINESS")
|
|
assert captured, "le faux fournisseur n'a pas été instancié"
|
|
assert captured[0] == "anthropic/claude-sonnet-5"
|
|
|
|
|
|
def test_admin_tier_default_applied_without_redeployment(monkeypatch, tmp_path, minimal_xlsx):
|
|
settings = _settings({"essential": {"models": [], "default_model": "z-ai/glm-5.3-flash"},
|
|
"premium": {"models": [], "default_model": ""}})
|
|
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
|
provider="openrouter", user_plan="PlanType.PRO")
|
|
assert captured[0] == "z-ai/glm-5.3-flash"
|
|
|
|
|
|
def test_essential_tier_resolving_premium_model_is_corrected(monkeypatch, tmp_path, minimal_xlsx):
|
|
"""Garde d'exécution : un ai_tiers essential contaminé par un modèle
|
|
Premium ne doit pas partir en production — repli sur le défaut du plan."""
|
|
settings = _settings({"essential": {"models": [], "default_model": "anthropic/claude-sonnet-5"},
|
|
"premium": {"models": [], "default_model": ""}})
|
|
captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
|
|
provider="openrouter", user_plan="PlanType.PRO")
|
|
assert captured[0] == "deepseek/deepseek-v4-flash"
|