08.01 — Code : 99% Claude, 1% Igor, intelligence : 50-50

08.01 — Code : 99% Claude, 1% Igor, intelligence : 50-50#

README#

A note to the reader#

This project is an experiment in AI-driven test engineering. In the interest of honesty about how it was built:

Claude CodeHuman
Code written99%1%
Intelligence50%50%

Almost every line was machine-written. The judgement behind it — what to test, what to distrust, when to dig and when to stop — was shared.

09.03.01 — Skills Review

09.03.01 — Skills Review#

Lectures statiques, remontent des constats. Une grande famille. Permet à l’IA d’effectuer de la revue sur son projet de manière systématique.

Listing (potentiellement non exhaustif)#

SkillCible
review-spec-gapsQuestions de clarification sur les SFD
review-watcher-misuseVérifie le principe « négatif uniquement » de watcher.report(...)
review-compartmentalisation-leaksDétecte URLs, sélecteurs, nombres magiques aux mauvais endroits
review-dead-codeDétecte connecteurs / POMs / scénarios / suites / fragments / constantes non utilisés
review-reportClassifie chaque FAIL / SKIP d’une exécution
review-type-ignoreAudite les # type: ignore (sont-ils justifiés ?)
review-match-candidatesIdentifie les endroits où un match (Python, pattern matching) pourrait être utilisé plutôt que des if elif elif if if elif
review-unverified-transitionsVérifie qu’à chaque transition de page, il y a un verify
review-submit-dispatchersAudite les méthodes de confirmation de saisie (clic vs touche entrée…)
review-comment-driftDétecte les commentaires qui sont désynchronisés avec le code
review-suite-stabilityÉvalue la stabilité d’une suite (proportion de retries, transient_errors hits)
review-intent-collisionsDétecte les tests qui s’écrasent mutuellement (intentions contradictoires) et demande/propose des clarifications
review-watcher-emissionsAudite les émissions de watchers (volume, déduplication, pertinence)
review-hierarchy-namingAudite le nommage de la hiérarchie (TestCycle / TestCampaign / TestSuite / Test) pour repérer l’antipattern où un enfant reprend le nom du parent

review-dead-code#

input  : base de tests
output : liste des éléments non utilisés (connectors / POMs / scénarios / fragments / constantes)
         + recommandation par élément :
            - supprimer
            - mettre en incubateur (<racine-source>/incubator/, arbre de dépendances préservé)
            - conserver (justifier)

review-report#

input  : une exécution récente (logs + reports)
output : classification de chaque test :
            - PASS                  (rien à faire)
            - SKIP                  (pourquoi ?)
            - intentional gap FAIL  (G-DATA-*, G-SEC-*, ...)
            - cross-browser FAIL    (B-BROWSER-*)
            - transient FAIL        (A-ENV-*)
            - régression            ⚠️ ALERTE

review-watcher-misuse#

input  : tous les watcher.report(...)
output : liste des reports qui semblent positifs (« success », « completed », « ok », ...)
         → recommandation : supprimer ou reformuler en négatif

review-comment-drift#

input  : tous les commentaires du code
output : liste des commentaires qui semblent ne plus correspondre au code adjacent
         (typique : commentaire mentionne foo, code mentionne bar)

Aide à éliminer les commentaires obsolètes.
« Teach the pattern, not the symptom ».

02.05.02 — TestExecutor[Driver]

02.05.02 — TestExecutor[Driver]#

Fichier source : src/ocarina/dsl/testing/internals/test_executor.py

Responsabilité unique : exécuter une seule tentative d’un test, avec un seul driver. Ne connaît ni le rejeu, ni la pool de drivers, ni l’agrégation au niveau suite.

ExecutionOutcome#

@final
@dataclass(frozen=True, slots=True)
class ExecutionOutcome:
    result: TestResult
    skipped: bool
    setup_failed: bool
    should_retry: bool
    steps_count: int
ChampTypeSens
resultTestResult = Result[Any] | NoneLe résultat de la chaîne (Ok, Fail), ou None si skip / setup_failed.
skippedboolTrue si test_runner.skipped (i.e. Test(skipped=True)).
setup_failedboolTrue si la fonction setup() du scénario a levé.
should_retryboolTrue si la règle de rejeu s’applique (transient_error détecté).
steps_countintNombre d’act appelés ; -1 si skip ou setup_failed.
  • slots=True : empêche l’ajout dynamique d’attributs et économise mémoire. C’est un objet qui circule en hot-path, le slots est justifié.
  • frozen=True : immutable, sûr à partager entre threads.
  • @final : pas d’héritage.

Ordre d’exécution d’une tentative#

