Aller au contenu

Tests et gate

État

Au 2026-10-26 : 919 tests unitaires et 83 tests d'intégration passent (dont 3 sautés sans le groupe forecast) (aucun xfail).

Configuration

pyproject.toml, section [tool.pytest.ini_options] : pytest==9.1.1 (groupe dev, installé par défaut), --strict-markers -ra, testpaths = ["tests"], pythonpath = ["."]. Deux marqueurs déclarés :

Marqueur Rôle
integration Le test a besoin des services du labo ; il est sauté quand ils ne répondent pas
services(*names) Services précis dont un test d'intégration a besoin, parmi polaris, trino, minio, litellm, marquez, docs ; sans ce marqueur, les cinq premiers sont requis (docs n'est requis que par les tests qui le nomment) ; services() vide : aucun service

tests/conftest.py saute chaque test integration dont un service requis ne répond pas à une connexion TCP sur l'URL lue dans l'environnement (POLARIS_URI, TRINO_URL, MINIO_ENDPOINT, LITELLM_BASE_URL, MARQUEZ_URL, DOCS_URL).

Fixtures communes :

Fixture Rôle
catalog Catalogue Iceberg réel sur SQLite et système de fichiers local (aucun service)
duck Connexion DuckDB hors ligne (ni extension, ni catalogue, ni cortex_lab)
tech_cfg, reader_cfg, tech_catalog, reader_catalog Configuration et catalogue Polaris réels pour lab_tech et lab_reader
trino trino(user) renvoie une fonction qui exécute du SQL sous ce nom d'utilisateur
scratch Crée des tables it_<id> uniques et les supprime en fin de test

Commandes

# Unitaires et de contrat : sans réseau ni service
uv run --env-file .env pytest -q -m "not integration"

# Intégration : pile démarrée, groupe forecast pour l'inférence réelle de t0-gamma
uv run --group forecast --env-file .env pytest -q -m integration

# Un sous-ensemble
uv run --env-file .env pytest -q -m integration tests/socle

Sans --env-file .env, les tests unitaires passent (aucun ne lit de secret) ; les tests d'intégration sont sautés faute d'URL. Sans --group forecast, les tests d'inférence réelle sont sautés (pytest.importorskip("t0")).

Organisation et contenu

Conventions du dépôt (tests/test_repo_conventions.py, sans réseau)

  • Ports publiés uniquement sous la forme 127.0.0.1:<port>:<port>, sans collision entre eux ni avec les ports pris sur le NUC (8080, 3000, 5432, 9000, 4000, 8000, 8001, 6333, 9900).
  • Aucune brique du NUC embarquée (PostgreSQL, MinIO, LiteLLM, Redis, Qdrant, Ollama, Keycloak, Authentik) ; services rattachés au réseau externe data ; briques adressées par leur nom réseau (pg-central:5432, minio-central:9000).
  • Routes Traefik : exactement quatre routeurs (Trino, Polaris, Marquez, openflake-docs), hôtes en .nuc, entrypoint web, middleware local-only@file, réseau web ; le service docs n'a ni port publié ni clé image, un seul réseau (web), read_only, cap_drop: [ALL].
  • Images épinglées à un tag exact (jamais latest), y compris dans les Dockerfiles.
  • Secrets : aucune valeur littérale dans le compose, .env.example limité à des valeurs factices change-me..., .env.example complet par rapport aux variables lues par le code, .env et secrets/ ignorés par git, aucun motif de clé réelle dans le dépôt, fichiers de configuration qui lisent leurs secrets dans l'environnement.
  • Sortie LLM : aucun hôte de fournisseur ni variable de fournisseur dans le code, LITELLM_BASE_URL seule, client OpenAI jamais construit avec une URL littérale, aucune URL dans le code Python de cortex_lab.
  • LiteLLM n'est pas une dépendance ; versions 1.82.7 et 1.82.8 absentes.
  • Dépendances Python épinglées exactement, Python 3.12, marqueurs déclarés, uv.lock présent et complet.
  • Aucun tiret cadratin dans le dépôt.
  • Aucun fichier d'option non retenue (soda/, cube/, infra/litellm, infra/postgres).
  • docker compose --env-file .env.example config valide (sauté sans CLI Docker).

