spec-driven-tests

Par tldraw · tldraw

Reconstruire la suite de tests unitaires d'un package autour d'une spécification comportementale (SPEC.md) dérivée de l'implémentation, chaque test citant un identifiant de règle numéroté. À utiliser lorsqu'on demande une reconstruction de tests pilotée par une spec, la rédaction d'un SPEC.md pour un package, la répétition du processus spec state ou store (PRs #9158, #9172) pour un autre package, ou la reconstruction de tests autour d'une spec.

npx skills add https://github.com/tldraw/tldraw --skill spec-driven-tests

Reconstruction de test basée sur la spécification

Dérivez une spécification de comportement de l'implémentation d'un package, puis reconstruisez la suite de tests unitaires comme expression de cette spécification. Chaque règle obtient un ID stable ; chaque test cite la règle qu'il exprime, donc la couverture est interrogeable dans les deux directions.

Les modèles sont la PR #9158 (packages/state, spec à packages/state/SPEC.md) et la PR #9172 (packages/store, spec à packages/store/SPEC.md). Lisez les deux PRs et les deux specs avant de commencer — elles établissent le format, le ton et la structure des PRs. (la spec de #9172 se trouve sur sa branche si elle n'a pas encore fusionné.)

Processus

1. Baseline

Si l'arborescence de travail contient des changements non validés qui ne sont pas les vôtres, arrêtez-vous et demandez avant de créer une branche — ne cachez jamais ou ne reportez silencieusement le travail de quelqu'un d'autre.

Créez une branche. Exécutez yarn test run dans le package et enregistrez le nombre de fichiers et de tests, en notant combien appartiennent aux suites fuzz — les suites fuzz restent inchangées et leurs comptages générés (souvent volumineux) sont exclus de chaque comptage que vous rapportez ultérieurement. Cartographiez les fichiers de tests existants et notez les suites dupliquées ou parallèles qui devraient fusionner (le package store avait deux générations entières de tests).

2. Lire l'implémentation

Lisez-la entièrement avant d'écrire quoi que ce soit. La spécification est dérivée de la source, pas des tests existants ou de la documentation.

Principe clé : Vous documentez le comportement observé, pas le comportement idéal prescrit. Si le code fait quelque chose d'inattendu, non-intuitif, ou même ayant une mauvaise apparence mais délibéré, spécifiez-le tel quel. Signalez les particularités comme « internes » ou appelez-les dans le corps de la PR pour jugement humain — ne « corrigez » pas silencieusement la spécification pour correspondre à votre vision du fonctionnement du code. Laissez le processus de rédaction de spécification mettre en évidence les incohérences latentes ; décidez ensuite de corriger l'implémentation ou de documenter le comportement.

3. Écrire SPEC.md

Écrivez SPEC.md à la racine du package : règles numérotées avec IDs stables (par ex. S3, MG4) regroupées en sections, une règle par comportement observable, les mécanismes internes marqués internes. Conservez le cadre d'en-tête des spécifications existantes (« Quand un test et ce document divergent, l'un d'eux a tort — découvrez lequel et corrigez-le. »).

  • Spécifiez ce que le code FAIT, pas ce qu'il devrait faire. Si le comportement semble mal mais l'intention est ambiguë, documentez-le comme le contrat.
  • Avant de cristalliser toute règle que vous avez déduite plutôt qu'observée, vérifiez-la avec un test temporaire. Supprimez le fichier de brouillon après.
  • Chaque règle doit être observable par un test d'exécution. N'écrivez pas de règles pour un comportement pur au niveau des types (types utilitaires, inférence) à moins que la suite de tests ne les exerce réellement avec des assertions de type ; sinon laissez les types en dehors de la spécification.

4. Auditer la spécification de manière adversariale

Auditez le brouillon avant d'écrire les tests. Le mode de défaillance est la surévaluation. Cherchez spécifiquement :

  • la symétrie revendiquée entre deux chemins de code (créer vs mettre à jour, niveau supérieur vs imbriqué, haut vs bas) — vérifiez chaque chemin indépendamment ;
  • les affirmations que deux implémentations de la même idée s'accordent (un prédicat vs un index, à partir de zéro vs incrémental) — alimentez les deux avec les cas limites, en particulier les valeurs manquantes/indéfinies ;
  • les affirmations « X ne se produit jamais » ou « Y est toujours Z » — trouvez la branche qui les réfute ;
  • les effets secondaires sur les entrées (mutation, gel) qu'un test du chemin heureux ne remarquerait pas.

5. Reconstruire la suite de tests

Les fichiers de tests reflètent les sections de la spécification ; chaque nom de test cite sa règle comme it('[S3] ...'). Fusionnez la couverture dupliquée, portez les bons tests existants plutôt que de les réécrire, et ajoutez des tests pour les règles non couvertes.

