[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"doc:en:plugins\u002Ftesting":3},{"title":4,"lede":5,"html":6,"headings":7},"Testing a plugin","Escouade gives plugin authors a command, escouade-plugin, that runs a plugin without the app: the conformance bench checks it against what Escouade expects of it, and the fake Escouade sends it facts and requests by hand. The bench is the one Escouade plays on its own plugins.","\u003Ch2 id=\"the-command\">The command\u003C\u002Fh2>\n\u003Cp>\u003Ccode class=\"key\">escouade-plugin\u003C\u002Fcode> ships with Escouade, beside its program: \u003Ccode>%LOCALAPPDATA%\\Escouade\\escouade-plugin.exe\u003C\u002Fcode> on Windows, \u003Ccode>Escouade.app\u002FContents\u002FMacOS\u002Fescouade-plugin\u003C\u002Fcode> on macOS, \u003Ccode>\u002Fusr\u002Fbin\u002Fescouade-plugin\u003C\u002Fcode> with the \u003Ccode class=\"key\">.deb\u003C\u002Fcode> package. For a \u003Ccode class=\"key\">run.js\u003C\u002Fcode> or \u003Ccode class=\"key\">run.wasm\u003C\u002Fcode> plugin, it needs neither Node nor Rust: it compiles its module as the store does and plays it in WASM, in the app’s host. For a plugin started by a command, it needs what that program needs to start.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin validate my-plugin   # the manifest, field by field\nescouade-plugin test my-plugin       # the conformance bench\nescouade-plugin dev my-plugin        # the fake Escouade\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--lang fr\u003C\u002Fcode> or \u003Ccode>--lang en\u003C\u002Fcode> picks the language of its messages; without it, the system’s.\u003C\u002Fp>\n\u003Cp>\u003Ccode>--escouade &lt;version&gt;\u003C\u002Fcode> plays the plugin as that version of Escouade, with \u003Ccode class=\"key\">validate\u003C\u002Fcode>, \u003Ccode class=\"key\">test\u003C\u002Fcode> and \u003Ccode class=\"key\">dev\u003C\u002Fcode>: the manifest’s \u003Ccode class=\"key\">escouade\u003C\u002Fcode> range is checked against it, and it is what the plugin sees in \u003Ccode class=\"key\">initialize\u003C\u002Fcode>. Without it, the command’s own version. Use it to write for a version to come:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin test my-plugin --escouade 2.2.0\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-conformance-bench\">The conformance bench\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin test &lt;folder&gt;\u003C\u002Fcode> reads the manifest, starts the program of \u003Ccode class=\"key\">run\u003C\u002Fcode> the way Escouade starts it (in its folder, with its three variables, with everything it starts), and plays up to eleven scenarios. Each says what happened; a scenario that fails also says \u003Cstrong>what is missing\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Scenario\u003C\u002Fth>\n\u003Cth>What the bench does\u003C\u002Fth>\n\u003Cth>What it expects\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Manifest\u003C\u002Ftd>\n\u003Ctd>Reads \u003Ccode class=\"key\">escouade-plugin.json\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>A manifest that holds, for this version of Escouade.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Initialization\u003C\u002Ftd>\n\u003Ctd>Sends \u003Ccode class=\"key\">initialize\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{&quot;protocolVersion&quot;: 1, &quot;capabilities&quot;: {…}}\u003C\u002Fcode> within 10 s.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Version refused\u003C\u002Ftd>\n\u003Ctd>Starts the plugin again and offers it version 999 of the protocol.\u003C\u002Ftd>\n\u003Ctd>Its own version, or an error: never the version offered.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Delays\u003C\u002Ftd>\n\u003Ctd>Times every call of the bench, and calls \u003Ccode class=\"key\">bench\u002Funknown\u003C\u002Fcode>, which no plugin serves.\u003C\u002Ftd>\n\u003Ctd>Each answer within its method’s delay; the error \u003Ccode class=\"key\">-32601\u003C\u002Fcode> at once to an unknown method.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A fact received\u003C\u002Ftd>\n\u003Ctd>Sends a fact of \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode> (the notification \u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode>), then a call.\u003C\u002Ftd>\n\u003Ctd>No answer to the notification, no crash, and the answer to the next call.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A decision\u003C\u002Ftd>\n\u003Ctd>Sends \u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode> for an \u003Ccode>npm test\u003C\u002Fcode> command.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{&quot;decision&quot;: &quot;allow&quot; | &quot;deny&quot; | &quot;ask&quot;}\u003C\u002Fcode>, with a \u003Ccode class=\"key\">message\u003C\u002Fcode> for a denial.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A secret\u003C\u002Ftd>\n\u003Ctd>Serves \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode> throughout the bench, with a test value.\u003C\u002Ftd>\n\u003Ctd>Only the keys of \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode>, and never their value written on standard error or in a \u003Ccode class=\"key\">log\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Crash\u003C\u002Ftd>\n\u003Ctd>Kills the plugin and everything it started in the middle of a call, then starts it again.\u003C\u002Ftd>\n\u003Ctd>A new process that initializes like the first.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Shutdown\u003C\u002Ftd>\n\u003Ctd>Sends \u003Ccode class=\"key\">shutdown\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>An answer within a second, then the end of the process.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Ticket source\u003C\u002Ftd>\n\u003Ctd>Only if \u003Ccode>contributes.ticketSources\u003C\u002Fcode> is declared: \u003Ccode class=\"key\">account\u002Fme\u003C\u002Fcode> and \u003Ccode class=\"key\">tickets\u002Fcontainers\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>An answer within the delay, or an error the contract names: \u003Ccode>-(32000 + HTTP status)\u003C\u002Fcode> (\u003Ccode class=\"key\">-32401\u003C\u002Fcode>: token refused), \u003Ccode class=\"key\">-32010\u003C\u002Fcode> (a lasting refusal), \u003Ccode class=\"key\">-32601\u003C\u002Fcode> (a call it does not serve).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Code host\u003C\u002Ftd>\n\u003Ctd>Only if \u003Ccode class=\"key\">contributes.forges\u003C\u002Fcode> is declared: \u003Ccode class=\"key\">account\u002Fme\u003C\u002Fcode> and \u003Ccode>forge\u002FpullRequestsOfBranch\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003Ctd>The same.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>A scenario that does not concern the plugin is not played, and says so: no fact without \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>, no decision without \u003Ccode class=\"key\">permissions.decide\u003C\u002Fcode>, no source or host without \u003Ccode>contributes.ticketSources\u003C\u002Fcode> or \u003Ccode class=\"key\">contributes.forges\u003C\u002Fcode>, and nothing of a program for a declarative plugin, of which only the manifest is read.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-text\">Conformance bench: dev.exemple.slow-rules 1.0.0\n\n✓ Manifest         holds, with a program\n✓ Initialization   answers in 61 ms, protocol 1\n✓ Version refused  offered version 999, it answers its own (1): Escouade stops it\n✗ Delays           permissions\u002Fdecide: answered in 812 ms, past its delay of 500 ms\n                   What is missing: answer within each method&#39;s delay (500 ms for a decision, 5 s in general); past it, Escouade no longer waits and its default answer applies.\n– A fact received  it asks for no fact (`permissions.events`)\n✓ A decision       &quot;allow&quot; in 812 ms\n– A secret         it asks for no secret (`permissions.secrets`)\n✓ Crash            killed in the middle of a call, it starts again: answers in 58 ms, protocol 1\n✓ Shutdown         answers in 2 ms, then ends\n\n6 passed, 1 failed, 2 not played\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The exit code is \u003Ccode class=\"key\">1\u003C\u002Fcode> when a scenario fails, \u003Ccode class=\"key\">0\u003C\u002Fcode> otherwise: the command works as is in CI.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode class=\"key\">--json\u003C\u002Fcode> gives the report in JSON: each scenario, its verdict (\u003Ccode class=\"key\">pass\u003C\u002Fcode>, \u003Ccode class=\"key\">fail\u003C\u002Fcode>, \u003Ccode class=\"key\">skip\u003C\u002Fcode>), what happened, what is missing, how long each call took, and what the plugin did on its own (its standard error, its logs, the secrets it asked for; the last 200). That is what the “Verified by Escouade” review reads, which plays the bench in a container without network: a connection or a write refused shows there.\u003C\u002Fli>\n\u003Cli>\u003Ccode>--measure [n]\u003C\u002Fcode> then measures \u003Ccode class=\"key\">n\u003C\u002Fcode> round trips (1,000 by default) and gives their median and 99th percentile, in microseconds.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"the-bench-s-facts-and-decisions\">The bench’s facts and decisions\u003C\u002Fh3>\n\u003Cp>The bench sends facts and decisions in the shape Escouade will give them: the project, the ticket and the agent by their ids and names, never the 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>A fact is a notification: it has no \u003Ccode class=\"key\">id\u003C\u002Fcode>, and the plugin does not answer it. The facts a plugin receives in Escouade, and their fields, come with the events, in an upcoming version; ignore the fields you do not know.\u003C\u002Fp>\n\u003Ch2 id=\"the-fake-escouade\">The fake Escouade\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin dev &lt;folder&gt;\u003C\u002Fcode> starts the plugin, initializes it, then sends what you type, line by line, and shows what comes back: its answers and how long they took, the secrets it asks for, its \u003Ccode class=\"key\">log\u003C\u002Fcode> lines, its standard error.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Line\u003C\u002Fth>\n\u003Cth>What is sent\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode>fact &lt;name&gt; [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The fact \u003Ccode class=\"key\">&lt;name&gt;\u003C\u002Fcode> (\u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode>); the JSON replaces its fields, for instance \u003Ccode>fact ticket.column {&quot;project&quot;:{&quot;id&quot;:&quot;p&quot;,&quot;name&quot;:&quot;Private&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>A permission request (\u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode>), and its answer; an answer past 500 ms is pointed out.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>call &lt;method&gt; [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A call, and its answer.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>notify &lt;method&gt; [json]\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A notification.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">restart\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Kills the plugin and starts it again.\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>, then the end.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>--secret &lt;key&gt;=&lt;value&gt;\u003C\u002Fcode> gives a secret’s value (repeatable); a key of the plugin without a value gets \u003Ccode class=\"key\">null\u003C\u002Fcode>, as in Escouade when nothing is kept, and a key outside its \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode> is refused with \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=\"a-plugin-in-rust\">A plugin in Rust\u003C\u002Fh2>\n\u003Cp>Three levels, from the quickest to the truest:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">cargo test                                 # the plugin, natively, on your machine\ncargo check --target wasm32-wasip1         # it compiles as a module (rustup target add wasm32-wasip1)\nescouade-plugin test my-plugin             # the bench, on the module\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Cstrong>\u003Ccode>cargo test\u003C\u002Fcode>\u003C\u002Fstrong> runs natively, with tokio and reqwest: the same source as the module, without its host. The kit’s \u003Ccode class=\"key\">serve_on\u003C\u002Fcode> plays the plugin on lines in memory, the ones Escouade sends. It needs a C compiler, for \u003Ccode class=\"key\">ring\u003C\u002Fcode>. The template’s tests:\n\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Ftests.rs\u003C\u002Fcode> (excerpt)\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> is the module’s target. \u003Ccode>cargo check --target wasm32-wasip1\u003C\u002Fcode> says fast what does not compile there: \u003Ccode class=\"key\">tokio\u003C\u002Fcode>, \u003Ccode class=\"key\">std::process\u003C\u002Fcode>, a dependency that assumes a system. It is not the published module: that one comes out of \u003Ccode>escouade-plugin build\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The bench and the fake Escouade\u003C\u002Fstrong> compile the module as the store does, then play it in the app’s host. A store refusal shows here: no \u003Ccode class=\"key\">Cargo.lock\u003C\u002Fcode>, a crate off the list. The toolchain, rustc 1.98.0 and \u003Ccode class=\"key\">wasm32-wasip1\u003C\u002Fcode>, is downloaded the first time it is needed into \u003Ccode class=\"key\">~\u002F.escouade\u002Ftools\u003C\u002Fcode> (650 MB, a minute) and checked at each use, without rustup. \u003Ccode>ESCOUADE_RUST_TOOLCHAIN\u003C\u002Fcode> names another one.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Ccode>--wasm &lt;module&gt;\u003C\u002Fcode> plays a given module instead of compiling; \u003Ccode class=\"key\">--offline\u003C\u002Fcode> refuses every network call, as the review does; \u003Ccode>--account &lt;address&gt;\u003C\u002Fcode> gives \u003Ccode class=\"key\">{account}\u003C\u002Fcode> an address. The bench serves \u003Ccode class=\"key\">fs\u002F*\u003C\u002Fcode> and \u003Ccode class=\"key\">proc\u002F*\u003C\u002Fcode> around an agent of its own, and \u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>. It does not serve \u003Ccode class=\"key\">settings\u002Fget\u003C\u002Fcode>, which answers \u003Ccode class=\"key\">-32601\u003C\u002Fcode>: a plugin must survive that.\u003C\u002Fp>\n\u003Ch2 id=\"in-ci\">In CI\u003C\u002Fh2>\n\u003Cp>The bench fits in one step: install Escouade, then\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sh\">escouade-plugin validate .\nescouade-plugin test .\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Escouade’s tests play this bench on its fake plugin and on its own plugins (\u003Ccode class=\"key\">plugins\u002F\u003C\u002Fcode> of its repository), with \u003Ccode class=\"key\">--first-party\u003C\u002Fcode>: their ids \u003Ccode class=\"key\">dev.escouade.*\u003C\u002Fcode> are reserved, the bench refuses them to any other plugin. The \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fsdk\u002F\">TypeScript SDK\u003C\u002Fa> (\u003Ccode class=\"key\">@escouade-dev\u002Fplugin\u003C\u002Fcode>) installs the same command in a plugin’s \u003Ccode class=\"key\">node_modules\u003C\u002Fcode>, and its CI plays this bench on the templates of \u003Ccode>npm create escouade-plugin\u003C\u002Fcode>.\u003C\u002Fp>\n",[8,12,15,19,22,25],{"id":9,"text":10,"depth":11},"the-command","The command",2,{"id":13,"text":14,"depth":11},"the-conformance-bench","The conformance bench",{"id":16,"text":17,"depth":18},"the-bench-s-facts-and-decisions","The bench’s facts and decisions",3,{"id":20,"text":21,"depth":11},"the-fake-escouade","The fake Escouade",{"id":23,"text":24,"depth":11},"a-plugin-in-rust","A plugin in Rust",{"id":26,"text":27,"depth":11},"in-ci","In CI",1793889382425]