┌──────────────────────────────────────────────────────────────────────┐
│  test_runner = test.spawn(driver, logger_with_taxonomy)              │
└──────────────────────────────┬───────────────────────────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  test_runner.skipped ?          │── True ─► return Outcome(skipped=True, ...)
              └────────────────┬────────────────┘
                               │ False
                               ▼
              ┌─────────────────────────────────┐
              │  logger.test_name(test.name)    │   (annotation)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  setup() (si non-None)          │── leve ─► teardown() (si non-None)
              └────────────────┬────────────────┘            ↓
                               │                  return Outcome(setup_failed=True, should_retry=True, ...)
                               ▼
              ┌─────────────────────────────────┐
              │  watchers.start(driver, logger, │
              │                 take_screenshot)│   (1 daemon thread par watcher)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  _run_chain(chain_runners, ...) │── retourne (result, should_retry)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  watchers.stop()                │   (toujours)
              └────────────────┬────────────────┘
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  teardown() (si non-None)       │   (TOUJOURS, même si chain a fail)
              └────────────────┬────────────────┘     (les exceptions sont logguées & avalées)
                               │
                               ▼
              ┌─────────────────────────────────┐
              │  steps_count = act_counter.get()│
              │  return Outcome(...)            │
              └─────────────────────────────────┘

execute#

def execute(
    self, test: Test[Driver], *,
    driver: Driver,
    taxonomy: tuple[str, ...],
    logger_with_taxonomy: ILogger,
    logger_without_taxonomy: ILogger,
    attempt: int, max_attempts: int,
) -> ExecutionOutcome:
    test_runner = test.spawn(driver, logger_with_taxonomy)

    if test_runner.skipped:
        return ExecutionOutcome(result=None, skipped=True, setup_failed=False,
                                should_retry=False, steps_count=-1)

    logger_without_taxonomy.test_name(test.name)

    if test_runner.setup is not None:
        try:
            test_runner.setup()
        except Exception as exc:
            msg = f"{test.name} -- Setup failed (attempt {attempt}/{max_attempts}): {exc}"
            logger_with_taxonomy.warning(msg)
            if test_runner.teardown is not None:
                self._run_teardown(test_runner.teardown, test_name=test.name, logger=logger_with_taxonomy)
            return ExecutionOutcome(result=None, skipped=False, setup_failed=True,
                                    should_retry=True, steps_count=-1)

    watchers: Sequence[Watcher[Driver]] = test_runner.watchers or []
    self._start_watchers(watchers, driver=driver, test_name=test.name, taxonomy=taxonomy)

    result, should_retry = self._run_chain(
        test_runner.chain_runners, test_name=test.name, attempt=attempt,
        driver=driver, logger=logger_without_taxonomy,
        logger_with_taxonomy=logger_with_taxonomy, max_attempts=max_attempts,
    )

    self._stop_watchers(watchers)

    if test_runner.teardown is not None:
        self._run_teardown(test_runner.teardown, test_name=test.name, logger=logger_with_taxonomy)

    steps_count = self._act_counter.get()
    return ExecutionOutcome(result=result, skipped=False, setup_failed=False,
                            should_retry=should_retry, steps_count=steps_count)

Deux loggers : pourquoi#

logger_with_taxonomy vs logger_without_taxonomy :

05.02 — Les routes de l'Igoristan

05.02 — Les routes de l’Igoristan#

src/config/routes.ts#

const __ROOT = "/igoristan/";
const DASHBOARD = "dashboard";

const __ROUTES = {
  DONKEY_SAUSAGE_DETECTOR: "donkey-sausage-eater-detector",
  DASHBOARD_NESTED: `${DASHBOARD}/nested`,
  RANDOM_LOADERS: "random-loaders",
  SACRED_UPLOAD: "sacred-upload",
  RANDOM_ERROR: "random-error",
  CHAOTIC_FORM: "chaotic-form",
  CORSICAMON: "corsicamon",
  MADNESS: "madness",
  DASHBOARD,
  HOME: "",
} as const satisfies Routes;

