Ajoutez un fournisseur IA à Microsoft Testing Platform, conservez des tests de pull request déterministes et réservez l’analyse des échecs à une CI protégée.

Tests .NET : intégrer une IA à Microsoft Testing Platform
Une IA qui analyse les échecs de test peut raccourcir le diagnostic d’une régression, mais elle ne doit jamais devenir l’arbitre de votre suite de tests. Un modèle est non déterministe, coûteux, et les journaux qu’il reçoit peuvent contenir du code ou des données sensibles. La bonne intégration distingue donc deux choses : les assertions reproductibles qui décident du statut d’une pull request, et une analyse IA strictement consultative, exécutée dans un environnement protégé.
Microsoft a publié fin août 2026 une intégration IA expérimentale pour Microsoft Testing Platform (MTP). Le package Microsoft.Testing.Platform.AI permet à une extension MTP de demander un IChatClient, sans la coupler à un fournisseur. Le fournisseur de référence Microsoft.Testing.Extensions.AzureFoundry crée ce client pour Azure AI Foundry. C’est une brique d’extension : elle n’ajoute pas, à elle seule, de comportement IA à vos tests.
Dans ce tutoriel, nous construisons la frontière qui manque souvent dans une CI : une extension reçoit un résumé d’échecs déjà filtré, demande une hypothèse de diagnostic à un modèle, valide un format JSON limité et publie un rapport à relire. Les tests unitaires restent entièrement locaux ; le job IA ne reçoit une identité OIDC que hors des pull requests de forks.
API expérimentale. Les API IA de MTP portent le diagnostic
TPEXPet peuvent évoluer. Isolez la suppression d’avertissement au plus près de l’appel, épinglez les versions NuGet dans votre verrou de dépendances et validez toute mise à jour sur une branche dédiée. Une API expérimentale n’est jamais un feu vert pour étendre les permissions de votre pipeline.
Ce qui est nouveau — et ce que cela ne remplace pas
MTP est un runner de tests portable, utilisé depuis le terminal, les IDE et la CI. Ses extensions sont découvertes et enregistrées automatiquement lorsqu’elles sont installées avec l’intégration MSBuild. La nouvelle couche IA s’insère dans ce modèle : une extension récupère un IChatClient auprès du IServiceProvider de la plateforme, au lieu de dépendre directement d’un SDK de modèle.
Cette séparation est utile lorsque la même extension doit pouvoir fonctionner avec plusieurs fournisseurs, ou lorsque l’application de test choisit l’identité et le fournisseur. Elle est différente de l’article Évaluer une IA .NET dans une pipeline CI, qui mesure la qualité d’une application IA. Ici, l’IA est un assistant de diagnostic pour la plateforme de tests elle-même.
| Besoin | Mécanisme à privilégier |
|---|---|
| Décider si une pull request est acceptable | Tests unitaires, intégration et assertions déterministes |
| Diagnostiquer un ensemble d’échecs répétés | Extension MTP avec rapport consultatif |
| Mesurer la qualité d’une réponse de votre produit IA | Corpus et évaluateurs CI |
| Relancer un test instable | Politique de retry explicite, jamais une conclusion de modèle |
| Corriger ou fusionner du code | Revue humaine et protections de branche |