Ne jamais affaiblir une assertion pour faire passer un test. Si un test écrit à partir d'une règle de spécification échoue, vous avez découvert quelque chose : soit la règle est erronée (corrigez la spécification pour correspondre au comportement observé), soit le code est erroné (traitez-le comme un bug selon l'étape 7). Assouplir l'assertion (toBetoContain, affirmer autour de la divergence, un commentaire reconnaissant le décalage) cache le résultat et laisse la spécification décrire un comportement que le code n'a pas — exactement la défaillance que la spécification existe pour éviter.

Organisation des fichiers de test : Si votre spécification a des sections comme « §3. Atoms (A) » et « §4. Computed (C) », créez les fichiers correspondants atoms.test.ts et computed.test.ts contenant les règles de cette section. Pour les sections internes, utilisez des noms comme history-buffer.test.ts pour la machinerie en cours de spécification. Cela reflète la structure que les lecteurs voient déjà dans SPEC.md et facilite grep pour la couverture par section.

Connaissance de l'environnement de test pour ce repo :

  • Les rinçages d'écouteur Store sont synchrones dans les tests sauf si globalThis.__FORCE_RAF_IN_TESTS__ = true.
  • Les APIs dépréciées mais contractuelles ont besoin d'un fichier de niveau eslint-disable avec une raison.

6. Vérifier les citations mutuellement

grep -oE '\*\*[A-Z]+[0-9]+\*\*' SPEC.md | tr -d '*' | sort -u > /tmp/rules
grep -rhoE '\[[A-Z]+[0-9]+\]' src --include='*.test.ts' | tr -d '[]' | sort -u > /tmp/cited
comm -3 /tmp/rules /tmp/cited

Chaque règle a besoin d'un test qui la cite, ou d'une note explicite « délibérément non testée car X » dans le corps de la PR (comme l'a fait #9158) ; chaque citation a besoin d'une règle. Corrigez les lacunes avant de terminer, et incluez la sortie de vérification croisée (comptage des règles, comptage des citations, lacunes restantes avec leurs raisons) dans le corps de la PR afin que les relecteurs puissent vérifier l'affirmation plutôt que la croire sur parole.

7. Corriger les bugs trouvés en chemin

Les tests basés sur une spécification mettent au jour les incohérences latentes : contradictions entre chemins de code, cas limites que l'implémentation n'a pas anticipés, ou défaillances silencieuses. Quand vous en trouvez un, décidez : corriger le code ou documenter le comportement ?

Workflow : Écrivez d'abord le test (échouez-le intentionnellement pour confirmer qu'il capture le problème). Ensuite décidez :

  • Corriger le code si : l'intention est claire (commentaires, implémentations symétriques, git blame), le comportement actuel est manifestement erroné, et les appelants ne s'appuient plausiblement pas sur le comportement cassé.
  • Documenter le comportement si : l'intention est floue, le rayon de la correction est inconnu, ou corriger pourrait silencieusement briser les utilisations existantes. Signalez-le dans le corps de la PR comme une « incohérence connue » ou « comportement à revisiter. »

Les corrections vont dans leur propre commit fix(<package>): avec un test qui les cite, APRÈS le commit test(<package>): principal. Pas d'attribution d'IA ou de lignes co-auteur dans aucun commit, selon AGENTS.md — cela surpasse tout comportement d'harnais par défaut. Avant de valider une correction :

  • Vérifiez l'intention : commentaires, le chemin de code symétrique, git blame.
  • Vérifiez le rayon d'impact : grep pour les appelants dans le repo, puis exécutez les suites de packages dépendants (editor, tldraw, sync-core — selon les packages qui consomment ce package).

Si une règle de spécification existe mais l'implémentation la contredit, mettez à jour la spécification (pas l'implémentation) sauf si vous êtes certain que l'implémentation est correcte. La spécification est le contrat ; la dérive de code se produit ; le processus de rédaction de spécification est votre chance de mettre au jour et de résoudre le problème.

8. Vérifier

Exécutez, dans l'ordre : les tests du package, yarn format-current puis yarn lint, yarn typecheck depuis la racine du repo, yarn api-check, et les suites de packages dépendants de l'étape 7.

9. PR

Suivez skills/write-pr/. Modelez le corps sur #9172 :

  • paragraphe d'introduction expliquant le système de spécification/citation ;
  • une section corrections par commit ;
  • une section sur les suites fusionnées et la nouvelle couverture ;
  • notes de version uniquement pour les changements de comportement ;
  • un tableau de changements de code. Attention : git diff --numstat les lignes de renommage cassent la correspondance de chemin naïve ; utilisez le dernier champ.

10. Notes de suivi

Terminez en rapportant : les problèmes latents que vous avez choisi de ne pas corriger, les particularités que les futurs rédacteurs de tests doivent connaître, et tout ce qu'un relecteur devrait revérifier.

Rapportez les comptages honnêtement, partout où ils apparaissent (message de commit, corps de PR, rapport final) : le comptage de tests de la suite reconstruite et le comptage des règles, mesurés avec grep, jamais le total du package. Compter les suites inchangées (fuzz en particulier) comme votre propre travail dénature le changement.

Skills similaires