Socle (tests/socle/)

Fichier Contenu
test_naming.py Nommage à trois niveaux, identifiants en minuscules, cohérence entre compose, Trino, Polaris et dbt
test_types.py JSON en chaîne, timestamp et timestamptz, decimal(38, s)
test_maintenance.py Time Travel, expire_snapshots, branches et tags sur de vraies tables Iceberg (catalogue SQLite)
test_polaris_init.py Job d'initialisation contre un faux de l'API de gestion : ordre, rejeu, privilèges, secrets, propriété de catalogue polaris.config.drop-with-purge.enabled (création, mise à jour d'un catalogue existant en conservant ses propriétés, rejeu sans écriture)
test_trino_rules.py rules.json évalué comme Trino (première règle gagnante)
test_duckdb_connect.py Accès DuckDB via un faux serveur REST local
test_stack_integration.py Intégration : préfixes MinIO, persistance de Polaris après redémarrage, même table lue par Trino et DuckDB en trois niveaux, FOR VERSION AS OF et FOR TIMESTAMP AS OF, B5 (inclusivité de FOR TIMESTAMP AS OF, snapshot parent pour BEFORE(STATEMENT), horodatage relatif pour OFFSET), B7 (fichiers laissés dans MinIO par une table remplacée puis supprimée), expire_snapshots, branche et tag, refus Polaris pour lab_reader, refus par règle Trino, absence de credential vending

Collecte et retraitement (tests/ingest/)

Fichier Contenu
test_landing.py Fichiers immuables sous landing/<source>/<date>/
test_evidence_columns.py Présence et contenu des colonnes de preuve et de _row_hash
test_schema_registry.py Empreintes, détection de changement, historique, évolution Iceberg
test_classify.py Règles de classification, AI_CLASSIFY avec faux client LiteLLM
test_raw_loader.py Chargement, NULL conservés, doublons par hash, mémoire de 14 jours, fichier modifié, colonnes ajoutées ou retirées, conflit de type, rapprochement des totaux, fichier non classé
test_mapping.py Seed versionné, cibles canoniques, suggestions en attente jamais appliquées, report au seed après acceptation, réponses invalides et pannes en exception
test_exceptions.py Table d'exceptions, file de revue, décisions, échecs dbt en exceptions
test_confidence_class.py Domaine fermé et NULL avec unknown sur chaque table standardized
test_dbt_project.py Projet dbt réel sur un fichier DuckDB local sans Polaris (dbt_helpers.py, classe Workspace) : build et tests dbt, dernier export gagnant, SCD2, lecture as-of, référentiel, KPI, MetricFlow, compilation pour duckdb et trino, profil duckdb (alias analytics, :memory:, transactions désactivées), noms des relations et schema_prefix, macros drop_relation, intermediate_relation et trino__list_relations_without_caching, .env.example sans chemin de fichier, profil snowflake déclaré, plugin cortex_lab, dispatch de get
test_lineage_and_snapshots.py Événements START, COMPLETE, FAIL ; panne de Marquez sans effet sur le chargement ; snapshots shared_calendar
test_pipeline_integration.py Intégration : dépôt et chargement raw avec preuves, registre, deux dbt build du projet réel sur Polaris (namespaces préfixés par schema_prefix, tables remplacées, SCD2 fermé, lues par Trino et DuckDB, format Iceberg 2, vraies couches intactes), MetricFlow sur occupancy_rate, droits de lab_reader, lineage de la CLI du chargeur vérifié dans l'API Marquez, shared_* en lecture seule
test_dbt_polaris_integration.py Intégration, test de faisabilité de dbt-duckdb sur Polaris : projet fixtures/dbt_polaris_probe exécuté deux fois (table remplacée, incremental delete+insert et merge, snapshot SCD2, seed), lu par Trino et DuckDB, information_schema, format Iceberg 2
test_dbt_snowflake_offline.py B1 et B2 : dbt parse et dbt compile --no-introspect sur la cible snowflake avec un compte factice .invalid et des proxys fermés ; sauté sans dbt-snowflake
test_dbt_trino_integration.py Intégration, B3 : deux dbt build --target trino passent, le second remplace les tables (nouvel UUID) et ferme les versions SCD2, mf query répond ; suppose la purge de suppression activée sur le catalogue Polaris
test_dbt_lineage_integration.py Intégration, B4 : dbt-ol build lu dans Marquez (jobs, datasets, espace de noms duckdb://:memory: contre iceberg://analytics)
test_raw_loader_cli.py CLI du chargeur : émetteur absent sans MARQUEZ_URL (ligne INFO), START et COMPLETE émis, dataset de sortie <raw_schema>.<table>, panne d'émission sans effet, aucun identifiant journalisé

cortex_lab (tests/cortex_lab/)

Fichier Contenu
fake_litellm.py Faux client : réponses valides, invalides, erreurs, sans réseau
test_ai_classify_contract.py, test_ai_sentiment_contract.py, test_ai_complete_contract.py Contrats : cas valides, chaque nombre d'arguments (macros SQL et façades), sorties invalides en erreur, NULL par ligne sans annuler le lot arrow, enveloppe return_error_details, arguments optionnels NULL, limites, forme exacte du retour
test_prompt_contract.py, test_ai_filter_contract.py, test_ai_extract_contract.py, test_ai_translate_contract.py, test_ai_summarize_contract.py (AI_SUMMARIZE texte : seuil de 110 caractères, résumé plus long que l'entrée, macro et arguments nommés, façade summarize, absence de façade Snowpark), test_ai_agg_contract.py (AI_AGG et AI_SUMMARIZE_AGG), test_ai_embed_contract.py, test_ai_similarity_contract.py, test_ai_redact_contract.py, test_ai_count_tokens_contract.py Contrats des fonctions de l'avenant du 2026-10-23, même méthode : cas valides, chaque nombre d'arguments (cœur, macros SQL, façades), sorties invalides en erreur, return_error_details, NULL par ligne (ou erreur levée pour AI_EXTRACT, AI_AGG, AI_SUMMARIZE_AGG, AI_REDACT), limites, routage vers l'alias attendu
test_local_window.py B11 hors ligne : fenêtre de contexte du modèle local, texte trop long refusé sans appel pour chaque fonction, sortie d'AI_EXTRACT plafonnée par la place restante
test_get_casts.py B6 : casts de GET vers varchar, integer, bigint, double, boolean, date, decimal(38, s) sur des valeurs JSON chaîne, nombre, booléen, NULL et clé absente ; rendu de la macro dbt get pour duckdb, trino et snowflake
test_duckdb_register.py UDF, macros, surcharges par nombre d'arguments (dont prompt de 2 à 11 paramètres), get (NULL pour clé absente), pas de masquage de fonctions intégrées (B13 : syntaxe nommée => et :=), agrégat par list(col) vers une UDF à argument liste (B14), types de retour
test_litellm_client.py Client contre un transport HTTP simulé : URL lue dans l'environnement, sortie structurée, route des embeddings, think: false, aucune fuite
test_facades.py cortex_lab.snowpark, cortex_lab.cortex, bascule CORTEX_BACKEND, plugin dbt
test_litellm_integration.py Intégration : un appel réel par fonction et par forme, contrat vérifié, jamais le contenu ; les macros de bout en bout ; B11 (fenêtres du chat et des embeddings mesurées, coupure silencieuse constatée) ; B12 (embeddings, 1 024 flottants) ; une clé du labo refusée sur un alias cloud et limitée aux alias cortex-*

Prévision (tests/forecast/)

Fichier Contenu
test_inputs.py Formes [B, T], [B, V, T], [B, F, T + H], NaN conservés, masques MISSING et PAD, refus de group_ids avec future_covariates, covariables lues as-of
test_forecast_table.py Schéma de forecast.forecast, confidence_class = 'estimated', traçabilité, épinglages, points d'entrée
test_backtest_metrics.py Métriques sur des cas calculés à la main
test_forecast_integration.py Intégration (4 tests) : inférence réelle t0-gamma sur CPU (quantiles finis et ordonnés), modèle chargé égal à l'épinglage, lecture des couches standardized et des snapshots SCD2 dans Polaris (même entrées qu'en local), écriture dans Polaris puis lecture par Trino et DuckDB

Site de documentation (tests/website/)

Fichier Contenu
test_site_conventions.py Sans réseau : COPY du Dockerfile limités à pyproject.toml, uv.lock, website/, docs/ ; images et téléchargement Mermaid épinglés (--checksum=sha256:) ; étape finale non root ; .dockerignore ; mkdocs.yml strict, polices système, aucune ressource externe, ni analytics ni dépôt ; navigation égale aux quatorze fichiers de docs/ ; nginx.conf ; groupe docs épinglé et dans default-groups
test_site_build.py Construction stricte par website/build.py hors ligne (script Mermaid factice, sauté sans le groupe docs) : pages publiées, trois diagrammes Mermaid, ancres accentuées, liens de l'accueil, aucune ressource externe, aucun fichier source ni secret ; un lien absent, une ancre absente et un fichier hors navigation font échouer la construction
test_site_integration.py Intégration, requiert le seul service docs (DOCS_URL) : 200 sur /, /documentation/guide-agent/ et le script Mermaid local ; 404 sur /.env, /secrets/..., /pyproject.toml, /uv.lock et les sources .md ; en-têtes ; conteneur non root ; arborescence servie sans source ni donnée

Effets de bord des tests d'intégration

Ils écrivent dans la pile réelle : tables scratch it_*, schéma raw scratch it_raw_<id>, namespaces préfixés it_<id>t_ (cible trino), it_<id>o_ (dbt-ol) et it_orph_<id> (fichiers orphelins), jobs et datasets de test dans Marquez (supprimés en fin de test, les espaces de noms vides restent listés), dates fictives (landing entre 2100 et 2108, snapshots de calendrier entre 2099 et 2107, as_of_date de prévision à partir de 2099), lignes dans les tables intake réelles. Le nettoyage n'est pas complet : des tables it_* et des données à dates fictives restent. test_polaris_keeps_catalog_namespaces_and_tables_after_a_restart redémarre le conteneur polaris (docker compose restart polaris).

Gate /gate

Définie par .claude/skills/gate/SKILL.md, lancée à la fin de chaque sprint, dans cet ordre :

  1. Tests (agent test-gate) : pytest -q -m "not integration", puis pytest -q -m integration si docker compose ps montre les services démarrés.
  2. Conventions : python3 .claude/hooks/no_em_dash.py --scan . et python3 .claude/hooks/guard_litellm.py --scan ..
  3. Sécurité (agent security-reviewer) : revue de git diff et git diff --staged.
  4. Conformité au plan : fichiers modifiés comparés à la section « Fichiers créés ou modifiés » de PLAN.md.
  5. Rapport : tableau contrôle | statut | détail, puis message de commit Conventional Commits proposé si tout est vert, sinon liste des points bloquants.

La gate ne commite ni ne pousse : Stéphane valide. Aucun sprint n'est soumis à validation sans gate au vert (règle 5 de CLAUDE.md). Voir methode-claude-code.md.