@fichier~/projets/quizops55 minutes (dont 10 pour l'exercice optionnel)
Le tag tp3-depart reconstruit l'état de sortie du TP 2 : l'application, CLAUDE.md, et le cahier des charges en dix sections dont les trois lots du filtrage par catégorie.
cd ~/projets/quizops
outils/depart.sh 3
.......... [100%]
10 passed in 0.14s
Le filtrage par catégorie manque volontairement, alors que ses tests d'acceptation sont déjà écrits et attendent dans tests/a-venir/.
$ ls tests/a-venir/
test_categories_lot1.py
test_categories_lot2.py
test_categories_lot3.py
test_classement.py
test_defi_a_chrono.py
test_defi_b_export_csv.py
test_defi_c_statistiques_par_categorie.py
test_score_partiel.py
test_statistiques.py
test_tirage.py
test_validation.py
Les trois premiers fichiers sont vos trois lots. Les autres appartiennent aux TP suivants et restent où ils sont.
15 minutes
Livrer le premier lot en partant du test qui le valide, et apprendre à réduire un plan qui propose plus que ce qui a été demandé.
$ mv tests/a-venir/test_categories_lot1.py tests/
$ uv run pytest -q
FAILED tests/test_categories_lot1.py::test_le_filtre_docker_ne_renvoie_que_des_questions_docker
FAILED tests/test_categories_lot1.py::test_le_filtre_kubernetes_ne_renvoie_que_des_questions_kubernetes
2 failed, 10 passed in 0.15s
❌ Deux tests rouges : c'est l'état voulu. Le contrat du lot est écrit, le code ne le remplit pas encore.
Passez en mode plan avec Maj+Tab, puis demandez le plan du lot 1 en donnant le fichier de test comme spécification.
lis tests/test_categories_lot1.py et PRD.md, puis propose le plan du lot 1
Voici la fin d'un plan produit sur ce même dépôt, pour une demande voisine. Le vôtre variera, mais il débordera de la même façon.
### Critical Files for Implementation
- ~/projets/quizops/app/database.py
- ~/projets/quizops/app/main.py
- ~/projets/quizops/tests/a-venir/test_classement.py
- ~/projets/quizops/static/app.js
- ~/projets/quizops/static/index.html
- ~/projets/quizops/static/style.css
Six fichiers, dont trois dans static/, pour une demande qui portait sur une seule route. Sur le lot 1, attendez-vous au même débordement : l'agent anticipe la fonctionnalité complète et vous propose les boutons du lot 3 avant l'heure.
Répondez sans quitter le mode plan, en réduisant le périmètre :
recentre le plan sur le lot 1 uniquement : ne touche pas à static/, pas de
route nouvelle, une seule fonction ajoutée dans app/quiz.py et un paramètre
optionnel sur la route existante de app/main.py
Approuvez le plan recadré. Voici la fonction du dépôt de référence, à comparer avec ce que vous obtenez.
def filtrer_par_categorie(questions: list[dict], categorie: str | None) -> list[dict]:
"""Les questions d'une catégorie, ou toutes les questions si aucune n'est demandée."""
if categorie is None:
return questions
return [question for question in questions if question["categorie"] == categorie]
$ uv run pytest -q
............ [100%]
12 passed in 0.16s
git diff --stat
La liste des fichiers varie ; la preuve est qu'elle ne contient aucune ligne static/.
Q: Pourquoi déplacer le test avant de demander le code, et non l'inverse ?
R: Parce que le test fixe le contrat avant que l'agent ait une occasion de l'interpréter. Le passage de 2 failed à 12 passed est une preuve, alors qu'un test écrit après le code ne fait que confirmer ce que le code fait déjà.
15 minutes
Livrer les deux lots restants avec un prompt par lot et un commit par lot, en donnant un fichier existant comme modèle de style.
Le lot 2 refuse une catégorie inconnue avec un code 404. Le lot 3 ajoute la route GET /api/categories et un bouton par catégorie dans l'interface, donc du JavaScript sans framework : l'occasion d'imposer un style par l'exemple.
@static/app.js dans votre prompt comme modèle de style.Contraintes à imposer dans vos prompts
HTTPException avec le code 404, jamais une liste videGET /api/categories renvoie une liste triée@static/app.js comme modèle, aucun framework ni dépendance ajoutée💡 Indice : @static/app.js insère le contenu du fichier dans le prompt. Un extrait vaut dix règles de style.
$ uv run pytest -q
.............. [100%]
14 passed in 0.20s
$ git log --oneline tp3-depart..HEAD | wc -l
3
Serveur lancé, les trois contrôles du cahier des charges répondent :
$ curl -s http://127.0.0.1:8000/api/categories
["docker","kubernetes"]
$ curl -s 'http://127.0.0.1:8000/api/questions?categorie=docker' | grep -c kubernetes
0
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8000/api/questions?categorie=inconnue'
404
1. Le prompt du lot 2 :
Le test tests/test_categories_lot2.py vient d'être activé et il échoue.
Fais en sorte que GET /api/questions?categorie=inconnue réponde 404 au lieu
de renvoyer une liste vide, en levant une HTTPException dont le message
nomme la catégorie demandée.
Ne modifie ni le test ni la fonction filtrer_par_categorie.
Lance uv run pytest -q et montre-moi la ligne finale.
Le résultat dans app/main.py tient en deux lignes :
questions = quiz.filtrer_par_categorie(QUESTIONS, categorie)
if categorie is not None and not questions:
raise HTTPException(status_code=404, detail=f"Catégorie inconnue : {categorie}")
2. Le prompt du lot 3 :
Livre le lot 3 du PRD : une route GET /api/categories qui renvoie la liste
triée des catégories, et un bouton par catégorie sur l'écran d'accueil.
@static/app.js est le modèle de style : même façon de nommer, mêmes appels
fetch, aucune dépendance ajoutée.
Le test tests/test_categories_lot3.py doit passer, lance uv run pytest -q.
La route du dépôt de référence :
@app.get("/api/categories")
def lister_categories() -> list[str]:
"""Les catégories disponibles, triées, pour les boutons de l'interface."""
return sorted({question["categorie"] for question in QUESTIONS})
3. Les trois messages de commit du dépôt de référence :
Filtre les questions par catégorie
Refuse une catégorie inconnue avec un 404
Liste les catégories et propose un bouton par catégorie
✅ Trois lots, trois commits, quatorze tests verts. Chaque commit peut être relu seul et annulé seul.
12 minutes
Comparer deux façons de signaler une panne à l'agent : la dernière ligne de la trace, puis la trace entière avec ce que vous attendiez.
$ uv run outils/panne.py 2
Panne 2 appliquée dans app/main.py. Retour à la normale : git checkout -- app/main.py
Relancez le serveur : il refuse de démarrer.
uv run uvicorn app.main:app --reload
ImportError: cannot import name 'donnee' from 'app' (~/projets/quizops/app/__init__.py)
Ouvrez une session et collez uniquement cette ligne, sans rien ajouter. Notez ce que l'agent fait : il doit deviner d'où vient l'import, quel fichier le contient et ce que vous attendiez.
Tapez /clear, puis recommencez avec la trace complète et trois éléments. Voici la trace, telle que le terminal l'affiche.
Traceback (most recent call last):
File "~/projets/quizops/.venv/bin/uvicorn", line 10, in <module>
sys.exit(main())
~~~~^^
[… une vingtaine de lignes de la pile click et uvicorn …]
File "~/.pyenv/versions/3.13.14/lib/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1395, in _gcd_import
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1023, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "~/projets/quizops/app/main.py", line 9, in <module>
from app import database, donnee, quiz
ImportError: cannot import name 'donnee' from 'app' (~/projets/quizops/app/__init__.py)
Les trois éléments à donner avec la trace :
Application startup complete.La trace nomme le fichier, la ligne et le symbole absent. L'avant-dernière ligne, File ".../app/main.py", line 9, est celle qui économise la moitié du travail : sans elle, l'agent cherche l'import dans quatre modules.
$ uv run pytest -q
.............. [100%]
14 passed in 0.20s
$ git status --short
La seconde commande ne renvoie rien : la correction a rendu au fichier son contenu d'origine, il n'y a même pas de commit à faire.
10 minutes
Mettre le README à jour depuis le code livré, sans le réécrire à la main.
Le README fait cinq lignes et date d'avant le filtrage par catégorie. Trois routes existent maintenant, dont deux inconnues du fichier.
@app/main.py dans votre prompt comme source unique de vérité.curl.curl.Contraintes à imposer dans votre prompt
curl par route, sur http://127.0.0.1:8000@app/main.py💡 Indice : demandez à l'agent de relire son propre README contre app/main.py avant de conclure. Une relecture explicite attrape ce qu'une génération laisse passer.
grep -c 'categorie' README.md
uv run pytest -q
Le compteur varie selon la rédaction, mais il vaut au moins 2 puisque deux routes concernent les catégories. La suite reste à 14 passed : documenter ne touche pas au code.
2 failed puis 12 passed, la preuve ne dépend jamais du texte de l'agent.@static/app.js impose un style que trois consignes n'auraient pas transmis.Rendez-vous au TP 4 : la même tâche sera confiée à deux modèles dans deux worktrees, et vous comparerez des mesures.
➡️ Module associé : Prompt Engineering et Conception · TP suivant : TP 4 : Comparer et Choisir : Modèles, Réflexion, Best-of-N