
Comment refactoriser du code sans tests ?
Vous héritez d’un projet legacy de 100 000 lignes. Pas de tests. Vous voulez refactoriser, mais impossible de savoir ce qui va casser. Pour écrire des tests, il faut comprendre le code. Pour comprendre le code, il faut de la documentation — qui n’existe pas non plus.
Personne n’y touche. Le code continue de pourrir.
Tous les systèmes legacy du monde sont pris dans cette impasse. Les entreprises du Fortune 500 consacrent 60 à 80 % de leur budget IT à la maintenance du legacy. 42 % du temps des développeurs est absorbé par la dette technique.
Et si un LLM pouvait écrire les tests à votre place ?
Les problèmes quand un LLM écrit les tests
Demandez à un LLM « écris un test pour cette fonction » et il produira quelque chose. Mais trois problèmes se posent.
Premier problème : on ne sait pas par où commencer. Quand il y a 527 fonctions, on commence par la première ? Par la plus importante ? Il n’y a aucun critère.
Deuxième problème : impossible de vérifier la qualité des tests. Le test écrit par le LLM passe. Mais vérifie-t-il réellement le comportement de la fonction, ou n’est-ce qu’une coquille vide qui appelle la fonction sans aucun assert ? Il faut relire chaque test manuellement pour le savoir.
Troisième problème : sans feedback, les tests du LLM plafonnent à 60–70 %. L’instruction « teste cette fonction » ne suffit pas pour atteindre 100 % de branch coverage. Il faut indiquer quelles branches manquent pour que le LLM complète le reste.
Le LLM n’est pas incapable d’écrire des tests. Ce qui manque, c’est une structure qui lui indique quoi tester et à quel point il a bien testé.
tsma : un rail de test piloté par une seule commande
tsma est un outil CLI qui indexe toutes les fonctions du projet, détecte la présence de tests, mesure le coverage et fournit un feedback précis à l’agent LLM.
L’agent n’a besoin de connaître qu’une seule commande :
$ tsma next
Cette unique commande pilote l’intégralité de la boucle :
$ tsma next # affiche la prochaine fonction sans test
→ on écrit le test
$ tsma next # détecte le nouveau test, l'exécute, mesure le coverage
→ 100 % ? PASS, on passe à la suivante
→ < 100 % ? affiche les branches non couvertes avec les numéros de ligne
$ tsma next # re-mesure le test corrigé
→ amélioré ou non, marque DONE et passe à la suite
On répète jusqu’à ce que « All functions complete! » apparaisse.
Validé sur 527 fonctions
tsma a été appliqué à un vrai projet Go (527 fonctions).
| Résultat | Nombre | Proportion |
|---|---|---|
| PASS (100 % branch coverage) | 246 | 46,7 % |
| DONE (best-effort) | 281 | 53,3 % |
| TODO (non traité) | 0 | 0 % |
246 fonctions ont atteint 100 % de branch coverage. Les 281 restantes n’y sont pas parvenues, mais ont été testées autant que possible.
Pourquoi certaines fonctions n’atteignent-elles pas 100 % ?
Fonctions qui atteignent 100 % et celles qui n’y arrivent pas
La capacité d’une fonction à atteindre 100 % de branch coverage dépend de la manière dont elle reçoit ses dépendances.
Interface (mockable) — 100 % atteignable :
type Handler struct {
svc AuthSvc // interface — remplaçable par un mock
}
En test, l’injection d’un mock permet de contrôler tous les chemins :
svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil) // chemin de succès
svc.EXPECT().Login(...).Return(nil, err) // chemin d'erreur
Type concret (not mockable) — 100 % impossible :
type Handler struct {
svc *service.SMSImportService // pointeur de struct — non remplaçable
}
L’implémentation réelle embarque des dépendances internes (BDD, API externes, etc.). Impossible de provoquer une erreur spécifique ou de forcer un résultat particulier. Les branches qui en dépendent sont inaccessibles en test unitaire.
Réponse de tsma : après le feedback sur les branches non couvertes, une seconde tentative est accordée. Si la branche reste inaccessible, la fonction est marquée DONE. Ce n’est pas une limite de l’outil, mais le reflet de la testabilité du code. L’introduction d’interfaces (DI) rendrait les 100 % possibles, mais cela implique de modifier le code source.
Le feedback transforme radicalement les tests du LLM
La valeur centrale de tsma n’est ni l’indexation ni la mesure du coverage. C’est l’indication précise des branches non couvertes avec leurs numéros de ligne.
Sans feedback :
"Écris un test pour la fonction ListContracts"
→ Le LLM ne teste que le happy path
→ coverage 60–70 %
Avec feedback :
"Écris un test pour la fonction ListContracts"
→ coverage 65 % (11/17)
→ UNCOVERED:
line 41 — if params.Status != nil
line 44 — if params.BuildingId != nil
line 70 — if err != nil (CountSummary)
→ Le LLM ajoute des tests couvrant exactement ces branches
→ coverage 100 %
C’est le même LLM. La seule différence est la présence du feedback. Trois lignes avec des numéros de ligne séparent 60 % de 100 %.
L’agent tombe — la progression est préservée
Les agents LLM plantent. Limite de tokens, erreur réseau, déconnexion de session. Impossible de traiter 527 fonctions en une seule session.
tsma persiste la progression dans .tsma/session.json.
$ tsma status
527 functions
PASS: 246 (46.7%)
DONE: 281 (53.3%)
TODO: 0 (0.0%)
L’agent plante à la 200e fonction ? Un nouvel agent lance tsma next et reprend à la 201e. session.json est le checkpoint.
Plusieurs agents peuvent travailler en relais sans conflit. L’atomicité est au niveau de la fonction.
La session est un cache, le code source est la vérité
Un des principes de conception de tsma : la session est un cache, le source of truth est le code source.
Si l’on supprime un fichier de test, la fonction revient à TODO même si session.json indique PASS. La session ne diverge pas de la réalité.
Principe :
session.json dit "PASS"
mais le fichier de test n'existe plus → TODO
le fichier source a changé → à re-mesurer
Instructions pour l’agent LLM
L’agent n’a besoin que de 6 lignes d’instructions :
1. Exécuter tsma next
2. TODO — lire la fonction et écrire le test
3. Échec du test — lire l'erreur et corriger le test
4. Branches non couvertes affichées — ajouter des tests pour ces branches
5. PASS/DONE — la fonction suivante s'affiche automatiquement
6. Répéter jusqu'à "All functions complete!"
L’agent n’a besoin de connaître qu’une commande : tsma next. Le reste est contraint par le CLI.
Le train et les rails
Le vibe coding, c’est un train. Rapide. Mais sans rails, il déraille.
Tous les outils d’AI coding se concentrent sur rendre le train plus rapide. Un modèle plus gros, un agent plus intelligent, un meilleur prompt. Mais plus le train accélère, plus le déraillement est dévastateur.
tsma, ce sont les rails. Le LLM génère les tests (Neural), le CLI définit les limites (Symbolic Constraint). La créativité du LLM reste intacte, mais la qualité du résultat est imposée par la machine.
| Avant | tsma | |
|---|---|---|
| Écriture des tests | Humain (lent) ou LLM (chaotique) | LLM écrit, CLI vérifie |
| Par où commencer ? | Décision humaine | CLI détermine l’ordre |
| Vérification qualité | Revue humaine | CLI mesure le coverage |
| Feedback | Aucun | Numéros de ligne des branches non couvertes |
| Suivi de progression | Aucun | session.json automatique |
Le LLM génère librement. Mais uniquement sur les rails de tsma next.
Langages supportés
| Langage | Indexeur | Test runner | Coverage |
|---|---|---|---|
| Go | go/ast | go test | go test -coverprofile |
| TypeScript | regex | npx vitest / npx jest | c8 / istanbul |
| Python | regex | pytest | coverage.py |
Go utilise un parseur AST pour une extraction précise des fonctions. TypeScript et Python reposent sur des expressions régulières.
Les fichiers générés (*_gen.go, *.pb.go), les fichiers de test et les répertoires vendor/node_modules sont automatiquement exclus de l’indexation.
Installation et exécution
make install
cd your-legacy-project
tsma next
C’est tout.
MIT License. github.com/park-jun-woo/tsma