File: WorkflowScripts\AnalyzeCiFailureReportTemplatesTests.cs
Web Access
Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.TestUtilities;
using VerifyXunit;
using Xunit;
 
namespace Infrastructure.Tests;
 
[Trait("Category", "AgenticWorkflow")]
public sealed class AnalyzeCiFailureReportTemplatesTests(ITestOutputHelper output)
{
    [Theory]
    [RequiresTools(["node"])]
    [InlineData("automatic", "workflow_run", "40727814372")]
    [InlineData("manual", "workflow_dispatch", "9223372036854775807")]
    [InlineData("zero", "workflow_dispatch", "0")]
    [InlineData("leading-zero", "workflow_dispatch", "01")]
    [InlineData("out-of-range", "workflow_dispatch", "9223372036854775808")]
    [InlineData("injection", "workflow_dispatch", "1)\n::error::injected")]
    public async Task OperationalTemplatesIncludeTrustedSourceContext(string scenario, string eventName, string sourceRunId)
    {
        var result = await RunAsync(eventName, sourceRunId);
 
        Assert.True(result["installed"]!.GetValue<bool>());
        Assert.True(result["originalsUnchanged"]!.GetValue<bool>());
        Assert.Equal(scenario is "automatic" or "manual" ? 0 : 1, result["warnings"]!.AsArray().Count);
        await Verifier.Verify(result["templates"]!.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseDirectory("Snapshots")
            .UseParameters(scenario);
    }
 
    [Theory]
    [RequiresTools(["node"])]
    [InlineData("agent_failure_issue.md")]
    [InlineData("failed_jobs_issue.md")]
    public async Task MissingPinnedTemplateDoesNotSelectPartialOverrides(string missingTemplate)
    {
        var result = await RunAsync("workflow_run", "40727814372", missingTemplate);
 
        Assert.False(result["installed"]!.GetValue<bool>());
        Assert.True(result["originalsUnchanged"]!.GetValue<bool>());
        Assert.Contains(missingTemplate, result["error"]!.GetValue<string>(), StringComparison.Ordinal);
    }
 
    [Theory]
    [RequiresTools(["node"])]
    [InlineData("SOURCE_EVENT", "pull_request")]
    [InlineData("SOURCE_RUN_ID", "0")]
    [InlineData("GITHUB_REPOSITORY", "microsoft/aspire)\nmalformed")]
    [InlineData("GITHUB_SERVER_URL", "https://github.com/other")]
    [InlineData("GITHUB_SERVER_URL", "https://user:password@github.com")]
    public async Task InvalidTrustedContextDoesNotSelectOverrides(string key, string value)
    {
        var result = await RunAsync(
            "workflow_run",
            "40727814372",
            overrides: new JsonObject { [key] = value });
 
        Assert.False(result["installed"]!.GetValue<bool>());
        Assert.True(result["originalsUnchanged"]!.GetValue<bool>());
        Assert.False(string.IsNullOrEmpty(result["error"]!.GetValue<string>()));
    }
 
    private async Task<JsonNode> RunAsync(
        string eventName,
        string sourceRunId,
        string? missingTemplate = null,
        JsonObject? overrides = null)
    {
        using var workspace = TemporaryWorkspace.Create(output);
        var inputs = Path.Combine(workspace.Path, "inputs.json");
        await File.WriteAllTextAsync(inputs, JsonSerializer.Serialize(new { sourceRunId, overrides }));
        var script = """
            const fs = require('node:fs');
            const path = require('node:path');
            const { prepareReportTemplates } = require(process.argv[1]);
            const root = process.argv[2];
            const eventName = process.argv[3];
            const input = JSON.parse(fs.readFileSync(process.argv[4], 'utf8'));
            const sourceId = input.sourceRunId;
            const missing = process.argv[5];
            const names = ['agent_failure_issue.md', 'agent_failure_comment.md', 'failed_jobs_issue.md',
              'missing_data_issue.md', 'missing_tool_issue.md'];
            const prompts = path.join(root, 'gh-aw', 'prompts');
            fs.mkdirSync(prompts, {recursive: true});
            const original = 'Upstream report for {workflow_name}.\n\n[Analyzer run]({run_url})\n';
            for (const name of names) if (name !== missing) fs.writeFileSync(path.join(prompts, name), original);
            fs.writeFileSync(path.join(prompts, 'other.md'), 'Unrelated template\n');
            const githubEnv = path.join(root, 'github-env');
            fs.writeFileSync(githubEnv, '');
            const warnings = [];
            console.warn = message => warnings.push(message);
            let error;
            try {
              prepareReportTemplates({
                RUNNER_TEMP: root, GITHUB_ENV: githubEnv, GITHUB_REPOSITORY: 'microsoft/aspire',
                GITHUB_SERVER_URL: 'https://github.com', SOURCE_EVENT: eventName, SOURCE_RUN_ID: sourceId,
                ...input.overrides
              });
            } catch (e) { error = e.message; }
            const envText = fs.readFileSync(githubEnv, 'utf8');
            const selected = envText.match(/^GH_AW_PROMPTS_DIR=(.+)$/m)?.[1].trim();
            const templates = {};
            if (selected) for (const name of [...names, 'other.md']) {
              templates[name] = fs.readFileSync(path.join(selected, name), 'utf8').replace(/\r\n/g, '\n');
            }
            console.log(JSON.stringify({
              installed: !!selected, templates, error, warnings,
              originalsUnchanged: names.filter(name => name !== missing).every(name =>
                fs.readFileSync(path.join(prompts, name), 'utf8') === original)
            }));
            """;
        using var command = new NodeCommand(output, label: "analyzer-report-templates")
            .WithWorkingDirectory(RepoRoot.Path)
            .WithTimeout(TimeSpan.FromSeconds(30));
        var result = await command.ExecuteScriptAsync(
            "-e",
            script,
            Path.Combine(RepoRoot.Path, ".github", "workflows", "analyze-ci-failure", "prepare-report-templates.js"),
            workspace.Path,
            eventName,
            inputs,
            missingTemplate ?? "");
        Assert.True(result.ExitCode == 0, result.Output);
        return JsonNode.Parse(result.Output)!;
    }
}