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.

08.02 — CURA Healthcare (SUT)

08.02 — CURA Healthcare (SUT)#

La première victime d’Ocarina#

Périmètre#

PageFonction
/Marketing homepage
/profile.php#loginLogin
/appointment.phpForm de booking (facility, programs, hospital_readmission, visit_date, comment)
/confirmation.phpConfirmation du booking
/history.phpHistorique des bookings du user
/profile.php#profilePage Profil (placeholder vide — gap §G-SPEC-2)

Pourquoi cibler CURA#

  1. Open source. On peut lire le PHP pour comprendre pourquoi tel comportement.
  2. Beaucoup de gaps. Riche terrain de findings.
  3. Public, démo. Pas d’autorisation à demander. Stable (maintenu par Katalon).

Plus une raison tacite : Katalon est un éditeur de framework de test concurrent (Katalon Studio).
C’est joliment poétique de tester leur app démo avec Ocarina.

08.03 — Documentation

08.03 — Documentation#

Le projet IA est le plus documenté de l’écosystème d’Ocarina.

Listing#

DocumentStatutLectorat
CLAUDE.mdContrat de travail Claude ↔ projetClaude + tout contributeur
CURA_FRD.mdSpec reconstruiteMainteneurs, stakeholders
CURA_TEST_STRATEGY.mdStratégie de testMainteneurs
IDENTIFIED_GAPS.mdInventaire technique des gapsMainteneurs

CLAUDE.md#

  1. Key documents : pointe vers README.md, CURA_FRD.md, CURA_TEST_STRATEGY.md, IDENTIFIED_GAPS.md, CLAUDE.local.md.
  2. Project : 2 phrases sur le projet (CURA, Python 3.14+, demo creds).
  3. Project philosophy : « No tricks or hacks. Teach the pattern, not the symptom. »
  4. CLAUDE.local.md template : ce que doit contenir le fichier gitignored.
  5. Layout : arbre src/ complet, audité (commande shell qui vérifie que CLAUDE.md et l’arbre réel sont synchronisés).
  6. Ocarina hierarchy : rappel Test → Suite → Campaign → Cycle.
  7. Test strategy : structure du cycle, smoke gate, taxonomie.
  8. Running tests : commandes CLI.
  9. Reports and screenshots : règle « un screenshot par drive_page ».
  10. Scenario fragments : quand extraire un fragment.
  11. Data-driven tests : quand utiliser le pattern, conventions de naming.
  12. Mixins partagés : SeleniumBackAndForwardNavigationMixin.
  13. Conventions : 12 règles concrètes (TYPE_CHECKING guard, log factories, etc.).
  14. Hard-won rules : règles tirées de l’expérience, avec exemples du passé.

Hard-won rules#

« Verify SUT behaviour — don’t theorise »#

CURA is open source. Before building on a server-side claim, read the PHP:

08.04 — Stratégie de test

08.04 — Stratégie de test#

Six types de tests, six rôles distincts. Documentés dans CURA_TEST_STRATEGY.md §3.

Types de test#

TypeStatut attenduSource d’autorité
Happy path (cas passant)PASSParcours nominaux décrits d’après les SFD
Unhappy path (cas non passant)PASS (le test passe quand l’application refuse de “laisser passer”)Validations existantes correctes
Edge case / boundary (tests aux limites)PASS ou FAILSelon le test, souvent accompagné d’une note ajoutée sur les SFD
Business logic vulnerability / gap test (tests fonctionnels pour essayer de faire “tomber” le produit et d’identifier des gaps)FAIL intentionnelComportements qu’un système devrait empêcher mais que CURA n’empêche pas
Exploratory / observed-behaviour (exploratoire)PASSDocumente le comportement actuel quand la spec ne le couvre pas
Permanent security regression fixture (non-régression)PASS (= la sécurité tient)Vérifie qu’un correctif de sécurité tient (correctifs qui resteront définitivement dans le patrimoine de test)

Happy path#

Exercises the nominal flow with valid inputs and an authenticated user. Verifies that the system produces the correct output (confirmation, history entry, etc.).

08.05 — Gaps sécurité (G-SEC-1 à G-SEC-3)

08.05 — Gaps sécurité (G-SEC-1 à G-SEC-3)#

Trois gaps de sécurité documentés. Pas exploités activement (cf. règle « security testing is functional and static, never active »), mais observés via tests fonctionnels.

