<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Parallelisation on Ocarina — précis</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/tags/parallelisation/</link>
    <description>Recent content in Parallelisation on Ocarina — précis</description>
    <generator>Hugo</generator>
    <language>fr-FR</language>
    <lastBuildDate>Thu, 04 Jun 2026 17:04:59 +0200</lastBuildDate>
    <atom:link href="https://mojo-molotov.github.io/from-ocarina-to-igor/fr/tags/parallelisation/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>02.05.04 — TestSuite[Driver] — moteur parallélisé</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/04-test-suite/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/04-test-suite/</guid>
      <description>&lt;h1 id=&#34;020504testsuitedrivermoteur-parallélisé&#34;&gt;02.05.04 — &lt;code&gt;TestSuite[Driver]&lt;/code&gt; — moteur parallélisé&lt;a class=&#34;anchor&#34; href=&#34;#020504testsuitedrivermoteur-parall%c3%a9lis%c3%a9&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test_suite.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test_suite.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Responsabilité unique&lt;/strong&gt; : exécuter une séquence de &lt;code&gt;Test&lt;/code&gt; en parallèle, gérer la &lt;em&gt;saturation&lt;/em&gt;, le filtrage par IDs, et la validation des invariants pré-exécution.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;constructeur&#34;&gt;Constructeur&lt;a class=&#34;anchor&#34; href=&#34;#constructeur&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tests: Sequence[Test[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    create_logger: Thunk[ILogger],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drivers_pool: WebDriversPool[Driver],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    take_screenshot: ITakeScreenshot[Driver],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    act_counter: ActCounter &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transient_errors: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt;[Exception], &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    copy_indicator: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;COPY&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    put_space_after_copy_indicator: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_retries_per_test: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    autoscreen_on_fail: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    only_ids: Iterable[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    exclude_ids: Iterable[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_guards_on_invoke(tests)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# ... assignations ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_tests &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; filter_tests_by_ids(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        tests,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        only&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;only_ids,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        exclude&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;exclude_ids,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;create_logger()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;set_prefix(&lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;: filtering tests...&amp;#34;&lt;/span&gt;),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Paramètre&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;          &lt;th&gt;Défaut&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Nom de la suite (apparaît dans logs &amp;amp; rapports)&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;tests&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sequence[Test[Driver]]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Liste des tests à exécuter&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_logger&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Thunk[ILogger]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Factory de logger&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;drivers_pool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;WebDriversPool[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Pool partagée&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;take_screenshot&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ITakeScreenshot[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Callable &lt;code&gt;(driver, logger, category) -&amp;gt; None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;act_counter&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ActCounter | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Si &lt;code&gt;None&lt;/code&gt; → &lt;code&gt;ThreadsBasedActCounter()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;transient_errors&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;tuple[type[Exception], ...]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Exceptions qui déclenchent un retry&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;copy_indicator&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Préfixe pour les tests clonés en saturation&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;&amp;quot;COPY&amp;quot;&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;put_space_after_copy_indicator&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[COPY 1]&lt;/code&gt; (&lt;code&gt;True&lt;/code&gt;) vs &lt;code&gt;[COPY1]&lt;/code&gt; (&lt;code&gt;False&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;max_retries_per_test&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Nombre maximum de retentatives d&amp;rsquo;un test potentiellement flaky&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt; → &lt;code&gt;_DEFAULT_MAX_RETRIES_PER_TEST&lt;/code&gt; (8)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;autoscreen_on_fail&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Capture automatiquement un ou des screenshots (rafale) en cas d&amp;rsquo;échec&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;saturate_workers&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Clonage des tests pour saturer tous les workers si la pool est suffisamment grosse&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt; → cascade : suite &amp;gt; campaign &amp;gt; bootstrap&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;only_ids&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Iterable[str]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Filtre &lt;code&gt;--only&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;exclude_ids&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Iterable[str]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Filtre &lt;code&gt;--exclude&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;garde-fous&#34;&gt;Garde-fous&lt;a class=&#34;anchor&#34; href=&#34;#garde-fous&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;au-moment-du-__init__&#34;&gt;Au moment du &lt;code&gt;__init__&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#au-moment-du-__init__&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_guards_on_invoke&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, tests: Sequence[Test[Driver]]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    validate_test_runners_ids(tests&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;tests, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;tests&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ Tous les &lt;code&gt;test_id&lt;/code&gt; doivent être uniques. Sinon : &lt;code&gt;AggregateInvariantViolationError&lt;/code&gt;. &lt;em&gt;Levé dès la construction&lt;/em&gt;, avant la moindre tentative d&amp;rsquo;exécution. Si l&amp;rsquo;utilisateur a dupliqué un &lt;code&gt;test_id&lt;/code&gt; par erreur, il l&amp;rsquo;apprend immédiatement.&lt;/p&gt;</description>
    </item>
    <item>
      <title>08.04 — Stratégie de test</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/08-ai-example/04-test-strategy/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/08-ai-example/04-test-strategy/</guid>
      <description>&lt;h1 id=&#34;0804stratégie-de-test&#34;&gt;08.04 — Stratégie de test&lt;a class=&#34;anchor&#34; href=&#34;#0804strat%c3%a9gie-de-test&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Six &lt;strong&gt;types de tests&lt;/strong&gt;, six rôles distincts. Documentés dans &lt;code&gt;CURA_TEST_STRATEGY.md&lt;/code&gt; §3.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;types-de-test&#34;&gt;Types de test&lt;a class=&#34;anchor&#34; href=&#34;#types-de-test&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Statut attendu&lt;/th&gt;&#xA;          &lt;th&gt;Source d&amp;rsquo;autorité&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Happy path&lt;/strong&gt; (cas passant)&lt;/td&gt;&#xA;          &lt;td&gt;PASS&lt;/td&gt;&#xA;          &lt;td&gt;Parcours nominaux décrits d&amp;rsquo;après les SFD&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Unhappy path&lt;/strong&gt; (cas non passant)&lt;/td&gt;&#xA;          &lt;td&gt;PASS (le test passe quand l&amp;rsquo;application refuse de &amp;ldquo;laisser passer&amp;rdquo;)&lt;/td&gt;&#xA;          &lt;td&gt;Validations existantes correctes&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Edge case / boundary&lt;/strong&gt; (tests aux limites)&lt;/td&gt;&#xA;          &lt;td&gt;PASS ou FAIL&lt;/td&gt;&#xA;          &lt;td&gt;Selon le test, souvent accompagné d&amp;rsquo;une note ajoutée sur les SFD&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Business logic vulnerability / gap test&lt;/strong&gt; (tests fonctionnels pour essayer de faire &amp;ldquo;tomber&amp;rdquo; le produit et d&amp;rsquo;identifier des gaps)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;FAIL intentionnel&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Comportements qu&amp;rsquo;un système devrait empêcher mais que CURA n&amp;rsquo;empêche pas&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Exploratory / observed-behaviour&lt;/strong&gt; (exploratoire)&lt;/td&gt;&#xA;          &lt;td&gt;PASS&lt;/td&gt;&#xA;          &lt;td&gt;Documente le comportement actuel quand la spec ne le couvre pas&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Permanent security regression fixture&lt;/strong&gt; (non-régression)&lt;/td&gt;&#xA;          &lt;td&gt;PASS (= la sécurité tient)&lt;/td&gt;&#xA;          &lt;td&gt;Vérifie qu&amp;rsquo;un correctif de sécurité tient (correctifs qui resteront définitivement dans le patrimoine de test)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;happy-path&#34;&gt;Happy path&lt;a class=&#34;anchor&#34; href=&#34;#happy-path&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Exercises the nominal flow with valid inputs and an authenticated user. Verifies that the system produces the correct output (confirmation, history entry, etc.).&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.05 — Saturation des workers</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/05-saturation/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/05-saturation/</guid>
      <description>&lt;h1 id=&#34;020505saturation-des-workers&#34;&gt;02.05.05 — Saturation des workers&lt;a class=&#34;anchor&#34; href=&#34;#020505saturation-des-workers&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Mécanisme propre à Ocarina : si une suite a moins de tests que de workers disponibles, &lt;strong&gt;on clone aléatoirement&lt;/strong&gt; des tests jusqu&amp;rsquo;à atteindre le nombre de workers. Les copies sont renommées &lt;code&gt;[COPY 1] &amp;lt;name&amp;gt;&lt;/code&gt;, &lt;code&gt;[COPY 2] &amp;lt;name&amp;gt;&lt;/code&gt;, etc.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;pourquoi&#34;&gt;Pourquoi&lt;a class=&#34;anchor&#34; href=&#34;#pourquoi&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Le Holy Book formalise la motivation dans le chapitre « &lt;em&gt;Premiers obstacles du monde réel&lt;/em&gt; » :&lt;/p&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Son option &lt;code&gt;saturate_workers&lt;/code&gt; permet de forcer du clonage aléatoire de tests à l&amp;rsquo;intérieur d&amp;rsquo;une suite.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapitre 02.05 — Orchestration</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/</guid>
      <description>&lt;h1 id=&#34;chapitre-0205orchestration&#34;&gt;Chapitre 02.05 — Orchestration&lt;a class=&#34;anchor&#34; href=&#34;#chapitre-0205orchestration&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Chaîne &lt;code&gt;Test → TestSuite → TestCampaign → TestCycle&lt;/code&gt; : comment chaque niveau s&amp;rsquo;articule, qui gère la parallélisation, qui gère les rejeux, qui décide du &lt;em&gt;skip&lt;/em&gt;, et où vivent les invariants pré-exécution.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;Fichier&lt;/th&gt;&#xA;          &lt;th&gt;Sujet&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/01-test/&#34;&gt;&lt;code&gt;01-test.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;La classe &lt;code&gt;Test[Driver]&lt;/code&gt; — &lt;code&gt;spawn&lt;/code&gt;, fragments pre/post, skip.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/02-test-executor/&#34;&gt;&lt;code&gt;02-test-executor.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestExecutor&lt;/code&gt; — exécution d&amp;rsquo;&lt;strong&gt;une&lt;/strong&gt; tentative, ordre : setup → watchers.start → chain → watchers.stop → teardown.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/03-test-flow-retries/&#34;&gt;&lt;code&gt;03-test-flow-retries.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestFlow&lt;/code&gt; — boucle de rejeu (1+max_retries), backoff linéaire, gestion des setup-failures.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/04-test-suite/&#34;&gt;&lt;code&gt;04-test-suite.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite&lt;/code&gt; — parallélisation avec &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;, filtrage IDs, garde-fous.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/05-saturation/&#34;&gt;&lt;code&gt;05-saturation.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Saturation des workers : clonage aléatoire &lt;code&gt;[COPY N]&lt;/code&gt;. Pourquoi et comment.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/06-test-campaign/&#34;&gt;&lt;code&gt;06-test-campaign.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCampaign&lt;/code&gt; — séquence de suites, &lt;code&gt;campaign_has_failed&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/07-test-cycle-modes/&#34;&gt;&lt;code&gt;07-test-cycle-modes.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCycle&lt;/code&gt; — smoke + main, modes &lt;code&gt;fail-fast&lt;/code&gt; vs &lt;code&gt;wait-for-all&lt;/code&gt;, &lt;code&gt;has_test_cycle_failed&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;08&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/08-filter-tests-by-ids/&#34;&gt;&lt;code&gt;08-filter-tests-by-ids.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;filter_tests_by_ids&lt;/code&gt; — &lt;code&gt;--only&lt;/code&gt;/&lt;code&gt;--exclude&lt;/code&gt;, mutex, ignore les unknown IDs.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;schéma&#34;&gt;Schéma&lt;a class=&#34;anchor&#34; href=&#34;#sch%c3%a9ma&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;            ┌────────────────────────────────────────────────┐&#xA;            │                  TestCycle                     │&#xA;            │                                                │&#xA;            │  ┌──────────────────────────────────────────┐  │&#xA;            │  │ smoke_tests_campaigns                    │  │ ◄── 1er, gate&#xA;            │  │  └─ TestCampaign                         │  │     mode : fail-fast |&#xA;            │  │      └─ TestSuite                        │  │            wait-for-all&#xA;            │  │          └─ Test                         │  │&#xA;            │  └──────────────────────────────────────────┘  │&#xA;            │                                                │&#xA;            │  ┌──────────────────────────────────────────┐  │&#xA;            │  │ campaigns (main)                         │  │ ◄── skippées si&#xA;            │  │  └─ TestCampaign                         │  │     un smoke fail&#xA;            │  │      └─ TestSuite                        │  │&#xA;            │  │          └─ Test                         │  │&#xA;            │  └──────────────────────────────────────────┘  │&#xA;            └────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;qui-fait-quoi&#34;&gt;Qui fait quoi ?&lt;a class=&#34;anchor&#34; href=&#34;#qui-fait-quoi&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Niveau&lt;/th&gt;&#xA;          &lt;th&gt;Responsabilité unique&lt;/th&gt;&#xA;          &lt;th&gt;Concurrence&lt;/th&gt;&#xA;          &lt;th&gt;Hors périmètre&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Test&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Métadonnées + &lt;code&gt;spawn(driver, logger)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;aucune&lt;/td&gt;&#xA;          &lt;td&gt;exécution&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestExecutor&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Une &lt;strong&gt;tentative&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;aucune&lt;/td&gt;&#xA;          &lt;td&gt;rejeu, acquisition de driver&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestFlow&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Boucle de &lt;strong&gt;rejeu&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;aucune&lt;/td&gt;&#xA;          &lt;td&gt;parallélisation, agrégation&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Parallélisation + saturation + filtrage IDs&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;N threads&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;séquence inter-suites&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCampaign&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Séquence de suites&lt;/td&gt;&#xA;          &lt;td&gt;suite-level&lt;/td&gt;&#xA;          &lt;td&gt;smoke vs main&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCycle&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Smoke + main + mode&lt;/td&gt;&#xA;          &lt;td&gt;campaign-level&lt;/td&gt;&#xA;          &lt;td&gt;bootstrap / plugins post-exec&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Cette séparation stricte est &lt;strong&gt;délibérée&lt;/strong&gt; : &lt;code&gt;TestExecutor&lt;/code&gt; ne sait rien des retries, &lt;code&gt;TestFlow&lt;/code&gt; ne sait rien de la concurrence, &lt;code&gt;TestSuite&lt;/code&gt; ne sait rien de l&amp;rsquo;agrégation campaign-level. Chaque classe a un seul axe de responsabilité.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.05.06 — TestCampaign[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/06-test-campaign/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/06-test-campaign/</guid>
      <description>&lt;h1 id=&#34;020506testcampaigndriver&#34;&gt;02.05.06 — &lt;code&gt;TestCampaign[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020506testcampaigndriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test_campaign.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test_campaign.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Responsabilité unique&lt;/strong&gt; : exécuter une &lt;strong&gt;séquence de suites&lt;/strong&gt; dans l&amp;rsquo;ordre, partager une config de workers, et déclarer &lt;code&gt;campaign_has_failed&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;TestCampaign&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        suites: Sequence[TestSuite[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        validate_test_suites_names(suites&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;suites, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;suites&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; suites&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results: TestCampaignResults &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_max_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; max_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_saturate_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; saturate_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; suite &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_campaign_name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;run_all&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, skip_all: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;, saturate_workers: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TestCampaignResults:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;clear()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        resolved_saturate_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_saturate_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_saturate_workers &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt; saturate_workers&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; skip_all:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; suite &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results[suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    name: (&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;, test_id)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; name, test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_names_and_ids&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; suite &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_suites:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results[suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; suite&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;run(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                max_workers&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_max_workers,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                saturate_workers&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;resolved_saturate_workers,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_results&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;campaign_has_failed&lt;/span&gt;(results: TestCampaignResults) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;any&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        is_test_result_fail(outcome)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; campaign_results &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; results&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;values()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; outcome, _, _ &lt;span style=&#34;color:#ff79c6&#34;&gt;in&lt;/span&gt; campaign_results&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;values()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;anatomie&#34;&gt;Anatomie&lt;a class=&#34;anchor&#34; href=&#34;#anatomie&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;garde-fou-au-constructeur&#34;&gt;Garde-fou au constructeur&lt;a class=&#34;anchor&#34; href=&#34;#garde-fou-au-constructeur&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;validate_test_suites_names(suites&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;suites, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;suites&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;→ Les noms de suites de la campagne doivent être uniques — de façon insensible à la casse depuis &lt;code&gt;1.1.10&lt;/code&gt; (NFC + casefold). Levé &lt;em&gt;dès la construction&lt;/em&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>06.07 — Flux de coordination OTP &#43; l&#39;anti-précision volontaire</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/07-otp-coordination-flow/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/07-otp-coordination-flow/</guid>
      <description>&lt;h1 id=&#34;0607flux-de-coordination-otp--limprécision-volontaire&#34;&gt;06.07 — Flux de coordination OTP + l&amp;rsquo;imprécision volontaire&lt;a class=&#34;anchor&#34; href=&#34;#0607flux-de-coordination-otp--limpr%c3%a9cision-volontaire&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;La raison d&amp;rsquo;être du backend : permettre à N workers parallèles d&amp;rsquo;Ocarina de &lt;strong&gt;récupérer le bon OTP&lt;/strong&gt; pour leur &lt;em&gt;user&lt;/em&gt;, même quand plusieurs OTP sont générés.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;flux&#34;&gt;Flux&lt;a class=&#34;anchor&#34; href=&#34;#flux&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │               Worker (parmi --workers 3 d&amp;#39;Ocarina)                │&#xA;   └───────────────────────────────────────────────────────────────────┘&#xA;                                     │&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium ouvre la page de connexion au Dashboard                  │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Acquisition du lock distribué Redis (OTP_SEND_LOCK_KEY)           │&#xA;   │   → seul ce worker peut cliquer &amp;#34;Send OTP&amp;#34; pendant ACQ            │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ min_utc_date = datetime.now(UTC)                                  │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Cache L1 : enregistre min_utc_date + username dans le cache       │&#xA;   │   (clés réservées par reserve_free_cache_key)                     │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium tape username + password + cocher OTP + click &amp;#34;REQ OTP&amp;#34;  │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ IGORISTAN UI : fetch /api/otp?_user=&amp;lt;username&amp;gt;                    │&#xA;   │   (x-api-key tapé par Selenium dans l&amp;#39;UI)                         │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ TESTS-WORKERS /api/otp :                                          │&#xA;   │   generate(secret) → otpCode                                      │&#xA;   │   createdAt = floor(now/1000)*1000  ← amputation ms               │&#xA;   │   event = { _user, otpCode, createdAt, expiresAt, ... }           │&#xA;   │   redis.set(&amp;#34;otp:&amp;lt;now&amp;gt;:&amp;lt;uuid&amp;gt;&amp;#34;, JSON, EX 360)                     │&#xA;   │   return event                                                    │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ IGORISTAN UI : affiche écran OTP                                  │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Release du lock Redis OTP_SEND_LOCK_KEY                           │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium : retrieve_dashboard_otp_code(min_utc_date, _user)       │&#xA;   │   ↓                                                               │&#xA;   │   GET /api/otp-history  (x-api-key = IGOR_API_KEY)                │&#xA;   │   ↓                                                               │&#xA;   │   TESTS-WORKERS : SCAN otp:* + MGET → all events                  │&#xA;   │   ↓                                                               │&#xA;   │   filtre côté client par _user                                    │&#xA;   │   filtre createdAt &amp;gt;= min_utc_date - 1s                           │&#xA;   │   tri ASC sur createdAt                                           │&#xA;   │   return first.otpCode                                            │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium tape l&amp;#39;OTP dans l&amp;#39;UI Igoristan                           │&#xA;   │   → AUTHENTICATED_WITH_MFA                                        │&#xA;   └───────────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;races-conditions&#34;&gt;Races conditions&lt;a class=&#34;anchor&#34; href=&#34;#races-conditions&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Worker A : &lt;code&gt;min_utc_date_A = 13:27:53.123&lt;/code&gt;, génère OTP_A à 13:27:53.250.&lt;/li&gt;&#xA;&lt;li&gt;Worker B : &lt;code&gt;min_utc_date_B = 13:27:53.130&lt;/code&gt;, génère OTP_B à 13:27:53.470.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Sans coordination, A pourrait récupérer OTP&lt;em&gt;B (au lieu de OTP_A) parce que les timestamps sont _très proches&lt;/em&gt; (et tronqués à la seconde près).&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
