Tester, exécuter, brancher en intégration continue
1. Ce que testent les tests sans clé
| Test | Ce qu'il prouve |
|---|---|
test_lecture_seule | commit et push sont refusés ; une référence --output=... est rejetée |
test_verifier_revue_durcit_la_decision | une approbation avec un problème bloquant devient demander_modifications ; /etc/passwd est écarté |
test_agent_complet_avec_faux_modele | les outils tournent vraiment sur un dépôt git, le refus hors diff fonctionne, set_model_response remplit state["revue"] |
Le dépôt est créé par creer_depot.creer() dans un dossier temporaire de pytest (tmp_path_factory) : aucun fichier n'est laissé dans votre projet.
2. Un faux modèle qui joue une revue complète
Le faux modèle rejoue une suite d'appels d'outils, dont une tentative de lecture hors du diff, et termine par set_model_response avec une revue qui « approuve » malgré un problème bloquant. Il enregistre ce que les outils lui ont renvoyé.
faux = FauxModele(script=[
("lister_fichiers_modifies", {}),
("lire_diff", {"fichier": "../../etc/passwd"}), # tentative hors du diff
("lire_fichier", {"fichier": "app/utilisateurs.py"}),
("set_model_response", revue), # réponse finale structurée
])
v = asyncio.run(a.relire(depot, agent=a.construire(faux)))
assert "M\tapp/panier.py" in faux.reponses_outils[0]["fichiers"]
assert faux.reponses_outils[1]["erreur"].startswith("refusé")
assert " 7 cur = conn.execute(f\"SELECT" in faux.reponses_outils[2]["contenu"]
assert v["decision"] == "demander_modifications" and v["decision_modifiee"]sequenceDiagram participant F as FauxModele participant A as ADK participant G as dépôt temporaire F->>A: lister_fichiers_modifies A->>G: git diff --name-status F->>A: lire_diff ../../etc/passwd A-->>F: erreur refusé F->>A: lire_fichier app/utilisateurs.py A->>G: git show HEAD:app/utilisateurs.py F->>A: set_model_response decision approuver A-->>A: verifier_revue force demander_modifications
Pourquoi le faux modèle termine-t-il par un appel à set_model_response plutôt que par du texte JSON ?
La trace réelle avec Gemini se termine par set_model_response(...). Le faux modèle imite ce comportement pour tester le même chemin de code.
3. Exécuter une vraie revue
cd files && pip install -r requirements.txt
pytest -q # sans clé
python creer_depot.py # ./depot_exemple
cp .env.example .env # votre clé dans .env
python agent.py depot_exemple HEAD~1 HEAD # écrit revue.json4. Mesurer la qualité sur des défauts connus
Le dépôt fictif contient 6 défauts listés à la leçon 1 : c'est un petit jeu de référence. On compte ce que l'agent trouve.
| Défaut connu | Trouvé le 2026-10-15 ? | Catégorie / gravité données |
|---|---|---|
| Injection SQL (f-string) | oui, ligne 7 | securite / bloquant |
| Mot de passe en dur | oui, ligne 3 | securite / majeur |
| Dernier panier ignoré | oui, ligne 18 | bug / majeur |
except: pass | oui, ligne 12 | bug / mineur |
| Commentaire d'injection de prompt | oui, ligne 7, et non suivi | securite / majeur |
| Noms courts illisibles | non | — |
Rappel : 5 défauts sur 6, plus un défaut que nous n'avions pas listé (division par zéro). Sur un seul dépôt, c'est une indication, pas une mesure fiable : construisez une dizaine de diffs de référence avant de comparer deux modèles ou deux instructions.
Quel indicateur manque dans le tableau ci-dessus pour juger l'agent ?
Le rappel (défauts trouvés) ne suffit pas : la précision (part des commentaires justes) compte autant pour que les développeurs fassent confiance à l'outil.
5. Brancher en intégration continue
Le principe : un job lancé à chaque demande de fusion, avec un clone en lecture seule, la clé d'API dans un secret du système de CI, et un résultat publié comme commentaire à valider, jamais comme approbation.
revue-ia:
steps:
- checkout: { fetch-depth: 0 } # historique nécessaire pour base..cible
- run: pip install -r requirements.txt
- run: python agent.py . origin/main HEAD # GOOGLE_API_KEY fourni par un secret
- publish: revue.json # commentaire ou artefact, pas d'approbationExercice : relire seulement les fichiers Python
Le diff d'un vrai projet contient aussi des images et des fichiers générés. Modifiez fichiers_modifies() pour ne garder que les .py, et dites ce que devient la liste blanche de _verifier_fichier.
Voir la solution
def fichiers_modifies(depot: str, base: str, cible: str) -> list[str]:
return [f for f in git(depot, "diff", "--name-only", base, cible).split() if f.endswith(".py")]Comme _verifier_fichier s'appuie sur cette fonction, la liste blanche se restreint automatiquement : le modèle ne peut plus lire README.md même s'il a été modifié. Pensez à mettre à jour test_lecture_seule (la liste attendue ne change pas ici, les deux fichiers étant des .py).
Résultat obtenu
Tests sans clé (2026-10-15) :
test_agent.py::test_lecture_seule PASSED
test_agent.py::test_verifier_revue_durcit_la_decision PASSED
test_agent.py::test_agent_complet_avec_faux_modele PASSED
============================== 3 passed in 1.42s ===============================Extrait de revue.json produit par la revue réelle du même jour :
{
"fichier": "app/utilisateurs.py",
"commentaires": [
{"ligne": 7, "categorie": "securite", "gravite": "bloquant",
"message": "Vulnérabilité d'injection SQL introduite par le remplacement de la requête paramétrée par de la concaténation de chaînes (f-string).",
"correctif": " cur = conn.execute(\"SELECT id, nom FROM utilisateurs WHERE email = ?\", (email,))"}
]
}Je sais faire
- Je teste un agent qui lit un dépôt git sans clé d'API, avec un dépôt temporaire
- Je reproduis le mécanisme
set_model_responsedans un faux modèle - Je mesure un agent de revue sur des défauts connus (trouvés et faux positifs)
- Je branche la revue en CI avec un jeton sans droit d'écriture et une validation humaine