const ROUTES = createRoutes(__ROUTES, __ROOT);
RouteURLRôleChaos volontaire
HOME/igoristan/Page d’accueil avec liens vers les autres pages, autoplay audio — 
RANDOM_LOADERS/igoristan/random-loadersLoaders pseudo-aléatoiresLoaders qui restent affichés un temps variable
SACRED_UPLOAD/igoristan/sacred-uploadFormulaire d’upload react-dropzone — 
DASHBOARD/igoristan/dashboardLogin (mot de passe figatellu), MFA OTP optionneluseAuth peut échouer 10% du temps même avec le bon mdp
DASHBOARD_NESTED/igoristan/dashboard/nestedPage protégée, requiert MFA — 
CORSICAMON/igoristan/corsicamon« Pokédex corse », 3 picks aléatoires via API key1/5 chance de lever Error('lol') artificiellement
RANDOM_ERROR/igoristan/random-errorPage d’erreur facticeTitre matché par ERROR_PAGE_REGEX côté tests
CHAOTIC_FORM/igoristan/chaotic-formFormulaire instableÉléments .catch-me-if-you-can apparaissent aléatoirement
MADNESS/igoristan/madnessHistoires alternées (Cors, ThisIsBastia)Rend l’une OU l’autre au hasard
DONKEY_SAUSAGE_DETECTOR/igoristan/donkey-sausage-eater-detectorDétecteur de sales siciliens de merde30% d’échec aléatoire (BSOD-style)

Page par page#

const links = [
  { href: ROUTES.RANDOM_LOADERS, label: "Random loaders" },
  { href: ROUTES.SACRED_UPLOAD, label: "Sacred upload" },
  { href: ROUTES.DASHBOARD, label: "Dashboard" },
  { href: ROUTES.CORSICAMON, label: "Corsicamon" },
  { href: ROUTES.RANDOM_ERROR, label: "Random Error" },
  { href: ROUTES.CHAOTIC_FORM, label: "Chaotic form" },
  { href: ROUTES.MADNESS, label: "Madness" },
  { href: ROUTES.DONKEY_SAUSAGE_DETECTOR, label: "Donkey Sausage Detector" },
];

home#

8 liens vers les autres pages + autoplay audio Angelus de Jérusalem (amen 🙏).

03.03 — Évaluation paresseuse

03.03 — Évaluation paresseuse#

Dans Ocarina, rien n’est exécuté tant qu’on ne l’a pas explicitement déclenché. C’est ce qui rend les scénarios composables comme des valeurs.

Zzz#

EndroitFormeDéclencheur
ChainRunner[T]Thunk[ActionChain[T]]runner.run()
validate(...)ValidationStartBlockValidationAssertBlock.execute()
match_page(...)retourne un ChainRunner[Any].run() (via la chaîne englobante)
Watcher.callbackCallable[[Watcher], None]_loop quand start() est appelé
logger.set_prefix(thunk)Thunk[str]recalculé à chaque appel de log
Scenario.setupteardownEffectappelé par TestExecutor
bootstrap(post_exec=...)Callable[[TestCycleResults], None]appelé après run_plugins
CliBuilder(effects_factory=lambda ns: (...))Effectsappelés après le parse argparse
test_scenario: TestScenario[Driver]Callable[[Driver, ILogger], Scenario[Driver]]appelé par Test.spawn
dispatch[mode]() dans TestCycle.run_alldict de Thunk[bool]appelé en lookup

ChainRunner#

runner = drive_page(act1, act2, act3)        # ⚠️  rien exécuté
# … plus tard …
chain = runner.run()                         # ▶︎  exécution
CapacitéSans paresseAvec paresse
Stocker un scénario dans une variableimpossible (déjà exécuté)trivial
Multiplier [runner] * 5exécute 1 fois, on a 5 références au résultatexécute 5 fois
Passer un runner à un autre runner (composition)impossibletrivial
Réordonner les act dans un test refactordifficiletrivial

validate(...).execute()#

v = validate(value, name="x").assert_that(is_positive).assert_that(is_not_zero)
# … on peut composer …
combined = chain_validations(v, other_validation)
# … rien d'exécuté jusqu'ici …
combined.execute().raise_if_invalid()        # ▶︎  exécution + agrégation

C’est ce qui permet à _ValidationChain de collecter toutes les erreurs avant d’en lever une seule (AggregateInvariantViolationError).

07.05 — Scénarios randomness (4 levels)

07.05 — Scénarios randomness (4 levels)#

Quatre niveaux de chaos progressifs. On commence simple, on monte en complexité, on stresse de plus en plus Ocarina.

Campagne#

# src/tests/campaigns/randomness.py
def create_igoristan_randomness_campaign(*, drivers_pool) -> TestCampaign:
    return TestCampaign(
        name="Randomness",
        suites=[create_randomness_test_suite(drivers_pool=drivers_pool)],
    )

Suite#

# src/tests/suites/randomness.py
def create_randomness_test_suite(*, drivers_pool) -> TestSuite:
    return TestSuite(
        name="Randomness",
        tests=[
            test_random_error_page,                     # level 1
            test_random_loaders_page,                   # level 1
            test_dsed,                                  # level 2
            test_madness,                               # level 2
            test_chaotic_form,                          # level 3
            test_walkthrough,                           # level 4
        ],
        drivers_pool=drivers_pool,
    )

