Aller au contenu

Tests e2e avec des agents IA de code en parallèle

Un git worktree isole les fichiers, pas la machine. Comment un verrou exclusif et une file FIFO empêchent les tests e2e de nos agents IA de se percuter.

Par SuperDevSpace

Publié le 9 min de lecture

Aussi en: English Türkçe Deutsch Español Português (Brasil) 日本語 简体中文

Lancer plusieurs agents IA de code en parallèle est simple : chacun reçoit son propre git worktree et son propre terminal, et ils cessent d'écraser les fichiers des autres. Les choses se corsent quand il faut lancer des tests e2e avec des agents IA en parallèle. Un worktree isole les fichiers. Il n'isole pas la machine. Voici une étude de cas tirée de notre propre dépôt : ce qui a cassé quand plusieurs agents ont lancé des tests de bout en bout en même temps, et le petit duo verrou + file d'attente que nous avons construit pour qu'ils passent chacun leur tour — en local, sur une seule machine, sans service externe.

Ce qu'un worktree n'isole pas

Un git worktree donne à chaque agent un checkout, une branche et un node_modules séparés. Cela couvre l'arborescence du code. Un lancement e2e touche bien plus :

  • l'écran — la suite e2e de notre application de bureau ouvre de vraies fenêtres sur un écran de test, et certaines specs capturent ce qui s'y trouve réellement ;
  • le navigateur installé — les specs qui pilotent un vrai Chrome se le disputent ;
  • le focus des fenêtres — notre suite compte les activations de l'application et fait échouer un test qui a volé le focus ; une activation causée par un lancement est comptée contre un autre ;
  • le CPU — deux suites à la fois resserrent chaque timeout.

Rien de tout cela n'est dans le worktree. Tout cela est partagé par tous les agents de la machine.

Nous l'avons appris directement. Une release gate est revenue avec 21 tests en rouge, tous dans des specs qui ouvrent un vrai Chrome ou capturent l'écran — pendant que des agents d'autres worktrees étaient en plein lancement. Le code n'avait rien. Deux lancements s'étaient mesurés l'un l'autre.

La règle « un seul lancement e2e à la fois » existait dans la tête de quelqu'un. Elle a tenu jusqu'au jour où elle n'a plus tenu. Nous en avons donc fait un mécanisme.

Première tentative : lire le verrou, puis l'écrire

Le premier verrou était un fichier dans /tmp — pas dans un worktree, car les lancements qu'il coordonne vivent dans des worktrees différents ; un verrou par worktree ne les verrait jamais. Il lisait le fichier, concluait que la machine était libre, puis l'écrivait.

Une race condition classique. Deux lancements qui lisaient tous deux avant que l'un n'écrive se croyaient tous deux propriétaires. Pire : quand le plus court finissait, sa libération supprimait le fichier alors que le plus long tournait encore. Un lancement design:qa a ainsi tourné environ trois minutes sans aucun fichier de verrou.

La correction tient en un drapeau. Créer le fichier de manière exclusive — O_CREAT | O_EXCL, exposé par Node sous le drapeau wx :

scripts/gate-lock.mjs
export function createExclusive(path, text) {
  try {
    writeFileSync(path, text, { flag: 'wx' })
    return true
  } catch (error) {
    if (error?.code === 'EEXIST') return false
    throw error
  }
}

Le système d'exploitation garantit qu'un seul appelant obtient le fichier. Plus de fenêtre entre lecture et écriture.

Un verrou doit survivre à un crash

Un fichier de verrou qui survit à son processus bloque tout le monde pour toujours. Le fichier contient donc le pid du détenteur, et celui qui attend le vérifie avec kill -0 — aucun signal n'est envoyé, on demande seulement si le processus existe. EPERM signifie qu'il existe mais appartient à quelqu'un d'autre : il compte comme vivant. Un verrou dont le pid est mort est périmé et repris.

On peut faire la même chose en bash pur. noclobber empêche > d'écraser un fichier existant, ce qui donne une création exclusive :

Terminal
$ ( set -o noclobber; echo $$ > /tmp/e2e.lock ) && echo "acquired"
acquired
$ kill -0 "$(cat /tmp/e2e.lock)" && echo "holder alive"

Pas de verrou ne veut pas dire machine libre

Voici le piège que nous avons failli rater. Les lancements démarrés avant l'existence du verrou n'en écrivent pas. Un lancement démarré à la main non plus. Lire « pas de fichier de verrou » comme un feu vert, c'est exactement l'erreur que le verrou doit empêcher.

Sans verrou, on interroge donc la machine elle-même : y a-t-il un runner Playwright dans la liste des processus ? Mais la correspondance doit porter sur la forme de l'exécutable, pas sur un mot. Un grep playwright trop large sur la liste des processus attrapait les sessions d'autres agents, car une session d'agent porte tout son prompt sur sa ligne de commande — et nos prompts parlent de Playwright. Même le shell de l'agent qui écrivait le scanner est apparu dans son propre scan comme une gate en cours. Un texte qui parle d'une chose ressemble exactement à la chose.

Les boucles de polling échouent deux fois : course et famine

Même avec un verrou correct, chaque agent devait attendre, et chacun a écrit sa propre boucle de polling. Résultat : les deux défauts pour lesquels les files d'attente existent :

  • la course — plusieurs boucles qui essaient dans la même seconde et se jettent sur le verrou dès qu'il se libère ;
  • la famine — une boucle polie (attendre 20 secondes après « libre », puis essayer) ne gagnait jamais. Un worker a attendu 50 minutes sans obtenir une seule place.

Nous avons donc placé une file premier arrivé, premier servi (FIFO) devant le verrou.

