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, entrypointweb, middlewarelocal-only@file, réseauweb; le servicedocsn'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.examplelimité à des valeurs facticeschange-me...,.env.examplecomplet par rapport aux variables lues par le code,.envetsecrets/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_URLseule, clientOpenAIjamais construit avec une URL littérale, aucune URL dans le code Python decortex_lab. - LiteLLM n'est pas une dépendance ; versions
1.82.7et1.82.8absentes. - Dépendances Python épinglées exactement, Python 3.12, marqueurs déclarés,
uv.lockpré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 configvalide (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 :
- Tests (agent
test-gate) :pytest -q -m "not integration", puispytest -q -m integrationsidocker compose psmontre les services démarrés. - Conventions :
python3 .claude/hooks/no_em_dash.py --scan .etpython3 .claude/hooks/guard_litellm.py --scan .. - Sécurité (agent
security-reviewer) : revue degit diffetgit diff --staged. - Conformité au plan : fichiers modifiés comparés à la section « Fichiers créés ou modifiés » de
PLAN.md. - 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.