[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"doc:fr:plugins\u002Ftesting":3},{"title":4,"lede":5,"html":6,"headings":7},"Tester un plugin","Escouade donne aux auteurs de plugins une commande, escouade-plugin, qui fait tourner un plugin sans l’app : le banc de conformité le passe au crible de ce qu’Escouade attend de lui, et le faux Escouade lui envoie des faits et des demandes à la main. Le banc est celui que joue Escouade sur ses propres plugins.","\u003Ch2 id=\"la-commande\">La commande\u003C\u002Fh2>\n\u003Cp>\u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> est livrée avec Escouade, à côté de son programme : \u003Ccode>%LOCALAPPDATA%\\Escouade\\escouade-plugin.exe\u003C\u002Fcode> sous Windows, \u003Ccode>Escouade.app\u002FContents\u002FMacOS\u002Fescouade-plugin\u003C\u002Fcode> sous macOS, \u003Ccode>\u002Fusr\u002Fbin\u002Fescouade-plugin\u003C\u002Fcode> avec le paquet \u003Ccode class=\"key\">.deb\u003C\u002Fcode>. Pour un plugin \u003Ccode class=\"key\">run.js\u003C\u002Fcode> ou \u003Ccode class=\"key\">run.wasm\u003C\u002Fcode>, elle n’a besoin ni de Node ni de Rust : elle compile son module comme le store et le joue en WASM, dans l’hôte de l’app. Pour un plugin lancé par une commande, il lui faut ce qu’il faut à ce programme pour démarrer.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin validate mon-plugin   # le manifeste, champ par champ\nescouade-plugin test mon-plugin       # le banc de conformité\nescouade-plugin dev mon-plugin        # le faux Escouade\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--lang fr\u003C\u002Fcode> ou \u003Ccode>--lang en\u003C\u002Fcode> choisit la langue de ses messages ; sans lui, c’est celle du système.\u003C\u002Fp>\n\u003Cp>\u003Ccode>--escouade &lt;version&gt;\u003C\u002Fcode> joue le plugin comme cette version d’Escouade, avec \u003Ccode class=\"key\">validate\u003C\u002Fcode>, \u003Ccode class=\"key\">test\u003C\u002Fcode> et \u003Ccode class=\"key\">dev\u003C\u002Fcode> : la plage \u003Ccode class=\"key\">escouade\u003C\u002Fcode> du manifeste se contrôle contre elle, et c’est elle que le plugin voit dans \u003Ccode class=\"key\">initialize\u003C\u002Fcode>. Sans lui, c’est la version de la commande. Il sert à écrire pour une version à venir :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin test mon-plugin --escouade 2.2.0\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"le-banc-de-conformite\">Le banc de conformité\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin test &lt;dossier&gt;\u003C\u002Fcode> lit le manifeste, lance le programme de \u003Ccode class=\"key\">run\u003C\u002Fcode> comme Escouade le lance (dans son dossier, avec ses trois variables, avec tout ce qu’il démarre), et joue jusqu’à onze scénarios. Chacun dit ce qui s’est passé ; un scénario qui échoue dit aussi \u003Cstrong>ce qui manque\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Scénario\u003C\u002Fth>\n\u003Cth>Ce que le banc fait\u003C\u002Fth>\n\u003Cth>Ce qu’il attend\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Manifeste\u003C\u002Ftd>\n\u003Ctd>Lit \u003Ccode class=\"key\">escouade-plugin.json\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>Un manifeste valide, pour cette version d’Escouade.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Initialisation\u003C\u002Ftd>\n\u003Ctd>Envoie \u003Ccode class=\"key\">initialize\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{&quot;protocolVersion&quot;: 1, &quot;capabilities&quot;: {…}}\u003C\u002Fcode> en moins de 10 s.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Version refusée\u003C\u002Ftd>\n\u003Ctd>Relance le plugin et lui offre la version 999 du protocole.\u003C\u002Ftd>\n\u003Ctd>Sa propre version, ou une erreur : jamais la version offerte.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Délais\u003C\u002Ftd>\n\u003Ctd>Mesure chaque appel du banc, et appelle \u003Ccode class=\"key\">bench\u002Funknown\u003C\u002Fcode>, qu’aucun plugin ne sert.\u003C\u002Ftd>\n\u003Ctd>Chaque réponse dans le délai de sa méthode ; l’erreur \u003Ccode class=\"key\">-32601\u003C\u002Fcode> tout de suite à une méthode inconnue.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Un fait reçu\u003C\u002Ftd>\n\u003Ctd>Envoie un fait de \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode> (la notification \u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode>), puis un appel.\u003C\u002Ftd>\n\u003Ctd>Pas de réponse à la notification, pas de plantage, et la réponse à l’appel suivant.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Une décision\u003C\u002Ftd>\n\u003Ctd>Envoie \u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode> pour une commande \u003Ccode>npm test\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{&quot;decision&quot;: &quot;allow&quot; | &quot;deny&quot; | &quot;ask&quot;}\u003C\u002Fcode>, avec un \u003Ccode class=\"key\">message\u003C\u002Fcode> pour un refus.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Un secret\u003C\u002Ftd>\n\u003Ctd>Sert \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode> pendant tout le banc, avec une valeur de test.\u003C\u002Ftd>\n\u003Ctd>Seulement les clés de \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode>, et jamais leur valeur écrite sur la sortie d’erreur ou dans un \u003Ccode class=\"key\">log\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Plantage\u003C\u002Ftd>\n\u003Ctd>Tue le plugin et tout ce qu’il a lancé au milieu d’un appel, puis le relance.\u003C\u002Ftd>\n\u003Ctd>Un nouveau process qui s’initialise comme le premier.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Fermeture\u003C\u002Ftd>\n\u003Ctd>Envoie \u003Ccode class=\"key\">shutdown\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>Une réponse en moins d’une seconde, puis la fin du process.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Source de tickets\u003C\u002Ftd>\n\u003Ctd>Seulement si \u003Ccode>contributes.ticketSources\u003C\u002Fcode> est déclaré : \u003Ccode class=\"key\">account\u002Fme\u003C\u002Fcode> et \u003Ccode class=\"key\">tickets\u002Fcontainers\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>Une réponse dans le délai, ou une erreur que le contrat nomme : \u003Ccode>-(32000 + statut HTTP)\u003C\u002Fcode> (\u003Ccode class=\"key\">-32401\u003C\u002Fcode> : jeton refusé), \u003Ccode class=\"key\">-32010\u003C\u002Fcode> (un refus durable), \u003Ccode class=\"key\">-32601\u003C\u002Fcode> (un appel qu’il ne sert pas).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Hébergeur\u003C\u002Ftd>\n\u003Ctd>Seulement si \u003Ccode class=\"key\">contributes.forges\u003C\u002Fcode> est déclaré : \u003Ccode class=\"key\">account\u002Fme\u003C\u002Fcode> et \u003Ccode>forge\u002FpullRequestsOfBranch\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>La même.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Un scénario qui ne concerne pas le plugin n’est pas joué et le dit : pas de fait sans \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>, pas de décision sans \u003Ccode class=\"key\">permissions.decide\u003C\u002Fcode>, pas de source ni d’hébergeur sans \u003Ccode>contributes.ticketSources\u003C\u002Fcode> ou \u003Ccode class=\"key\">contributes.forges\u003C\u002Fcode>, et rien d’un programme pour un plugin déclaratif, dont seul le manifeste est lu.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-text\">Banc de conformité : dev.exemple.slow-rules 1.0.0\n\n✓ Manifeste        valide, avec un programme\n✓ Initialisation   répond en 61 ms, protocole 1\n✓ Version refusée  offerte la version 999, il répond la sienne (1) : Escouade l&#39;arrête\n✗ Délais           permissions\u002Fdecide : répondu en 812 ms, au-delà de son délai de 500 ms\n                   Ce qui manque : répondre dans le délai de chaque méthode (500 ms pour une décision, 5 s en général) ; passé ce délai, Escouade n&#39;attend plus et sa réponse par défaut s&#39;applique.\n– Un fait reçu     il ne demande aucun fait (`permissions.events`)\n✓ Une décision     « allow » en 812 ms\n– Un secret        il ne demande aucun secret (`permissions.secrets`)\n✓ Plantage         tué au milieu d&#39;un appel, il redémarre : répond en 58 ms, protocole 1\n✓ Fermeture        répond en 2 ms, puis se termine\n\n6 réussis, 1 échoués, 2 sans objet\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Le code de sortie est \u003Ccode class=\"key\">1\u003C\u002Fcode> si un scénario échoue, \u003Ccode class=\"key\">0\u003C\u002Fcode> sinon : la commande sert telle quelle en CI.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode class=\"key\">--json\u003C\u002Fcode> donne le rapport en JSON : chaque scénario, son verdict (\u003Ccode class=\"key\">pass\u003C\u002Fcode>, \u003Ccode class=\"key\">fail\u003C\u002Fcode>, \u003Ccode class=\"key\">skip\u003C\u002Fcode>), ce qui s’est passé, ce qui manque, la durée de chaque appel, et ce que le plugin a fait de lui-même (sa sortie d’erreur, ses journaux, les secrets demandés ; les 200 derniers). C’est ce que lit la revue « Vérifié par Escouade », qui passe le banc dans un conteneur sans réseau : une connexion ou une écriture refusée s’y voit.\u003C\u002Fli>\n\u003Cli>\u003Ccode>--measure [n]\u003C\u002Fcode> mesure ensuite \u003Ccode class=\"key\">n\u003C\u002Fcode> allers-retours (1 000 par défaut) et donne leur médiane et leur 99e centile, en microsecondes.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"les-faits-et-les-decisions-du-banc\">Les faits et les décisions du banc\u003C\u002Fh3>\n\u003Cp>Le banc envoie les faits et les décisions dans la forme que leur donnera Escouade : le projet, le ticket et l’agent par leurs ids et leurs noms, jamais la conversation.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;events\u002Ffact&quot;,&quot;params&quot;:{&quot;name&quot;:&quot;ticket.column&quot;,&quot;version&quot;:1,&quot;at&quot;:&quot;2026-10-29T10:00:00Z&quot;,&quot;project&quot;:{&quot;id&quot;:&quot;bench-project&quot;,&quot;name&quot;:&quot;Banc&quot;,&quot;private&quot;:false},&quot;ticket&quot;:{&quot;id&quot;:&quot;bench-ticket&quot;,&quot;key&quot;:&quot;BEN-1&quot;,&quot;title&quot;:&quot;Un ticket du banc&quot;,&quot;from&quot;:&quot;doing&quot;,&quot;column&quot;:&quot;test&quot;},&quot;agent&quot;:{&quot;id&quot;:&quot;bench-agent&quot;,&quot;name&quot;:&quot;Agent du banc&quot;}}}\n{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:3,&quot;method&quot;:&quot;permissions\u002Fdecide&quot;,&quot;params&quot;:{&quot;request&quot;:{&quot;id&quot;:&quot;bench-request&quot;,&quot;kind&quot;:&quot;command&quot;,&quot;tool&quot;:&quot;Bash&quot;,&quot;input&quot;:{&quot;command&quot;:&quot;npm test&quot;}},&quot;project&quot;:{&quot;id&quot;:&quot;bench-project&quot;,&quot;name&quot;:&quot;Banc&quot;,&quot;private&quot;:false},&quot;ticket&quot;:{&quot;id&quot;:&quot;bench-ticket&quot;,&quot;key&quot;:&quot;BEN-1&quot;,&quot;title&quot;:&quot;Un ticket du banc&quot;},&quot;agent&quot;:{&quot;id&quot;:&quot;bench-agent&quot;,&quot;name&quot;:&quot;Agent du banc&quot;}}}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Un fait est une notification : il n’a pas d’\u003Ccode class=\"key\">id\u003C\u002Fcode>, et le plugin n’y répond pas. Les faits qu’un plugin reçoit dans Escouade, et leurs champs, arrivent avec les événements, dans une prochaine version ; ignore les champs que tu ne connais pas.\u003C\u002Fp>\n\u003Ch2 id=\"le-faux-escouade\">Le faux Escouade\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin dev &lt;dossier&gt;\u003C\u002Fcode> lance le plugin, l’initialise, puis envoie ce que tu tapes, ligne par ligne, et montre ce qui revient : ses réponses avec leur durée, ses demandes de secrets, ses \u003Ccode class=\"key\">log\u003C\u002Fcode>, sa sortie d’erreur.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Ligne\u003C\u002Fth>\n\u003Cth>Ce qui part\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode>fact &lt;nom&gt; [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Le fait \u003Ccode class=\"key\">&lt;nom&gt;\u003C\u002Fcode> (\u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode>) ; le JSON remplace ses champs, par exemple \u003Ccode>fact ticket.column {&quot;project&quot;:{&quot;id&quot;:&quot;p&quot;,&quot;name&quot;:&quot;Privé&quot;,&quot;private&quot;:true}}\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>decide [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une demande d’autorisation (\u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode>), et sa réponse ; une réponse au-delà de 500 ms est signalée.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>call &lt;méthode&gt; [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un appel, et sa réponse.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>notify &lt;méthode&gt; [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une notification.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">restart\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Tue le plugin et le relance.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">quit\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">shutdown\u003C\u002Fcode>, puis la fin.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>--secret &lt;clé&gt;=&lt;valeur&gt;\u003C\u002Fcode> donne la valeur d’un secret (répétable) ; une clé du plugin sans valeur reçoit \u003Ccode class=\"key\">null\u003C\u002Fcode>, comme dans Escouade quand rien n’est gardé, et une clé hors de ses \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode> est refusée par \u003Ccode class=\"key\">-32001\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-text\">$ escouade-plugin dev slack --secret webhook=https:\u002F\u002Fexample.invalid\nslack\u002Fslack.cmd (pid 30112)\n→ initialize\n← {&quot;capabilities&quot;:{},&quot;protocolVersion&quot;:1}  (47 ms)\n&gt; fact ticket.column\n→ events\u002Ffact {&quot;agent&quot;:{…},&quot;name&quot;:&quot;ticket.column&quot;,&quot;project&quot;:{…},&quot;ticket&quot;:{…},&quot;version&quot;:1}\n→ secrets\u002Fget &quot;webhook&quot;\n→ log [info] no Slack webhook kept: nothing sent\n&gt; call nope {&quot;a&quot;:1}\n← error -32601 unknown method nope  (0 ms)\n&gt; quit\n→ shutdown\n← null  (0 ms)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"un-plugin-en-rust\">Un plugin en Rust\u003C\u002Fh2>\n\u003Cp>Trois niveaux, du plus rapide au plus vrai :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">cargo test                                 # le plugin, en natif, sur ta machine\ncargo check --target wasm32-wasip1         # il compile en module (rustup target add wasm32-wasip1)\nescouade-plugin test mon-plugin            # le banc, sur le module\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Cstrong>\u003Ccode>cargo test\u003C\u002Fcode>\u003C\u002Fstrong> tourne en natif, avec tokio et reqwest : la même source que le module, sans son hôte. \u003Ccode class=\"key\">serve_on\u003C\u002Fcode> du kit joue le plugin sur des lignes en mémoire, les mêmes qu’envoie Escouade. Il lui faut un compilateur C, pour \u003Ccode class=\"key\">ring\u003C\u002Fcode>. Les tests du modèle :\n\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Ftests.rs\u003C\u002Fcode> (extrait)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F What the plugin writes for `messages`, one message a line.\nfn play(messages: &amp;[Value]) -&gt; Vec&lt;Value&gt; {\n    let input: String = messages.iter().map(|m| format!(&quot;{m}\\n&quot;)).collect();\n    let runtime = tokio::runtime::Builder::new_current_thread()\n        .enable_all()\n        .build()\n        .unwrap();\n    runtime.block_on(async {\n        let (output, written) = tokio::io::duplex(64 * 1024);\n        serve_on(Arc::new(Template), input.as_bytes(), output)\n            .await\n            .unwrap();\n        let mut lines = BufReader::new(written).lines();\n        let mut read = Vec::new();\n        while let Some(line) = lines.next_line().await.unwrap() {\n            read.push(serde_json::from_str(&amp;line).unwrap());\n        }\n        read\n    })\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Cstrong>\u003Ccode class=\"key\">wasm32-wasip1\u003C\u002Fcode>\u003C\u002Fstrong> est la cible du module. \u003Ccode>cargo check --target wasm32-wasip1\u003C\u002Fcode> dit vite ce qui n’y compile pas : \u003Ccode class=\"key\">tokio\u003C\u002Fcode>, \u003Ccode class=\"key\">std::process\u003C\u002Fcode>, une dépendance qui suppose un système. Ce n’est pas le module publié : celui-là sort de \u003Ccode>escouade-plugin build\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Le banc et le faux Escouade\u003C\u002Fstrong> compilent le module comme le store, puis le jouent dans l’hôte de l’app. Une erreur du store se voit donc ici : pas de \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode>, un crate hors de la liste. La chaîne d’outils, rustc 1.98.0 et \u003Ccode class=\"key\">wasm32-wasip1\u003C\u002Fcode>, est téléchargée au premier besoin dans \u003Ccode class=\"key\">~\u002F.escouade\u002Ftools\u003C\u002Fcode> (650 Mo, une minute) et vérifiée à chaque usage, sans rustup. \u003Ccode>ESCOUADE_RUST_TOOLCHAIN\u003C\u002Fcode> en nomme une autre.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Ccode>--wasm &lt;module&gt;\u003C\u002Fcode> joue un module donné au lieu de compiler ; \u003Ccode class=\"key\">--offline\u003C\u002Fcode> refuse tout appel réseau, comme la revue ; \u003Ccode>--account &lt;adresse&gt;\u003C\u002Fcode> donne une adresse à \u003Ccode class=\"key\">{account}\u003C\u002Fcode>. Le banc sert \u003Ccode class=\"key\">fs\u002F*\u003C\u002Fcode> et \u003Ccode class=\"key\">proc\u002F*\u003C\u002Fcode> autour d’un agent à lui, et \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>. Il ne sert pas \u003Ccode class=\"key\">settings\u002Fget\u003C\u002Fcode>, qui répond \u003Ccode class=\"key\">-32601\u003C\u002Fcode> : un plugin doit y survivre.\u003C\u002Fp>\n\u003Ch2 id=\"en-ci\">En CI\u003C\u002Fh2>\n\u003Cp>Le banc tient dans une étape : installe Escouade (ou seulement \u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode>, à côté de l’app), puis\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin validate .\nescouade-plugin test .\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Les tests d’Escouade jouent ce banc sur son faux plugin et sur ses propres plugins (\u003Ccode class=\"key\">plugins\u002F\u003C\u002Fcode> de son dépôt), avec \u003Ccode class=\"key\">--first-party\u003C\u002Fcode> : leurs identifiants \u003Ccode class=\"key\">dev.escouade.*\u003C\u002Fcode> sont réservés, le banc les refuse à tout autre plugin. Le \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Fsdk\u002F\">SDK en TypeScript\u003C\u002Fa> (\u003Ccode class=\"key\">@escouade-dev\u002Fplugin\u003C\u002Fcode>) installe la même commande dans les \u003Ccode class=\"key\">node_modules\u003C\u002Fcode> d’un plugin, et sa CI joue ce banc sur les gabarits de \u003Ccode>npm create escouade-plugin\u003C\u002Fcode>.\u003C\u002Fp>\n",[8,12,15,19,22,25],{"id":9,"text":10,"depth":11},"la-commande","La commande",2,{"id":13,"text":14,"depth":11},"le-banc-de-conformite","Le banc de conformité",{"id":16,"text":17,"depth":18},"les-faits-et-les-decisions-du-banc","Les faits et les décisions du banc",3,{"id":20,"text":21,"depth":11},"le-faux-escouade","Le faux Escouade",{"id":23,"text":24,"depth":11},"un-plugin-en-rust","Un plugin en Rust",{"id":26,"text":27,"depth":11},"en-ci","En CI",1793889379755]