Tests d'acceptation
En bref
- Un test d'acceptation vérifie un critère d'un besoin de Besoins par profil, avec des valeurs littérales et par les points d'entrée publics.
- Chaque test porte l'identifiant
AT-<besoin>-<n>, le même dans toutes les langues. - La page confronte chaque test à la suite existante et le marque couvert, partiel ou absent.
- Les tests écrits pour combler les manques sont dans
tests/acceptance/.
Les décisions qui restent à prendre sont dans Points à régler. Les termes sont définis dans le glossaire.
Comment lire les tableaux
- « Tests existants » cite des nœuds pytest relatifs à la racine du dépôt
piighost. Un nœud préfixépiighost-api:vient du dépôtpiighost-api. - Les tests LlamaIndex sont ignorés sans l'extra
llama-index. Ils passent avecuv run --with llama-index-core pytest tests/integrations/llama_index. - Les valeurs d'exemple des scénarios sont en français. Les tests emploient leurs équivalents anglais, par exemple « Hi Emma » pour « Bonjour Claire ».
- Statut : couvert (le comportement est vérifié, parfois avec d'autres valeurs que celles de l'inventaire, et ces valeurs sont alors signalées), partiel (ce qui manque est indiqué), absent.
- Les tests de la bibliothèque utilisent
ExactMatchDetectorou un catalogue simulé. La qualité de détection d'un vrai modèle ou d'un vrai groupe du catalogue reste hors de ces tests.
DPO (responsable conformité)
| Id | Besoin | Scénario | Résultat attendu | Tests existants | Statut |
|---|---|---|---|---|---|
| AT-DPO-1-1 | DPO-1 | Étant donné un agent protégé (middleware LangChain, capacité Pydantic AI ou proxy), quand l'utilisateur écrit « Écrivez à Jean Dupont, jean.dupont@exemple.fr », alors le modèle reçoit le texte dé-identifié | Le modèle reçoit « Écrivez à <<PERSON:1>>, <<EMAIL:1>> », sans « Jean Dupont » ni « jean.dupont@exemple.fr » dans aucun message | tests/integrations/langchain/test_middleware_e2e.py::test_second_model_call_never_sees_clear_tool_argstests/integrations/test_pydantic_ai_hooks.py::TestAroundTheModel::test_model_sees_placeholders_and_reply_is_restoredpiighost-api:tests/routes/test_openai_chat.py::test_upstream_sees_tokens_reply_is_restored | couvert (valeurs « Patrick Dupont » et « patrick@acme.com ») |
| AT-DPO-1-2 | DPO-1 | Étant donné une config qui tire un groupe de secrets du catalogue, quand le message contient une clé d'API, alors la clé part en jeton | Le texte dé-identifié contient un jeton du label du groupe (par exemple « <<API_KEY:1>> ») et plus la clé | tests/acceptance/test_dpo.py::TestSecretsLeaveAsTokens::test_an_api_key_leaves_as_a_token | couvert (motifs OPENAI_API_KEY et AWS_ACCESS_KEY copiés de catalog:piighost/logs:b635d867, catalogue simulé, jetons « <<OPENAI_API_KEY:1>> » et « <<AWS_ACCESS_KEY:1>> ») |
| AT-DPO-2-1 | DPO-2 | Étant donné une config dont catalogs contient catalog:piighost/fr-default:<commit>, quand le texte contient l'IBAN « FR76 3000 6000 0112 3456 7890 189 » et un NIR, alors les deux sont masqués | Deux jetons, ni l'IBAN ni le NIR en clair | tests/config/test_detectors.py::TestRegexCatalogs::test_a_catalog_is_pulled_at_buildtests/test_catalog.py::TestFromCatalog::test_builds_a_detector_that_detectstests/pipeline/test_pipeline.py::TestUnicodeSpaces::test_a_regex_value_typed_with_no_break_spaces_is_hidden | partiel (le chargement d'un groupe est vérifié sur un catalogue simulé, l'IBAN n'est masqué que par un motif écrit dans la configuration, le NIR jamais) |
| AT-DPO-2-2 | DPO-2 | Étant donné la même config sans le groupe qui porte l'IBAN, quand le même texte passe, alors l'IBAN reste en clair | « FR76 3000 6000 0112 3456 7890 189 » figure tel quel dans la sortie | tests/acceptance/test_dpo.py::TestChosenGroups::test_a_removed_group_leaves_its_values_clear | couvert (catalogue simulé) |
| AT-DPO-2-3 | DPO-2 | Étant donné une config avec un motif propre à l'entreprise, écrit dans la configuration, quand le texte contient une valeur qui lui correspond, alors elle est masquée | « écrivez à a@b.co vite » devient « écrivez à <<REDACT>> vite » | tests/config/test_settings.py::TestLoadPipeline::test_builds_a_working_pipelinetests/config/test_detectors.py::TestRegexCatalogs::test_catalogs_merge_in_order_then_inline_patterns | couvert |
| AT-DPO-3-1 | DPO-3 | Étant donné une liste à masquer (deny_list) contenant « Acme » et un détecteur qui ne voit rien, quand « Acme recrute » passe, alors « Acme » est masqué | « <<ORG:1>> recrute » | tests/pipeline/test_override_integration.py::TestBasePipelineOverride::test_deny_list_forces_a_value_the_detector_missedtests/config/test_settings.py::TestOverrideEffect::test_deny_list_forces_a_detection | couvert |
| AT-DPO-3-2 | DPO-3 | Étant donné une liste à laisser en clair (allow_list) contenant « Paris » et un détecteur qui le voit, quand « Visitez Paris » passe, alors « Paris » reste en clair | « Visitez Paris » | tests/pipeline/test_override_integration.py::TestBasePipelineOverride::test_allow_list_keeps_a_false_positive_in_clear | couvert |
| AT-DPO-4-1 | DPO-4 | Étant donné un garde-fou qui détecte les adresses e-mail et un détecteur principal qui les rate, quand « Claire a écrit à leak@x.com » passe, alors rien n'est renvoyé | PIIRemainingError est levée | tests/pipeline/test_override_integration.py::TestBasePipelineOverride::test_a_real_leak_still_trips_the_guardtests/config/test_settings.py::TestGuardEffect::test_detector_guard_raises_on_residualtests/pipeline/test_pipeline.py::TestGuard::test_a_flagged_guard_raises | couvert |
| AT-DPO-5-1 | DPO-5 | Étant donné une mémoire Redis avec hacheur SHA-256 et chiffrement AES-GCM, quand le message « Je suis Claire » est mémorisé, alors la base ne contient ni le message ni la valeur | Aucune clé ne contient « Je suis Claire », aucune valeur stockée ne contient « Claire » | tests/conversation_memory/test_redis.py::TestAtRestProtection::test_message_text_is_not_stored_in_any_keytests/conversation_memory/test_redis.py::TestAtRestProtection::test_detections_are_encrypted_at_rest | couvert (sur RedisConversationMemory et fakeredis, pas via un fichier de config) |
| AT-DPO-5-2 | DPO-5 | Étant donné un fichier de config Redis avec hacheur et chiffreur, quand PIIGHOST_HASH_PEPPER ou PIIGHOST_CIPHER_KEY manque, alors load_thread_pipeline refuse de construire | ConfigError, aucun pipeline rendu | tests/acceptance/test_ops.py::TestSecretsFromTheEnvironment::test_a_full_redis_config_without_its_secrets_does_not_buildtests/config/test_crypto_models.py::TestHasherConfig::test_missing_pepper_is_rejectedtests/config/test_crypto_models.py::TestCipherConfig::test_missing_key_is_rejectedtests/config/test_redis_memory.py::TestRedisMemoryConfig::test_half_configured_crypto_raises_config_error | couvert |
| AT-DPO-6-1 | DPO-6 | Étant donné une conversation « t1 » qui a dé-identifié « Bonjour Claire », quand forget_thread("t1") est appelé puis deanonymize("<<PERSON:1>>", "t1"), alors la valeur n'est plus rendue | La restauration rend « <<PERSON:1>> », jamais « Claire » | tests/acceptance/test_dpo.py::TestErasure::test_a_forgotten_thread_restores_nothingtests/pipeline/test_thread.py::TestForget::test_forget_thread_clears_the_memorytests/pipeline/test_thread.py::TestForget::test_forget_thread_drops_the_memoized_tokenstests/conversation_memory/test_redis.py::TestForget::test_forget_purges_and_reports | couvert |
| AT-DPO-6-2 | DPO-6 | Étant donné le serveur piighost-api, quand le client envoie DELETE /v1/threads/t1, alors la conversation est effacée | 200 et {"messages": 2, "detections": 3} | piighost-api:tests/test_app.py::test_forget_thread_returns_countstests/integrations/client/test_client.py::TestForgetThread::test_deletes_and_returns_a_forgotten | couvert (pipeline simulé côté serveur) |
| AT-DPO-7-1 | DPO-7 | Étant donné un pipeline avec observation_redactor, quand « Bonjour Claire ! » passe, alors les traces exportées ne contiennent que des jetons | Aucun attribut de span ne contient « Claire », le span piighost.detect porte un jeton | tests/observation/test_pipeline_spans.py::TestRedaction::test_redactor_replaces_values_with_its_tokenstests/observation/test_pipeline_spans.py::TestRedaction::test_redactor_removes_clear_values_from_payloads | couvert (jeton « <<REDACT>> » et non « <<PERSON:1>> », le test utilise RedactPlaceholderFactory) |
| AT-DPO-8-1 | DPO-8 | Étant donné la documentation, quand on ouvre docs/fr/dpia.md et docs/en/dpia.md, alors la page liste risques, mesures et réglages | Les deux pages existent et portent une section qui associe chaque risque à un réglage | aucun | absent (critère documentaire, make lint ne contrôle que le style des pages) |
| AT-DPO-9-1 | DPO-9 | Étant donné un LLMDetector dont le modèle rend une sortie sans champ entities, quand un message passe, alors le pipeline lève une erreur | Une erreur levée, aucun texte rendu | tests/components/detector/test_llm.py::TestDetect::test_an_unreadable_output_refuses_the_message | couvert |
| AT-DPO-9-2 | DPO-9 | Étant donné le même détecteur avec l'échec ouvert demandé explicitement, quand le message passe, alors il part sans détection et un avertissement est journalisé | Le texte inchangé, un avertissement | tests/components/detector/test_llm.py::TestDetect::test_fail_open_lets_an_unreadable_output_through | couvert |
| AT-DPO-10-1 | DPO-10 | Étant donné une correction humaine exportée sous la forme « jetons », quand le jeu de données est lu, alors il ne contient aucune valeur réelle | Le jeu porte « <<PERSON:1>> », jamais « Claire » | aucun | absent (fonction à concevoir) |
Développeur (qui intègre piighost)
| Id | Besoin | Scénario | Résultat attendu | Tests existants | Statut |
|---|---|---|---|---|---|
| AT-DEV-1-1 | DEV-1 | Étant donné un agent create_agent existant, quand on ajoute middleware=[PIIAnonymizationMiddleware(pipeline)], alors l'agent tourne sans autre changement et le modèle ne voit que des jetons | L'appel ainvoke aboutit, aucun appel au modèle ne contient la valeur en clair | tests/integrations/langchain/test_middleware_e2e.py::test_second_model_call_never_sees_clear_tool_args | couvert |
| AT-DEV-1-2 | DEV-1 | Étant donné un client OpenAI et un piighost-api doté d'un amont par défaut, quand seul base_url pointe vers /openai/v1, alors l'amont reçoit des jetons et la réponse revient restaurée | L'amont reçoit « <<PERSON:1>> », le client lit « Bonjour Patrick » | piighost-api:tests/routes/test_openai_chat.py::test_upstream_sees_tokens_reply_is_restoredpiighost-api:tests/routes/test_upstream.py::test_upstream_default_used_when_header_absentpiighost-api:tests/routes/test_anthropic_messages.py::test_default_upstream_used_without_header | partiel (le test de bout en bout passe l'en-tête x-piighost-upstream, l'amont par défaut est testé à part, aucun test ne passe par le SDK openai) |
| AT-DEV-2-1 | DEV-2 | Étant donné une conversation où « Bonjour Claire » a été dé-identifié, quand le modèle répond « Bonjour <<PERSON:1>> », alors l'application reçoit la réponse restaurée | « Bonjour Claire » | tests/integrations/langchain/test_middleware.py::TestWhenInstalled::test_after_model_deanonymizes_for_displaytests/integrations/test_pydantic_ai_hooks.py::TestAroundTheModel::test_model_sees_placeholders_and_reply_is_restoredtests/pipeline/test_thread.py::TestDeanonymize::test_restores_tokens_the_pipeline_never_anonymized | couvert |
| AT-DEV-3-1 | DEV-3 | Étant donné une conversation « t1 », quand « Bonjour Claire » puis « Au revoir Claire » passent, alors la valeur garde son jeton | « Bonjour <<PERSON:1>> » puis « Au revoir <<PERSON:1>> » | tests/pipeline/test_thread.py::TestThreadConsistency::test_a_value_keeps_its_token_across_messagestests/config/test_thread_pipeline.py::TestLoadThreadPipeline::test_memory_shares_placeholder_across_messages | couvert (deux messages, pas trois) |
| AT-DEV-3-2 | DEV-3 | Étant donné « Marc » dans la conversation « a » et « Claire » dans la conversation « b », quand on restaure « <<PERSON:1>> » dans la conversation « b », alors on obtient la valeur de la conversation « b » | Chaque conversation numérote à partir de « <<PERSON:1>> », la restauration dans « b » rend « Claire », jamais « Marc » | tests/acceptance/test_dev.py::TestThreadIsolation::test_a_token_restores_in_its_own_thread_onlytests/pipeline/test_thread.py::TestThreadConsistency::test_threads_are_isolatedtests/config/test_thread_pipeline.py::TestLoadThreadPipeline::test_threads_are_isolated | couvert (un troisième fil vide rend le jeton tel quel) |
| AT-DEV-4-1 | DEV-4 | Étant donné la stratégie FULL (par défaut), quand le modèle appelle send_mail(to="<<EMAIL:1>>"), alors l'outil reçoit la vraie valeur et son résultat repart en jetons | L'outil reçoit « patrick@acme.com », l'appel suivant au modèle ne contient ni « patrick@acme.com » ni « Patrick Dupont » | tests/integrations/langchain/test_middleware_e2e.py::test_second_model_call_never_sees_clear_tool_argstests/integrations/langchain/test_middleware.py::TestToolCalls::test_strategy_routes_each_directiontests/integrations/test_pydantic_ai_hooks.py::TestTools::test_tool_gets_the_value_and_its_result_is_reanonymized | couvert |
| AT-DEV-5-1 | DEV-5 | Étant donné une config avec patterns = { ORDER = "CMD-\d{4}-\d{4}" }, quand « CMD-2024-0042 » passe, alors le numéro est masqué | « <<ORDER:1>> » à la place du numéro | tests/config/test_settings.py::TestLoadPipeline::test_builds_a_working_pipeline | couvert (motif EMAIL écrit dans la configuration au lieu du numéro de commande) |
| AT-DEV-5-2 | DEV-5 | Étant donné une classe qui n'hérite de rien mais expose async detect(text) -> list[Detection], quand on la passe à AnonymizationPipeline, alors ses détections deviennent des jetons | La sortie porte les jetons des détections de la classe | tests/pipeline/test_pipeline.py::TestMergeOverlap::test_the_merge_resolver_leaves_no_fragment_of_a_longer_spantests/pipeline/test_pipeline.py::TestUnicodeSpaces::test_a_value_keeps_one_token_whatever_its_spaces | couvert (de façon incidente, par le _FixedDetector de ces tests) |
| AT-DEV-6-1 | DEV-6 | Étant donné une config valide, quand on lance piighost validate config.toml, alors la commande réussit | Code de sortie 0, sortie qui commence par « OK: » | tests/cli/test_cli.py::TestValidate::test_valid_config_exits_zero | couvert |
| AT-DEV-6-2 | DEV-6 | Étant donné une config où patterns est écrit pattern, quand on lance piighost validate, alors la commande échoue en nommant l'erreur | Code de sortie 1, stderr qui nomme la clé fautive | tests/cli/test_cli.py::TestValidate::test_invalid_schema_exits_onetests/config/test_catalog_config.py::TestLoadConfigFromTheCatalog::test_an_invalid_configuration_names_the_referencetests/config/test_models.py::TestDetectorConfig::test_unknown_key_is_rejected | partiel (la CLI est testée sur un type manquant et ne vérifie que « invalid configuration », la clé mal orthographiée n'est testée que dans le chargeur) |
| AT-DEV-7-1 | DEV-7 | Étant donné ExactMatchDetector({"Claire": "PERSON", "Marc": "PERSON"}), quand « Claire a vu Marc » passe, alors le texte est dé-identifié sans modèle | « <<PERSON:1>> a vu <<PERSON:2>> » | tests/pipeline/test_pipeline.py::TestAnonymize::test_replaces_detected_piitests/components/detector/test_exact.py::TestDetect::test_finds_multiple_values | couvert (l'absence de réseau n'est pas imposée par un blocage de socket) |
| AT-DEV-8-1 | DEV-8 | Étant donné la stratégie RAISE, quand le modèle répond avec « <<PERSON:9>> » jamais émis, alors la réponse est refusée | InventedPlaceholderError est levée | tests/integrations/langchain/test_middleware.py::TestInventedPlaceholders::test_raise_refuses_an_invented_tokentests/integrations/test_pydantic_ai_hooks.py::TestInventedStrategy::test_an_invented_token_in_the_reply_is_refused | couvert |
| AT-DEV-8-2 | DEV-8 | Étant donné la stratégie DROP, quand le modèle répond « Bonjour <<PERSON:1>>, cc <<PERSON:9>> », alors le jeton inventé disparaît | « Bonjour Claire, cc » | tests/integrations/langchain/test_middleware.py::TestInventedPlaceholders::test_kept_or_dropped | couvert |
| AT-DEV-9-1 | DEV-9 | Étant donné une conversation où « <<PERSON:1>> » vaut « Patrick », quand deanonymize_stream reçoit « Bonjour <<PER » puis « SON:1>> », alors la valeur apparaît une seule fois | « Bonjour Patrick » | tests/integrations/langchain/test_middleware_stream.py::test_middleware_deanonymize_stream_restores_split_tokentests/integrations/test_deidentify_stream.py::test_token_split_across_chunks_is_restored | couvert |
| AT-DEV-10-1 | DEV-10 | Étant donné le middleware LangChain, les hooks Claude Code ou le serveur d'API, quand un appel ne nomme aucune conversation, alors il est refusé | MissingThreadIdError, ou 400 sur le serveur | tests/integrations/langchain/test_middleware.py::TestThreadId::test_a_missing_id_raisestests/integrations/test_claude_code_hooks.py::test_an_event_without_a_session_id_is_refusedpiighost-api:tests/test_app.py::test_a_thread_route_without_a_thread_id_is_refused | couvert |
| AT-DEV-10-2 | DEV-10 | Étant donné un appel qui nomme "default", quand il passe par le middleware, alors il est accepté | La conversation utilisée est "default" | tests/integrations/langchain/test_middleware.py::TestThreadId::test_the_shared_thread_is_named_explicitly | couvert |
| AT-DEV-11-1 | DEV-11 | Étant donné la stratégie PRESERVE (par défaut), quand l'assistant écrit « C'était Napoléon. » puis l'utilisateur demande « Qu'a fait Napoléon ? », alors la valeur reste en clair | « Napoléon » en clair dans les deux messages | tests/pipeline/test_thread.py::TestProvenance::test_assistant_introduced_value_stays_cleartests/pipeline/test_thread.py::TestProvenance::test_user_reference_after_assistant_stays_cleartests/integrations/langchain/test_middleware.py::TestAssistantProvenance::test_preserve_keeps_assistant_value_clear | couvert |
| AT-DEV-11-2 | DEV-11 | Étant donné la stratégie ANONYMIZE ou IGNORE, quand l'assistant introduit la même valeur, alors elle passe en jeton, ou le message de l'assistant n'est pas analysé | « <<PERSON:1>> », ou aucun appel au détecteur pour l'assistant | tests/integrations/langchain/test_middleware.py::TestAssistantProvenance::test_anonymize_treats_assistant_value_as_piitests/integrations/langchain/test_middleware.py::TestAssistantProvenance::test_ignore_does_not_analyze_assistant_messages | couvert |
Exploitant (qui déploie et fait tourner)
| Id | Besoin | Scénario | Résultat attendu | Tests existants | Statut |
|---|---|---|---|---|---|
| AT-OPS-1-1 | OPS-1 | Étant donné piighost-api serve --config hub:piighost/fr-default:e6990159, quand un client envoie POST /v1/anonymize, alors le serveur répond | 201 et un anonymized_text qui porte des jetons | piighost-api:tests/test_cli_serve.py::test_serve_takes_a_hub_referencepiighost-api:tests/test_app.py::test_anonymizepiighost-api:tests/test_routes_labels.py::test_the_labels_of_a_hub_catalog_come_from_the_hub | partiel (serve est testé avec uvicorn simulé, la route avec un pipeline simulé, aucun test ne relie les deux) |
| AT-OPS-2-1 | OPS-2 | Étant donné deux ThreadAnonymizationPipeline sur la même base Redis, quand l'instance A traite « Bonjour Claire » et l'instance B « Au revoir Claire » dans la conversation « t1 », alors elles s'accordent | Les deux rendent « <<PERSON:1>> », B restaure « <<PERSON:1>> » en « Claire » | tests/acceptance/test_ops.py::TestSharedMemory::test_two_instances_agree_on_a_thread | couvert (fakeredis partagé, une nouvelle valeur vue par B se restaure aussi depuis A) |
| AT-OPS-3-1 | OPS-3 | Étant donné une config Redis avec hacheur et chiffreur, quand le serveur démarre sans PIIGHOST_HASH_PEPPER ni PIIGHOST_CIPHER_KEY, alors il refuse de démarrer | ConfigError au démarrage, aucun démarrage en clair | tests/acceptance/test_ops.py::TestSecretsFromTheEnvironment::test_a_full_redis_config_without_its_secrets_does_not_buildtests/config/test_crypto_models.py::TestHasherConfig::test_missing_pepper_is_rejectedtests/config/test_crypto_models.py::TestCipherConfig::test_missing_key_is_rejectedtests/config/test_redis_memory.py::TestRedisMemoryConfig::test_half_configured_crypto_raises_config_error | partiel (vérifié via load_thread_pipeline sur un fichier complet, pas au démarrage du serveur) |
| AT-OPS-4-1 | OPS-4 | Étant donné aucun API_KEY_<nom> et pas de PIIGHOST_ALLOW_ANONYMOUS, quand le serveur démarre, alors il refuse | RuntimeError qui nomme PIIGHOST_ALLOW_ANONYMOUS | piighost-api:tests/test_auth.py::test_startup_fails_without_keys_by_default | couvert |
| AT-OPS-4-2 | OPS-4 | Étant donné le serveur, quand un client envoie un texte de 2 000 000 caractères, alors la requête est rejetée | 413 | piighost-api:tests/test_app.py::test_oversized_body_is_rejected | couvert |
| AT-OPS-4-3 | OPS-4 | Étant donné PIIGHOST_RATE_LIMIT=minute:1, quand un client envoie deux requêtes dans la minute, alors la seconde est refusée | 200 puis 429, /health reste à 200 | piighost-api:tests/test_app.py::test_rate_limit_throttles_second_request | couvert |
| AT-OPS-5-1 | OPS-5 | Étant donné un détecteur NER avec max_chars=20 et auto_chunk=True, quand « SECRET » arrive après 96 caractères, alors il est détecté | Une détection dont le span lit « SECRET » dans le texte d'origine | tests/components/detector/ner/test_base.py::TestMaxChars::test_long_text_is_chunked_and_offsets_remapped | couvert (détecteur NER factice dérivé de BaseNERDetector) |
| AT-OPS-5-2 | OPS-5 | Étant donné max_chars=20 et auto_chunk=False, quand un texte de 200 caractères passe, alors le détecteur refuse | TextTooLongError | tests/components/detector/ner/test_base.py::TestMaxChars::test_long_text_raises_when_auto_chunk_is_off | couvert |
| AT-OPS-6-1 | OPS-6 | Étant donné la référence catalog:piighost/notarial:2f602547, quand on appelle load_pipeline dessus, alors le pipeline se construit et tourne | « écrivez à a@b.co » devient « écrivez à <<EMAIL:1>> » | tests/config/test_catalog_config.py::TestLoadConfigFromTheCatalog::test_load_pipeline_builds_and_runs_it | couvert (catalogue simulé) |
| AT-OPS-6-2 | OPS-6 | Étant donné la même référence épinglée, quand on appelle load_pipeline deux fois, alors le second appel lit le cache disque | Une seule requête HTTP vers le catalogue | tests/test_catalog.py::TestPullConfig::test_a_pinned_configuration_is_fetched_once | partiel (le cache est vérifié sur pull_config, pas de bout en bout via load_pipeline) |
| AT-OPS-6-3 | OPS-6 | Étant donné une référence sans commit, quand on la charge deux fois, alors le catalogue est interrogé deux fois | Deux requêtes, rien en cache | tests/test_catalog.py::TestCache::test_a_moving_selector_is_never_cached | couvert |
| AT-OPS-6-4 | OPS-6 | Étant donné un catalogue en file://, ou un catalogue qui sert un détecteur de modèle, quand on tire une référence, alors elle est refusée | CatalogUrlError ou CatalogPayloadError | tests/test_catalog.py::TestPull::test_a_catalog_that_is_not_http_is_refusedtests/test_catalog.py::TestPull::test_a_model_detector_is_refused | couvert |
| AT-OPS-7-1 | OPS-7 | Étant donné une mémoire en processus construite sans réglage, quand 10 001 conversations sont écrites, ou qu'une conversation reste un jour sans message, alors la plus ancienne ou l'inactive est oubliée | La première conversation ne rend plus ses détections | tests/conversation_memory/test_in_memory.py::TestBounding::test_the_default_store_caps_its_thread_counttests/conversation_memory/test_in_memory.py::TestBounding::test_the_default_store_expires_an_idle_threadtests/config/test_thread_pipeline.py::TestInMemoryConfig::test_a_bare_section_is_bounded_like_the_store | couvert |
Utilisateur (de l'application construite avec piighost)
| Id | Besoin | Scénario | Résultat attendu | Tests existants | Statut |
|---|---|---|---|---|---|
| AT-USER-1-1 | USER-1 | Étant donné une application LangChain, Pydantic AI, LlamaIndex ou passant par un proxy, quand le modèle répond « Bonjour <<PERSON:1>> », alors l'utilisateur lit la vraie valeur | « Bonjour Jean Dupont », jamais « Bonjour <<PERSON:1>> » | tests/integrations/langchain/test_middleware.py::TestWhenInstalled::test_after_model_deanonymizes_for_displaytests/integrations/test_pydantic_ai_hooks.py::TestAroundTheModel::test_model_sees_placeholders_and_reply_is_restoredtests/integrations/llama_index/test_query_engine.py::TestQuery::test_anonymizes_query_and_restores_answerpiighost-api:tests/routes/test_openai_chat.py::test_upstream_sees_tokens_reply_is_restoredpiighost-api:tests/routes/test_anthropic_messages.py::test_upstream_sees_tokens_reply_is_restored | couvert (le test LlamaIndex est ignoré sans l'extra) |
| AT-USER-1-2 | USER-1 | Étant donné Claude Code avec les hooks piighost, quand l'assistant répond avec un jeton, alors l'utilisateur lit la vraie valeur | La réponse affichée ne contient aucun jeton | aucun | absent (limite connue, handle_hook ne traite que UserPromptSubmit, PreToolUse et PostToolUse) |
| AT-USER-2-1 | USER-2 | Étant donné une conversation qui cite « Claire » puis « Claire et Marc », quand le modèle répond « Merci <<PERSON:1>> et <<PERSON:2>>. », alors chaque personne garde son jeton et sa valeur | « <<PERSON:1>> et <<PERSON:2>> » puis « Merci Claire et Marc. » | tests/pipeline/test_thread.py::TestThreadConsistency::test_a_new_value_gets_the_next_numbertests/pipeline/test_thread.py::TestDeanonymize::test_restores_tokens_the_pipeline_never_anonymized | couvert (on ne peut pas tester de façon déterministe que le modèle ne confond pas deux jetons) |
| AT-USER-3-1 | USER-3 | Étant donné un agent LangChain ou Pydantic AI, quand l'assistant envoie un e-mail à « <<EMAIL:1>> », alors l'outil reçoit la vraie adresse | L'outil reçoit « patrick@acme.com » | tests/integrations/langchain/test_middleware_e2e.py::test_second_model_call_never_sees_clear_tool_argstests/integrations/test_pydantic_ai_hooks.py::TestTools::test_tool_gets_the_value_and_its_result_is_reanonymized | couvert |
| AT-USER-3-2 | USER-3 | Étant donné un proxy OpenAI ou Anthropic, quand le modèle renvoie un appel d'outil avec {"who": "<<PERSON:1>>"}, en flux ou non, alors le client reçoit l'argument restauré | {"who": "Patrick"} dans les quatre cas (OpenAI et Anthropic, en flux et non) | piighost-api:tests/routes/test_rewrite.py::test_deanonymize_chat_response_restores_content_and_tool_argspiighost-api:tests/routes/test_anthropic_shape.py::test_restorer_restores_tool_input_json_safelypiighost-api:tests/routes/test_anthropic_messages.py::test_tool_result_anonymized_and_tool_use_restored | partiel (le cas OpenAI en flux n'est ni restauré ni testé) |
| AT-USER-4-1 | USER-4 | Étant donné le décodeur de flux, quand « Bonjour <<PER » arrive, alors seul « Bonjour » est émis, puis « SON:1>>! » émet « Claire! » | Aucun morceau émis ne contient « <<PER » | tests/components/placeholder/test_streaming_async.py::TestFeed::test_a_split_token_is_held_then_completedtests/components/placeholder/test_streaming.py::TestFeed::test_split_delimiter_is_held | couvert (un flux coupé au milieu d'un jeton rend « <<PER » à la fin, par choix, voir tests/integrations/test_deidentify_stream.py::test_trailing_incomplete_token_is_flushed) |
| AT-USER-4-2 | USER-4 | Étant donné un proxy en flux, quand l'amont coupe « <<PERSON:1>> » en deux événements, alors le client ne reçoit jamais de fragment | Le flux reçu contient « Patrick » et pas « <<PERSON » | piighost-api:tests/routes/test_openai_stream.py::test_stream_restores_a_token_split_across_chunkspiighost-api:tests/routes/test_anthropic_messages.py::test_stream_restores_token_split_across_deltas | couvert |
| AT-USER-5-1 | USER-5 | Étant donné « Paris » dans la liste à laisser en clair, quand « Visitez Paris » passe, alors la ville reste lisible | « Visitez Paris » | tests/pipeline/test_override_integration.py::TestBasePipelineOverride::test_allow_list_keeps_a_false_positive_in_clear | couvert |
| AT-USER-5-2 | USER-5 | Étant donné un groupe générique du catalogue, quand « Réunion le 12/03/2026 » passe, alors la date reste en clair | « Réunion le 12/03/2026 » | aucun | absent (dépend du contenu des groupes du catalogue, jamais chargé dans les tests de la bibliothèque) |
| AT-USER-6-1 | USER-6 | Étant donné un détecteur qui rate « Acme », quand l'utilisateur ajoute la détection, alors « Acme recrute » part en jeton et se restaure | « <<ORG:1>> recrute », restauré en « Acme » | tests/pipeline/test_thread_hitl.py::TestAnonymizeCorrected::test_adds_a_missed_valuetests/pipeline/test_thread_hitl.py::TestAnonymizeCorrected::test_added_value_is_deanonymizable_thread_wide | couvert |
| AT-USER-6-2 | USER-6 | Étant donné « Paris » masqué à tort, quand l'utilisateur retire la détection, alors ce message part en clair et les suivants ne changent pas | « Visitez Paris », puis « Allez à <<LOCATION:1>> » | tests/pipeline/test_thread_hitl.py::TestAnonymizeCorrected::test_drops_a_false_positivetests/pipeline/test_thread_hitl.py::TestAnonymizeCorrected::test_correction_is_local_to_the_message | couvert |
| AT-USER-6-3 | USER-6 | Étant donné une liste à masquer qui contient la valeur, quand l'utilisateur la retire, alors elle reste masquée | Un jeton, pas la valeur | tests/pipeline/test_override_integration.py::TestThreadPipelineOverride::test_deny_list_trumps_a_hitl_droptests/pipeline/test_override_integration.py::TestThreadPipelineOverride::test_allow_list_trumps_a_hitl_add | couvert |
Bilan
| Profil | Couvert | Partiel | Absent | Total |
|---|---|---|---|---|
| DPO | 14 | 1 | 2 | 17 |
| DEV | 16 | 2 | 0 | 18 |
| OPS | 10 | 3 | 0 | 13 |
| USER | 9 | 1 | 2 | 12 |
| Total | 49 | 7 | 4 | 60 |
Manquants
Classés par risque, en commençant par ceux qui peuvent causer une fuite.
- AT-USER-3-2 (partiel) : arguments d'outil restaurés dans le flux OpenAI. Défaut fonctionnel connu, l'outil reçoit un jeton.
- AT-DPO-2-1 (partiel) : un vrai groupe du catalogue (IBAN, NIR) masque ses valeurs. Seul un catalogue simulé est chargé.
- AT-DPO-10-1 (absent) : la forme de stockage des corrections humaines, fonction à concevoir.
- AT-OPS-3-1 (partiel) : le refus sans secrets est vérifié via
load_thread_pipeline, pas au démarrage depiighost-api. - AT-OPS-1-1 (partiel) :
servesur une référence du catalogue puisPOST /v1/anonymizedans un seul test. - AT-DEV-6-2 (partiel) :
piighost validatesur une clé mal orthographiée, avec la clé nommée dans le message. - AT-DEV-1-2 (partiel) : le SDK
openaiavec seulementbase_urlchangé. - AT-OPS-6-2 (partiel) : le cache disque vu à travers
load_pipeline. - AT-USER-5-2 (absent) : une date n'est pas masquée par un groupe générique. Dépend du contenu des groupes, à tester dans le dépôt du catalogue,
piighost-hub. - AT-USER-1-2 (absent) : réponse affichée restaurée sous Claude Code, impossible tant qu'aucun hook ne réécrit la réponse.
- AT-DPO-8-1 (absent) : contenu de la page AIPD, contrôlable par un test de documentation.
Comportements testés absents de l'inventaire
| Comportement | Tests qui le gardent | Besoin ou critère proposé |
|---|---|---|
| Un jeton tapé par l'utilisateur ne restaure pas la valeur d'un autre | tests/components/anonymizer/test_span_anonymizer.py::TestAnonymize::test_user_typed_token_cannot_hijack_a_restoretests/components/anonymizer/test_span_anonymizer.py::TestDeanonymize::test_no_prefix_collision_when_one_token_prefixes_another | Critère de DPO-1 : « un message qui contient <<PERSON:2>> ne fait pas apparaître la valeur de la deuxième personne » |
| Une valeur tapée avec des espaces insécables reste masquée et garde son jeton | tests/pipeline/test_pipeline.py::TestUnicodeSpaces::test_a_regex_value_typed_with_no_break_spaces_is_hiddentests/pipeline/test_thread.py::TestUnicodeSpaces::test_a_value_keeps_its_token_across_spacings_and_messages | Critère de DPO-1 |
| Deux détections qui se chevauchent ne laissent aucun fragment en clair | tests/pipeline/test_pipeline.py::TestAnonymize::test_overlapping_detections_are_resolved_by_defaulttests/pipeline/test_pipeline.py::TestMergeOverlap::test_the_merge_resolver_leaves_no_fragment_of_a_longer_span | Critère de DPO-1 |
| L'historique, le contenu en blocs et les arguments d'outil déjà passés repartent en jetons | tests/integrations/test_pydantic_ai_hooks.py::TestMultiTurn::test_prior_turn_pii_is_not_leaked_to_the_modeltests/integrations/langchain/test_middleware.py::TestWhenInstalled::test_before_model_anonymizes_block_contenttests/integrations/langchain/test_middleware.py::TestToolCalls::test_before_model_reanonymizes_clear_tool_call_args | Critère de DPO-1 : « aucun tour suivant ne renvoie une valeur en clair » |
Le texte analysé par LLMDetector ne peut pas sortir de sa zone de données (injection) | tests/components/detector/test_llm.py::TestDetect::test_a_data_tag_in_the_text_cannot_close_the_data_region | Point de vigilance de DPO-1 |
Tracer en clair, ou stocker en clair dans Redis ou une base SQL réseau, émet PIIGhostSecurityWarning | tests/observation/test_pipeline_spans.py::TestClearTextTracingWarning::test_warns_without_a_redactor_or_acknowledgmenttests/conversation_memory/test_redis.py::TestRedisPlaintext::test_round_trips_without_cryptotests/conversation_memory/test_sqlalchemy.py::TestWarning::test_networked_dialect_without_crypto_warns | Critère d'OPS-3 |
| Avec des clés configurées, une route protégée sans jeton Bearer répond 401 | piighost-api:tests/test_auth.py::test_protected_route_401s_without_bearer_when_auth_enabled | Critère d'OPS-4 |
| Le proxy OpenAI oublie la conversation éphémère d'une requête sans en-tête de conversation | piighost-api:tests/routes/test_openai_chat.py::test_ephemeral_thread_is_forgotten | Critère de DPO-6 , au titre de la minimisation |
piighost anonymize dé-identifie un argument ou l'entrée standard sans fichier de config | tests/cli/test_cli.py::TestAnonymize::test_default_detector_anonymizes_an_argumenttests/cli/test_cli.py::TestAnonymize::test_reads_stdin_on_dash | un besoin développeur à créer (dé-identifier sans fichier de configuration) |
| Le middleware refuse un pipeline dont les jetons ne sont pas reconnaissables | tests/integrations/langchain/test_middleware.py::TestFactoryContract::test_a_pipeline_without_a_recognizer_is_refused | Critère de DEV-1 |
BridgeDetector lit les positions dans l'unité du runner (UTF-16 côté navigateur) | tests/components/detector/ner/test_bridge.py::TestDetect::test_offsets_are_read_in_the_runner_unit | un besoin développeur à créer (détection locale dans le navigateur) |