Tests
ZeroClaw utilise une taxonomie de tests à cinq niveaux reposant sur l’organisation du système de fichiers. Chaque niveau a une frontière différente et un coût différent ; choisissez le niveau le plus bas qui prouve ce que vous devez prouver.
Lorsqu’une PR revendique un comportement qu’un utilisateur exécute, clique, envoie, installe ou observe directement, utilisez la preuve de limite utilisateur pour identifier le plus petit test ou la plus petite vérification manuelle qui atteint cette limite.
Les cinq niveaux
| Niveau | Ce que cela teste | Limite | Où il se trouve |
|---|---|---|---|
| Unité | Une seule fonction ou struct | Tout est simulé | Les blocs #[cfg(test)] dans src/** ou tests.rs co-localisés |
| Composant | Un sous-système à l’intérieur de sa propre limite | Sous-système réel, tout le reste est simulé | tests/component/ |
| Intégration | Plusieurs composants internes câblés ensemble | Vrais internes, API externes simulées | tests/integration/ |
| Système | Requête complète → réponse à travers toutes les limites internes | Seules les API externes sont simulées | tests/system/ |
| En direct | Pile complète avec de vrais services externes | Rien de simulé, #[ignore] | tests/live/ |
Plus deux répertoires non liés aux tests :
| Répertoire | Objectif |
|---|---|
tests/manual/ | Scripts de test pilotés manuellement (shell, Python), exécutés directement, et non via cargo |
tests/support/ | Infrastructure de mocks partagée, pas un binaire de test, incluse via mod support; depuis chaque niveau |
Exécution des tests
sh
cargo test # unitaire + composant + intégration + système
cargo test --lib # unité uniquement
cargo test --test component # uniquement le composant
cargo test --test integration # intégration uniquement
cargo test --test system # système uniquement
cargo test --test live -- --ignored # live (nécessite des identifiants API)
cargo test --test integration agent # filtre au sein d'un niveau
cargo nextest run --locked --workspace --exclude zeroclaw-desktop # ce que la CI exécute
./scripts/ci/parallel_runtime_test_gate.sh # tests répétés de l’environnement d’exécution et du canal dans un même processus
./dev/ci.sh all # batterie CI complète (Docker)
./dev/ci.sh firmware-protocol # passerelle hôte autonome du protocole de micrologiciel (Docker)
./dev/ci.sh test-component # commandes CI spécifiques au niveau (Docker)
La commande firmware-protocol vérifie le crate autonome firmware/zeroclaw-fw-protocol, qui se trouve en dehors du workspace Cargo racine. scripts/ci/firmware_protocol_gate.sh est la définition canonique de ses vérifications de formatage, de Clippy strict et de tests verrouillés ; le CI requis et le hook pre-push invoquent le même assistant.
La passerelle d’exécution parallèle répète l’intégralité des binaires de test des bibliothèques zeroclaw-runtime et zeroclaw-channels avec 16 threads de harnais. L’exécution des binaires complets est intentionnelle : elle détecte les interférences entre les tests à mutation d’état et les tours d’agent sans lien apparent que les exécutions de tests filtrés ne peuvent pas exposer. La CI obligatoire exécute cette passerelle dans un job distinct pour les modifications apportées à l’un ou l’autre des crates, aux manifestes de dépendances de l’espace de travail ou aux propres fichiers CI de la passerelle. Les autres PR la sautent ; les poussées vers master et les exécutions de la file d’attente de fusion conservent le filet de sécurité de régression complet. Remplacez le nombre de répétitions avec ZEROCLAW_PARALLEL_TEST_RUNS et les threads de harnais avec ZEROCLAW_PARALLEL_TEST_THREADS.
Choisir un niveau pour un nouveau test
- Tester un sous-système de manière isolée ? →
tests/component/ - Tester plusieurs composants câblés ensemble ? →
tests/integration/ - Tester le flux complet des messages de bout en bout ? →
tests/system/ - Requiert de vraies clés API ? →
tests/live/avec#[ignore]
Après avoir créé le fichier, ajoutez-le au mod.rs du niveau et utilisez l’infrastructure partagée de tests/support/.
Infrastructure partagée
Chaque binaire de test inclut mod support;, rendant les mocks partagés disponibles sous forme de crate::support::*.
| Module | Contenu |
|---|---|
mock_model_provider.rs | MockModelProvider (script FIFO), RecordingModelProvider (capture les requêtes), TraceLlmModelProvider (relecture de fixtures JSON) |
mock_tools.rs | EchoTool, CountingTool, FailingTool, RecordingTool |
mock_channel.rs | TestChannel (capture les envois, enregistre les événements de frappe) |
helpers.rs | make_memory(), make_observer(), build_agent(), text_response(), tool_response(), StaticRecallMemory |
trace.rs | Types LlmTrace, TraceTurn, TraceStep + LlmTrace::from_file() |
assertions.rs | verify_expects() pour l’assertion de trace déclarative |
Utilisation typique :
#![allow(unused)]
fn main() {
use crate::support::{MockModelProvider, EchoTool, CountingTool};
use crate::support::helpers::{build_agent, text_response, tool_response};
}
Fixtures de trace JSON
Les fixtures de trace sont des scripts de réponses LLM préenregistrés, stockés sous forme de fichiers JSON dans tests/fixtures/traces/. Ils remplacent la configuration de mocks en ligne par des scripts de conversation déclaratifs, bien plus faciles à lire et à modifier que des chaînes mockall.
Comment ça marche :
TraceLlmModelProvidercharge une fixture et implémente le traitModelProvider.- Chaque appel à
provider.chat()renvoie l’étape suivante du jeu d’essais dans l’ordre FIFO. - Les outils réels s’exécutent normalement (
EchoTooltraite effectivement ses arguments). - Après tous les tours,
verify_expects()vérifie les assertions déclaratives. - Si l’appel de l’agent au fournisseur dépasse le nombre d’étapes, le test échoue.
Format du jeu de données :
{
"model_name": "test-name",
"tours": [
{
"entrée_utilisateur": "Message utilisateur",
étapes: [
{
"réponse": {
"type": « texte »,
"contenu": "Réponse du LLM",
"jetons_d_entrée": 20,
"output_tokens": 10
}
}
]
}
],
"attend": {
"response_contains": ["texte attendu"],
"outils_utilisés": ["echo"],
"max_tool_calls": 1
}
}
Types de réponse : "text" (texte brut) ou "tool_calls" (requêtes LLM pour exécuter des outils).
Attend les champs : response_contains, response_not_contains, tools_used, tools_not_used, max_tool_calls, all_tools_succeeded, response_matches (regex).
Conventions de test en direct
Les tests en conditions réelles sollicitent de véritables services externes et engendrent des coûts réels ; ils sont marqués #[ignore] par défaut et ne s’exécutent qu’avec une activation explicite.
- Toujours
#[ignore]. Ne laissez jamais un test en direct s’exécuter lors d’uncargo testnormal. - Lisez les identifiants depuis
env::var("ZEROCLAW_TEST_*"). Ne lisez pas la configuration de l’opérateur ; les tests en conditions réelles doivent être hermétiques. - Exécutez avec
cargo test --test live -- --ignored --nocapture.
Les tests de base de données sont des tests d’intégration
Ne mockez pas SQLite pour les tests qui exercent le schéma ou le SQL ; les tests d’intégration doivent utiliser une vraie base de données. La classe de bugs « le mock passe mais la prod échoue » est bien réelle et nous en avons déjà fait les frais.
Tests manuels
tests/manual/ contient des scripts de tests manuels destinés à être exécutés par un humain et qui ne peuvent pas être automatisés via cargo test. Exécutez-les directement. Les tests de fumée manuels spécifiques à une chaîne se trouvent sous tests/manual/<channel>/.