G-SEC-1 — Pas de jeton CSRF sur les formulaires côté front-end#

Symptôme#

Le <form> rendu côté Heroku ne contient pas de CSRF token.

Source#

views/page_appointment.php du repo GitHub appelle $antiCSRF->insertHiddenToken() avant le bouton submit. Mais sur la version déployée, ce token n’apparaît pas dans le HTML.

08.06 — Gaps data (G-DATA-1, G-DATA-2)

08.06 — Gaps data (G-DATA-1, G-DATA-2)#

Deux gaps majeurs sur l’intégrité des données.

G-DATA-1 — Pas de validation serveur de visit_date#

Source#

// appointment.php
array_diff_key($_POST, array(
    "facility" => "",
    "hospital_readmission" => "",
    "programs" => "",
    "visit_date" => "",
    "comment" => ""
))

Cette ligne fait juste une vérification de présence des clés.
Elle ne valide jamais la valeur de visit_date.

Symptôme#

  1. Empty visit_date : si on supprime l’attribut HTML5 required via JS et qu’on submit avec une date vide → prise de RDV acceptée.
  2. Past visit_date : si on envoie une réservation antidatée → prise de RDV acceptée.
  3. La réservation est bien ajoutée à l’historique telle quelle.

Tests associés#

TestManière
Appointment - Server accepts empty date when client bypass appliedRetire l’attribut required, envoie avec date de réservation vide → s’attend à être rejeté par le système (FAIL)
Appointment - Past date booking acceptedEnvoie une demande de RDV pour hier → s’attend à être rejeté par le système (FAIL)

Les deux tests sont intentionnellement en échec.
Ils documentent le gap.

08.07 — Gaps spec (G-SPEC-1 à G-SPEC-3)

08.07 — Gaps spec (G-SPEC-1 à G-SPEC-3)#

Trois gaps sur le comportement métier vs la spec attendue d’un système de santé.

G-SPEC-1 — History trié par ordre de soumission, pas par date#

Source#

// views/page_history.php
foreach ($_SESSION['history'] as $appointment) {
    // ... render ...
}

Pas de usort. Pas de tri. Aucune étape de tri n’existe nulle part.

appointment.php push avec array_push($_SESSION['history'], $_POST) — append en queue (c’est un pushback).

08.08 — BFCache Chrome (B-BROWSER-1) + artefacts d'environnement

08.08 — BFCache Chrome (B-BROWSER-1) + artefacts d’environnement#

Chrome restore une page no-store après logout, depuis le BFCache, alors qu’il s’agit d’une page protégée par authentification.

B-BROWSER-1 — Chrome nous emmerde avec son BFCache#

Symptôme#

Après déconnexion, cliquer sur le bouton “précédent” afin d’être redirigé vers une page qui était protégée par authentification (par exemple : history.php) :

  • Chrome : la page reste affichée avec son contenu tel qu’il est affiché à un utilisateur encore authentifié. L’URL reste history.php, le serveur n’est pas recontacté pour valider ou invalider l’accès.
  • Firefox : une nouvelle requête est envoyée au serveur, ce dernier voit que la session est morte, il redirige.

Gap très ennuyeux#

history.php envoie correctement :

08.09 — CI : ai_proof_ci.yml + ai_proof_e2e.yml

08.09 — CI : ai_proof_ci.yml + ai_proof_e2e.yml#

Deux workflows. PR rapide (lint+typecheck). e2e manuel avec matrice Firefox/Chrome + warm-up Heroku + filtrage stacktrace ChromeDriver.

ai_proof_ci.yml#