Prérequis et périmètre de sécurité
Préparez les éléments suivants :
- le SDK .NET 10 ou ultérieur et une suite utilisant Microsoft Testing Platform ;
- une ressource Azure OpenAI accessible via Azure AI Foundry, uniquement pour le job consultatif ;
- une application Microsoft Entra avec une crédential fédérée GitHub Actions, limitée à l’inférence nécessaire ;
- un environnement GitHub, par exemple
ai-test-review, avec réviseurs requis si le rapport a un impact opérationnel ; - un emplacement d’artefacts dont la rétention est compatible avec votre politique de données.
Ne transmettez pas un dump complet par défaut. Un message d’exception, une pile et le nom d’un test peuvent révéler des noms de clients, des chemins internes, des paramètres HTTP ou du code. Commencez avec un sous-ensemble connu : nom de test, catégorie d’erreur, première ligne normalisée et hash de build. N’envoyez les détails complémentaires qu’après validation avec les responsables sécurité et conformité.
L’authentification par clé est supportée par le fournisseur de référence via AZURE_OPENAI_API_KEY, mais une identité fédérée évite de conserver une clé de modèle dans GitHub. Important : si AZURE_OPENAI_API_KEY est défini, le fournisseur l’utilise avant une TokenCredential. Ne laissez donc pas une clé résiduelle transformer silencieusement un job OIDC en job à secret statique.
1. Préparer un projet de tests MTP
Dans un nouveau projet de démonstration, installez le package d’abstraction et le fournisseur de référence. Les versions de cette intégration étant expérimentales, utilisez la version publiée compatible avec votre runner MTP, puis conservez-la dans packages.lock.json.
dotnet new mstest --framework net10.0 --name Inventory.Tests
Set-Location Inventory.Tests
dotnet add package Microsoft.Testing.Platform.AI --prerelease
dotnet add package Microsoft.Testing.Extensions.AzureFoundry --prerelease
dotnet add package Azure.Identity
dotnet restore --use-lock-file
Une extension NuGet est normalement auto-enregistrée avec Microsoft.Testing.Platform.MSBuild. Pour contrôler explicitement le démarrage — notamment afin de passer une identité Entra — désactivez le point d’entrée généré dans le fichier projet et reprenez l’enregistrement du framework de test existant dans votre Main. La documentation MTP décrit ce mode pour les extensions qui nécessitent un code de configuration.
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>
</PropertyGroup>
Dans le point d’entrée MTP personnalisé, enregistrez le fournisseur avant de construire l’application de test. Le reste de l’enregistrement (MSTest, xUnit ou NUnit) dépend du runner déjà choisi par votre solution : conservez celui généré ou documenté par ce runner, plutôt que de mélanger des fragments issus de frameworks différents.
using Azure.Identity;
// builder est le ITestApplicationBuilder créé par votre point d’entrée MTP.
// L’enregistrement du framework de tests reste inchangé et précède BuildAsync().
#pragma warning disable TPEXP
builder.AddAzureOpenAIChatClientProvider(new DefaultAzureCredential());
#pragma warning restore TPEXP
Localement, DefaultAzureCredential peut utiliser une session az login. En CI, après azure/login, il résout le jeton de la charge de travail fédérée. Ne loggez ni le type de credential finalement sélectionné, ni l’en-tête d’autorisation. Ajoutez à la place un smoke test délibérément inoffensif dans l’environnement protégé : une requête minuscule qui confirme que l’identité peut appeler le déploiement prévu.
2. Garder le prompt petit, filtré et traçable
Une extension de diagnostic devrait prendre un objet métier déjà assaini, pas une sortie console brute. Le code suivant limite la taille, supprime les lignes fréquentes de secrets et construit un prompt qui demande une hypothèse, jamais une modification de code ou un déploiement.
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
public sealed record FailedTest(string Name, string ExceptionType, string Message);
public static partial class FailureDigestPrompt
{
private const int MaximumMessageLength = 500;
[GeneratedRegex("(?i)(api[_-]?key|authorization|password)\\s*[:=]\\s*\\S+")]
private static partial Regex SensitiveAssignment();
public static string Build(IEnumerable<FailedTest> failures, string buildId)
{
var safeFailures = failures.Take(20).Select(failure => new
{
Name = Clip(failure.Name),
Type = Clip(failure.ExceptionType),
Message = Clip(SensitiveAssignment().Replace(failure.Message, "$1=[REDACTED]")),
});
string payload = System.Text.Json.JsonSerializer.Serialize(safeFailures);
string traceId = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(buildId)))[..12];
return $$"""
Tu es un assistant de diagnostic. Analyse uniquement les échecs JSON ci-dessous.
Ne propose ni commande destructive, ni changement de permissions, ni secret.
Réponds en JSON avec les champs category, hypothesis et nextCheck.
build={{traceId}}
failures={{payload}}
""";
}
private static string Clip(string value) => value.Length <= MaximumMessageLength
? value
: value[..MaximumMessageLength] + "…";
}
Le hash court relie le rapport à un build sans envoyer son identifiant complet si celui-ci contient une information interne. Ce n’est pas une anonymisation cryptographique : une petite plage d’identifiants peut être devinée. Traitez-le comme un identifiant de corrélation, non comme une protection de données.
Avant tout appel distant, rendez cette transformation testable sans modèle. Ces tests s’exécutent sur toutes les pull requests et constituent le vrai garde-fou de l’exemple.
[TestClass]
public sealed class FailureDigestPromptTests
{
[TestMethod]
public void Build_redacts_secret_like_values_and_limits_each_message()
{
string longMessage = "password=very-secret " + new string('x', 600);
string prompt = FailureDigestPrompt.Build(
[new FailedTest("Checkout_rejects_invalid_card", "InvalidOperationException", longMessage)],
"build-2026-09-19-42");
StringAssert.Contains(prompt, "password=[REDACTED]");
StringAssert.DoesNotContain(prompt, "very-secret");
StringAssert.DoesNotContain(prompt, longMessage);
StringAssert.Contains(prompt, "Checkout_rejects_invalid_card");
}
}
Ajoutez des cas pour les jetons Bearer, les chaînes de connexion et les données personnelles propres à votre domaine. Une expression régulière n’est pas un DLP complet : la liste blanche de champs reste plus fiable que le nettoyage d’un texte libre.
3. Consommer le client depuis une extension, sans rendre le modèle obligatoire
Dans une extension MTP, récupérez le client fourni par l’application de test. GetChatClientAsync peut retourner null : le mode sans IA doit rester un chemin normal, pas une erreur qui casse tous les tests de développeurs ne disposant pas d’accès Foundry.
using Microsoft.Extensions.AI;
using Microsoft.Testing.Platform.AI;
public sealed class AiFailureReviewer
{
public async Task<string?> ReviewAsync(
IServiceProvider services,
IReadOnlyCollection<FailedTest> failures,
string buildId,
CancellationToken cancellationToken)
{
#pragma warning disable TPEXP
IChatClient? client = await services.GetChatClientAsync(cancellationToken);
#pragma warning restore TPEXP
if (client is null || failures.Count == 0)
{
return null;
}
string prompt = FailureDigestPrompt.Build(failures, buildId);
ChatResponse response = await client.GetResponseAsync(
[new ChatMessage(ChatRole.User, prompt)],
cancellationToken: cancellationToken);
return response.Text;
}
}
L’extension ne doit appeler cette méthode qu’après l’exécution des assertions et après avoir décidé que le jeu de données est autorisé. Ne modifiez pas les résultats MTP ni le code de sortie à partir de response.Text. Un modèle peut être indisponible, produire un JSON invalide ou simplement se tromper ; le résultat est un artefact diagnostique secondaire.
Si vous exigez un format structuré, désérialisez-le dans un type strict, vérifiez la taille des champs et écartez toute catégorie inconnue. Une validation minimale est préférable à une chaîne affichée directement dans un commentaire de pull request : les sorties de modèle sont des données non fiables et peuvent contenir des instructions injectées par le texte d’un échec.
public sealed record Diagnostic(string Category, string Hypothesis, string NextCheck);
public static bool IsAcceptable(Diagnostic diagnostic) =>
diagnostic.Category is "configuration" or "dependency" or "test" or "application"
&& diagnostic.Hypothesis.Length is > 0 and <= 800
&& diagnostic.NextCheck.Length is > 0 and <= 400;
Ne donnez au modèle aucun outil d’écriture, aucune permission GitHub et aucune capacité de déploiement. La valeur de l’extension est de proposer « vérifier le changement de variable X dans staging », pas d’exécuter cette vérification à votre place.
4. Séparer la CI de pull request du job IA
La CI de pull request doit compiler et tester sans identité Azure ni appel de modèle. Elle reste rapide, gratuite et reproductible, y compris lorsqu’une pull request vient d’un fork.
name: verify-tests
on:
pull_request:
permissions:
contents: read
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- run: dotnet test --configuration Release --no-restore
Le job consultatif est déclenché après intégration sur main, ou à la demande sur une branche de confiance. Il cible l’environnement GitHub protégé et demande explicitement id-token: write. Cette permission émet un jeton OIDC ; ne l’accordez pas aux jobs qui n’en ont pas l’usage.
name: review-failed-tests-with-ai
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
ai-review:
environment: ai-test-review
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: dotnet test tests/Inventory.Tests --configuration Release
env:
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
Créez la crédential fédérée Entra avec un sujet limité au dépôt, à la branche ou à l’environnement attendu. L’identité ne doit recevoir que le rôle nécessaire à l’inférence sur la ressource cible. Les valeurs d’identifiant, de tenant et d’abonnement restent des secrets GitHub selon les recommandations Azure ; l’endpoint et le nom de déploiement peuvent être des variables si votre politique les considère non sensibles.
Pour publier un rapport, uploadez un fichier JSON ou Markdown comme artefact avec une durée de rétention réduite. Ne commentez pas automatiquement une pull request depuis ce workflow : cela impliquerait un jeton à droits d’écriture et transforme une aide de diagnostic en surface d’attaque. Un mainteneur peut consulter l’artefact et transférer la conclusion utile dans la revue.
5. Vérifier le comportement avant la promotion
Exécutez localement les tests qui ne demandent aucun modèle :
dotnet test --configuration Release
Puis, dans un environnement Azure isolé, vérifiez ces cas avant d’activer le job sur main :
- aucun fournisseur enregistré : l’extension renvoie
nullet les tests gardent leur code de sortie habituel ; - fournisseur accessible : un échec synthétique produit un rapport, sans secret ni sortie console brute ;
- refus d’accès ou timeout : l’artefact contient une erreur d’infrastructure courte, sans retry infini ;
- sortie malformée : le parseur rejette le rapport et l’exécution de tests reste inchangée ;
- pull request de fork : aucun secret, environnement protégé ou jeton OIDC n’est rendu disponible.
Surveillez le nombre de rapports, le temps supplémentaire de la tâche IA, les erreurs d’authentification et le volume envoyé. Fixez un budget de tokens et une limite de temps globale. Si l’analyse échoue régulièrement, c’est un incident d’intégration à traiter ; ce n’est pas une raison de rendre les tests déterministes dépendants du fournisseur.
Points de vigilance
- Une analyse ne vaut pas une assertion. N’utilisez pas le texte d’un modèle pour passer, échouer ou relancer un test.
- Évitez les fuites par les logs. Nettoyez avant sérialisation, réduisez les artefacts et ne consignez jamais le prompt complet par défaut.
- Protégez la frontière OIDC. Limitez le sujet de la fédération et le rôle Azure ;
id-token: writen’est pas une permission anodine. - Traitez
nullcomme un mode supporté. Un poste de développement et une pull request doivent pouvoir exécuter les tests sans fournisseur IA. - Épinglez les préversions.
TPEXPsignale une surface en mouvement ; les tests de contrat autour de votre extension sont le filet de sécurité lors des mises à niveau. - Conservez la revue humaine. Un rapport peut grouper les échecs et suggérer une piste, jamais autoriser une correction, un secret ou un déploiement.
Ressources officielles
- Microsoft documente l’intégration IA de Microsoft Testing Platform, les packages,
GetChatClientAsync, le fournisseur Foundry et le diagnostic expérimentalTPEXP. - La présentation de Microsoft Testing Platform explique son exécution en CLI, IDE et CI ainsi que l’auto-enregistrement des extensions NuGet.
- Le guide des extensions MTP détaille les points d’extension et le rôle du
IServiceProvider. - Microsoft explique l’authentification Azure depuis GitHub Actions avec OIDC et la permission
id-token: writerequise parazure/login. - Le dépôt microsoft/testfx contient le code source open source de MTP et de ses extensions.