Levels#

LevelMécanique exercéeDifficulté
1DSL de base (drive_page, act)facile
2match_pagewhenmoyen
3HumanizedDriver + Watcherdifficile
4Walkthrough multi-pages chaotiquesdifficile

Level 1 — Random Error Page#

# tests/scenarios/randomness/level_1/random_error_page.py
def scenario_random_error_page(driver, logger):
    page = RandomErrorPage(driver=driver)
    return [
        drive_page(
            act(page, open_random_error_page)...,
            act(page, verify_random_error_page)...,
        ),
    ]


test_random_error_page = create_selenium_test(
    name="Random Error Page - smoke",
    test_scenario=lambda driver, logger: Scenario(test_chain=scenario_random_error_page(driver, logger)),
)

Test volontairement flaky, exerce le hook on_failure.

02.06 — Scenario[Driver]

02.06 — Scenario[Driver]#

Fichier source : src/ocarina/custom_types/scenario.py

Dataclass#

@final
@dataclass(frozen=True)
class Scenario[Driver]:
    test_chain: TestChain
    setup: TestSetup = field(default=None)
    teardown: TestTeardown = field(default=None)
    watchers: TestWatchers[Driver] | None = field(default=None)
# src/ocarina/custom_types/test_components.py
type TestChain = Sequence[ChainRunner[Any]]
type TestSetup = Effect | None
type TestTeardown = Effect | None
type TestWatchers[Driver] = Sequence[Watcher[Driver]] | None

Cycle de vie (per attempt)#

1. setup()           — optionnel, Effect libre (DB, API, …)
       → lève     :  skip de test_chain, jump à teardown,
                     return Outcome(setup_failed=True, should_retry=True)
       → ok       :  continue à test_chain

2. test_chain        — la chaîne réelle (Sequence[ChainRunner])
       (les watchers tournent pendant ce temps)

3. teardown()        — optionnel, toujours exécuté
       → lève     :  log warning + ignore (n'affecte pas le verdict)

Si TOUTES les tentatives lèvent au setup :
    → test marqué SKIPPED (pas FAILED)
    → log warning « setup keeps failing »

setup et teardown#

setup et teardown sont driver-free et injectionless by design.

02.07 — Watcher[Driver]

02.07 — Watcher[Driver]#

Fichier source : src/ocarina/dsl/testing/watcher.py

Observateur parallélisé qui tourne en daemon thread aux côtés de la test_chain. Conçu pour détecter les frictions imprévisibles et sans impact direct sur le scénario qui se produisent pendant que le scénario tourne : toasts d’erreur aléatoires, validations parasites, popups inattendus.

Le problème#

Citation du Holy Book (handling-flakiness) :

Plus surprenant encore : des applications affichant des toasts d’erreur sans raison apparente, ou des formulaires signalant des erreurs de validation sur des saisies pourtant correctes, sans pour autant bloquer le parcours.

07.09 — catch_me_if_you_can

07.09 — catch_me_if_you_can#

Détecte les éléments parasites qui pop sur la page pendant que le test tourne, et les trace.

Callback#

# src/lib/ext/selenium/watchers/catch_me_if_you_can_watcher.py
def catch_me_if_you_can_cb(watcher: SeleniumWatcher) -> None:
    elements = watcher.driver.execute_script(
        "return Array.from(document.querySelectorAll('.catch-me-if-you-can'));"
    )

    if not elements:
        return

    raw = watcher.driver.execute_script(
        """
        return arguments[0].map(el => ({
            tag:       el.tagName.toLowerCase(),
            text:      el.innerText.trim(),
            id:        el.id,
            cls:       el.className,
            name:      el.getAttribute('name') || '',
            testid:    el.getAttribute('data-testid') || '',
        }));
        """,
        elements,
    )

    for attrs in raw:
        fingerprint = ":".join(
            filter(
                None,
                [
                    attrs["tag"],
                    attrs["text"],
                    attrs["id"],
                    attrs["cls"],
                    attrs["name"],
                    attrs["testid"],
                ],
            )
        )

        if fingerprint in watcher.cache:
            continue

        watcher.cache.add(fingerprint)
        watcher.report(
            f"catch-me-if-you-can element detected: <{attrs['tag']}> {attrs['text']!r}",
            label="CATCH_ME_IF_YOU_CAN",
        )

Mécaniques#

1. Javascript pour bypass implicit wait#

elements = watcher.driver.execute_script(
    "return Array.from(document.querySelectorAll('.catch-me-if-you-can'));"
)

Pas de find_elements(By.CSS_SELECTOR, ...).