name: ai_proof CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  lint-and-typecheck:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.14"
          allow-prereleases: true
      - uses: actions/cache@v4
        with:
          path: .venv
          key: venv-${{ runner.os }}-py${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
      - name: Install dependencies
        if: steps.cache-venv.outputs.cache-hit != 'true'
        run: |
          python -m venv .venv
          .venv/bin/pip install . ruff mypy mypy-extensions typing-extensions pre-commit

      - name: Ruff format check
        run: .venv/bin/ruff format src/ --check

      - name: Ruff lint
        run: .venv/bin/ruff check src/

      - name: Mypy
        run: .venv/bin/mypy src/
StepCmd
Ruff format checkruff format src/ --check
Ruff lintruff check src/
Mypymypy src/

ai_proof_e2e.yml#

name: ai_proof e2e

on:
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  e2e:
    name: e2e (${{ matrix.browser }})
    runs-on: ubuntu-latest
    env:
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
      WAIT_TIMEOUT: 15
    strategy:
      fail-fast: false
      matrix:
        include:
          - browser: firefox
            driver: geckodriver
          - browser: chrome
            driver: chromedriver

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.14"
          allow-prereleases: true
      - uses: actions/cache@v4
        with:
          path: .venv
          key: venv-${{ runner.os }}-py${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
      - name: Install dependencies
        if: steps.cache-venv.outputs.cache-hit != 'true'
        run: |
          python -m venv .venv
          .venv/bin/pip install . ruff mypy mypy-extensions typing-extensions pre-commit

      # --- Firefox ---

      - name: Install geckodriver
        if: matrix.browser == 'firefox'
        run: |
          GECKO_VERSION=0.36.0
          wget -q https://github.com/mozilla/geckodriver/releases/download/v$GECKO_VERSION/geckodriver-v$GECKO_VERSION-linux64.tar.gz
          tar -xzf geckodriver-v$GECKO_VERSION-linux64.tar.gz
          chmod +x geckodriver

      - name: Setup Firefox
        if: matrix.browser == 'firefox'
        uses: browser-actions/setup-firefox@fcf821c621167805dd63a29662bd7cb5676c81a8

      # --- Chrome ---

      - name: Install chromedriver
        if: matrix.browser == 'chrome'
        run: |
          CHROME_VER=$(google-chrome-stable --version | grep -oP '\d+\.\d+\.\d+\.\d+')
          wget -q -O chromedriver.zip \
            "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VER}/linux64/chromedriver-linux64.zip"
          unzip -q chromedriver.zip
          mv chromedriver-linux64/chromedriver ./chromedriver
          chmod +x ./chromedriver

      # --- Warm-up ---

      - name: Warm up Heroku dyno
        run: |
          # CURA runs on a Heroku dyno that sleeps after inactivity.
          # Tests hitting a cold dyno produce timeouts (false fails) and slow
          # confirmations (false passes on gap tests). Wake it explicitly
          # before any browser opens.
          curl -sf --retry 6 --retry-delay 5 --retry-all-errors \
            --max-time 30 https://katalon-demo-cura.herokuapp.com/ > /dev/null
          echo "Dyno is warm"

      # --- Run ---

      - name: Run e2e cycle
        run: |
          # ChromeDriver release builds ship without debug symbols. On errors it
          # dumps raw C++ stack frames ("#N 0x<addr> <unknown>") into the WebDriver
          # response, which Selenium surfaces as the exception message — polluting
          # both the live log and the pretty-print summary with unreadable hex noise.
          # No Python-side fix exists. Strip the frames with sed before they land.
          # pipefail preserves Python's exit code through the pipe.
          set -o pipefail
          .venv/bin/python -u src/main.py \
            --driver-path ./${{ matrix.driver }} \
            --browser ${{ matrix.browser }} \
            --workers 3 \
            --wait-timeout ${{ env.WAIT_TIMEOUT }} \
            --logger terminal+file \
            2>&1 | sed -u \
              -e '/Stacktrace:/{N;/\n#[0-9][0-9]* 0x[0-9a-f][0-9a-f]* <unknown>/{s/Stacktrace:\n[^\n]*/HIDDEN/;b};P;D}' \
              -e '/^#[0-9][0-9]* 0x[0-9a-f][0-9a-f]* <unknown>/d'

      - name: Enumerate traces
        if: always()
        run: |
          sync
          find $GITHUB_WORKSPACE/.screenshots -type f 2>/dev/null || true
          find $GITHUB_WORKSPACE/.ocarina_logs -type f 2>/dev/null || true
          find $GITHUB_WORKSPACE/.reports -type f 2>/dev/null || true

      - name: Upload screenshots
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: screenshots-${{ matrix.browser }}
          path: .screenshots/
          retention-days: 7

      # … upload logs, reports similar ...

Analyse#

fail-fast: false#

strategy:
  fail-fast: false
  matrix:
    include:
      - browser: firefox
      - browser: chrome

fail-fast: false = si Firefox fail, Chrome continue. Indispensable pour ce projet : la valeur du run est de voir les deux browsers, même si l’un fail.