Comment un lancement e2e obtient la machineComment un lancement e2e obtient la machine
Comment un lancement e2e obtient la machine
Le diagramme en texte
flowchart TD
  start["Un agent démarre un lancement e2e"] --> join["Créer un fichier ticket en exclusif"]
  join --> head{"Mon ticket est-il en tête ?"}
  head -->|"non"| wait["Attendre 2 s + aléa, rafraîchir le heartbeat"]
  wait --> head
  head -->|"oui"| lock{"Création exclusive du fichier de verrou"}
  lock -->|"tenu par un pid vivant"| wait
  lock -->|"créé"| run["Lancer la seule spec concernée"]
  run --> release["Supprimer le verrou, écrire une ligne de log"]

La conception, en cinq règles :

  1. Un fichier par attente, créé en exclusif. Rejoindre la file ne lit ni ne réécrit jamais une liste partagée. Le nom du fichier est la place du ticket : <band>-<joined-at ms>-<sequence>-<pid>.json, si bien que les noms se trient dans l'ordre de la file.
  2. Seule la tête peut prendre le verrou. Celui qui voit le verrou libre sans être premier continue d'attendre. Un appelant qui n'a pas fait la queue est refusé tant que quelqu'un attend.
  3. Les attentes mortes ou bloquées sortent. Un ticket dont le pid est mort est supprimé, tout comme un ticket dont le heartbeat date de plus de 60 secondes — une attente suspendue ne doit pas prendre la machine en otage depuis la tête de file. Si votre ticket a été retiré, vous repartez en fin de file.
  4. Ajouter de l'aléa au polling. Toutes les 2 secondes, plus jusqu'à 500 ms aléatoires, pour que les attentes ne se réveillent pas en même temps.
  5. La priorité déplace un ticket, jamais un détenteur. Seule la release gate obtient la bande de tête, inscrite dans son ticket et dans le log. Elle attend quand même le lancement qui tient le verrou.

Après 60 minutes, une attente abandonne et sort avec le code 3, le même qu'un refus. Le drapeau --no-wait refuse tout de suite ; et tant que quelqu'un attend, il est refusé plutôt que de doubler la file.

Journaliser chaque transition

Quand ce verrou a disparu pendant trois minutes, personne n'a pu l'expliquer ensuite : rien n'enregistrait qui avait touché au fichier. Désormais, chaque acquisition, refus, reprise de verrou périmé, entrée dans la file, sortie, éviction et libération est une ligne dans un log à côté du verrou, plafonné à 1 Mo. « Qui avait la machine à 14 h 02 ? » devient un grep, pas un débat.

Le dire aux agents, dans leurs instructions

Un mécanisme que les agents contournent n'est pas un mécanisme. Le CLAUDE.md de notre dépôt — le fichier que Claude Code lit comme mémoire de projet — l'écrit noir sur blanc : attendre est le comportement par défaut, et n'écrivez pas votre propre boucle de polling. Il demande aussi à chaque agent de ne lancer que la spec qui couvre sa modification, une seule fois. Moins de lancements, file plus courte.

Un terminal montrant un lancement e2e d'une seule spec, six tests réussis et le résumé de la gate, à côté d'une incrustation en direct des positions de fenêtres
Une spec, un lancement : le résumé de la gate e2e dans le terminal de SuperDevSpace (incrustation en turc)

Si vous faites tourner des agents en parallèle, prenez cette checklist

Boucle de polling par agentVerrou + file FIFO
Deux lancements sur un même écran✓ possible✗
Un lancement planté bloque tout le monde✓ (sans vérification du pid)✗
Une attente peut mourir de faim✓✗
On sait qui avait la machine✗✓
  • Placer le verrou là où chaque worktree le voit (/tmp, ou le dossier temporaire du système sous Windows).
  • Le créer en exclusif ; stocker le pid ; considérer un pid mort comme libre.
  • Ne pas se fier à l'absence de verrou — chercher le binaire du vrai runner.
  • Faire la queue avec un fichier exclusif par attente, des heartbeats et de l'aléa.
  • Journaliser chaque transition.
  • Écrire la règle dans le fichier d'instructions des agents.

Faire cela dans SuperDevSpace

Dans SuperDevSpace, un manager découpe un sprint en tâches et chaque worker travaille dans son propre git worktree et son propre terminal : le travail en parallèle sur un même dépôt ne se percute pas au niveau des fichiers, et tout reste en local sur votre machine. Le verrou et la file ci-dessus ne sont pas une fonctionnalité du produit — ce sont des scripts de notre propre dépôt, écrits par les agents qui construisent SuperDevSpace, pour la seule ressource que les worktrees ne peuvent pas partager. Si vos agents partagent un écran de test, un navigateur ou un port, il vous faudra la même chose ; le modèle est assez petit pour être copié. Claude Code est entièrement pris en charge ; les autres CLI comme Codex et Gemini CLI sont expérimentales.

Pour le côté worktree de l'histoire, lisez Faire tourner des agents de code en parallèle dans des git worktrees ; pour l'installation de l'application, voyez la documentation.

Télécharger SuperDevSpace

SuperDevSpace

Écrit par l’équipe qui construit SuperDevSpace, une application de bureau qui fait travailler une équipe d’agents de code IA sur l’abonnement CLI que vous avez déjà.

Partager

Recevoir l’e-mail de lancement

Un seul e-mail quand SuperDevSpace ouvrira à tous. Vous confirmez d’abord l’adresse depuis votre boîte de réception ; un clic suffit pour partir.

Votre propre équipe d’IA

SuperDevSpace fonctionne avec l’abonnement CLI que vous avez déjà. Essai gratuit de 14 jours, sans carte.

Essayer gratuitement

À lire aussi