File: AnalyzeCiFailureCSharpRerunTests.cs
Web Access
Project: src\tests\AnalyzeCiFailure.Tests\AnalyzeCiFailure.Tests.csproj (AnalyzeCiFailure.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.Globalization;
using System.Text.Json;
using AnalyzeCiFailure;
using Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
/// <summary>Checks exact request binding, fresh run and PR facts, and guarded rerun effects.</summary>
public sealed partial class AnalyzeCiFailureWorkflowTests
{
    [Theory]
    [InlineData("9007199254740992", "enabled", true)]
    [InlineData("9007199254740993", "enabled", true)]
    [InlineData("9223372036854775807", "enabled", true)]
    [InlineData("9007199254740993", "main", true)]
    [InlineData("9007199254740993", "disabled", false)]
    [InlineData("9007199254740993", "enabled-uppercase", true)]
    [InlineData("9007199254740993", "enabled-one", false)]
    [InlineData("9007199254740993", "enabled-empty", false)]
    [InlineData("9007199254740993", "extra-field", true)]
    [InlineData("9007199254740993", "wrong-run", false)]
    [InlineData("9007199254740993", "wrong-pr", false)]
    [InlineData("9007199254740993", "unavailable", false)]
    [InlineData("9007199254740993", "infrastructure-tests", false)]
    [InlineData("9007199254740993", "code-issue", false)]
    [InlineData("9007199254740993", "closed", false)]
    [InlineData("9007199254740993", "locked", false)]
    [InlineData("9007199254740993", "unknown-pr", false)]
    [InlineData("9007199254740993", "lookup-failure", false)]
    [InlineData("9007199254740993", "advanced", false)]
    [InlineData("9007199254740993", "wrong-current-id", false)]
    [InlineData("9007199254740993", "missing-current-id", false)]
    [InlineData("9007199254740993", "missing-current-attempt", false)]
    [InlineData("9007199254740993", "malformed-current-attempt", false)]
    [InlineData("9007199254740993", "run-lookup-failure", false)]
    [InlineData("9007199254740993", "malformed-current-json", false)]
    [InlineData("9007199254740993", "post-failure", false)]
    [InlineData("9007199254740993", "duplicate-requests", false)]
    [InlineData("9223372036854775808", "enabled", false)]
    [InlineData("123\n", "enabled", false)]
    [InlineData("123\r", "enabled", false)]
    [InlineData("123\u2028", "enabled", false)]
    [RequiresTools(["bash"])]
    public async Task CSharpRerunValidatesFreshFactsAndUsesExactStringApiPaths(string id, string scenario, bool mutation)
    {
        var parsedId = long.TryParse(id, NumberStyles.None, CultureInfo.InvariantCulture, out var number) && number > 0
            ? number : 9007199254740993;
        var evidence = AnalyzeCiFailureFixtures.CreateEvidence(scenario == "main" ? RunScope.Main : RunScope.PullRequest);
        evidence = evidence with
        {
            Run = evidence.Run with { Id = parsedId, Url = $"https://github.com/microsoft/aspire/actions/runs/{parsedId}" },
            SubjectPr = evidence.SubjectPr is { } pr ? pr with { Number = 9007199254740993 } : null,
            TestEvidence = evidence.TestEvidence with { State = scenario == "unavailable" ? EvidenceState.Unavailable : EvidenceState.Complete },
        };
        var proposal = AnalyzeCiFailureFixtures.CreateInfrastructureProposal(evidence);
        if (scenario == "infrastructure-tests")
        {
            evidence = evidence with
            {
                TestEvidence = evidence.TestEvidence with
                {
                    Failures =
                    [
                        new CollectedTestFailure
                        {
                            Ref = "test-1", JobId = 123, Name = "Tests.Fixture", DisplayName = "Tests.Fixture",
                            Diagnostics = new CollectedDiagnostics { Error = "Runner prerequisite failed", StackTrace = "", StandardOutput = "", StandardError = "" },
                            Sources = [new TestResultSource { ArtifactId = 10, ResultFile = "results.trx", Format = TestArtifactFormat.Trx, ExecutionId = "execution" }],
                        },
                    ],
                },
            };
            proposal = proposal with
            {
                Tests = [new ProposedTestDecision { Ref = "test-1", Classification = TestClassification.TransientInfrastructure, Reason = "Runner prerequisite failed" }],
            };
        }
        if (scenario == "code-issue")
        {
            proposal = proposal with { Jobs = [proposal.Jobs[0] with { Classification = JobClassification.CodeIssue }], Causes = [] };
        }
        var data = Directory.CreateDirectory(EvidenceDirectory).FullName;
        var analysis = Directory.CreateDirectory(Path.Combine(_workspace.Path, "proposal")).FullName;
        await File.WriteAllTextAsync(Path.Combine(data, "evidence.json"), TransientJson.Write(evidence));
        await File.WriteAllTextAsync(Path.Combine(analysis, "proposal.json"), TransientJson.Write(proposal));
        // A planted legacy acceptance file must not replace fresh evidence/proposal validation.
        await File.WriteAllTextAsync(
            Path.Combine(analysis, "accepted.json"),
            """{"schema_version":1,"verdict":"transient-infra","run_id":"9007199254740993"}""");
        var request = new
        {
            type = "rerun_failed_jobs", run_id = scenario == "wrong-run" ? "1" : id,
            pr_numbers = scenario == "main" ? "" : scenario == "wrong-pr" ? "1" : "9007199254740993",
            reason = "Transient infrastructure",
        };
        var output = Path.Combine(_workspace.Path, "agent-output.json");
        await File.WriteAllTextAsync(
            output,
            JsonSerializer.Serialize(new
            {
                items = scenario == "duplicate-requests" ? new[] { request, request } : [request],
            }));
        if (scenario == "extra-field")
        {
            var envelope = System.Text.Json.Nodes.JsonNode.Parse(await File.ReadAllTextAsync(output))!;
            envelope["engine_metadata"] = "not authority";
            envelope["items"]![0]!["engine_added_field"] = 123;
            await File.WriteAllTextAsync(output, envelope.ToJsonString());
        }
        var fakeGh = await CreateFakeGhAsync("""
            #!/usr/bin/env bash
            set -eu
            printf '%s\n' "$*" >> rerun-calls.log
            case "$*" in
              "api repos/microsoft/aspire/pulls/"*)
                if [ "$RERUN_SCENARIO" = lookup-failure ]; then printf 'lookup failed: token=private-value' >&2; exit 23; fi
                if [ "$RERUN_SCENARIO" = unknown-pr ]; then printf '%s' '{"state":"open","locked":"false"}'
                elif [ "$RERUN_SCENARIO" = closed ]; then printf '%s' '{"state":"closed","locked":false}'
                elif [ "$RERUN_SCENARIO" = locked ]; then printf '%s' '{"state":"open","locked":true}'
                else printf '%s' '{"state":"open","locked":false}'; fi ;;
              *"/rerun-failed-jobs --method POST")
                if [ "$RERUN_SCENARIO" = post-failure ]; then printf 'request failed: token=private-value' >&2; exit 23; fi
                printf '{}' ;;
              "api repos/microsoft/aspire/actions/runs/"*)
                if [ "$RERUN_SCENARIO" = run-lookup-failure ]; then printf 'lookup failed: token=private-value' >&2; exit 23; fi
                if [ "$RERUN_SCENARIO" = malformed-current-json ]; then printf 'not JSON'
                elif [ "$RERUN_SCENARIO" = missing-current-id ]; then printf '{"run_attempt":1}'
                elif [ "$RERUN_SCENARIO" = missing-current-attempt ]; then printf '{"id":%s}' "$RERUN_ID"
                elif [ "$RERUN_SCENARIO" = malformed-current-attempt ]; then printf '{"id":%s,"run_attempt":"1"}' "$RERUN_ID"
                else printf '{"id":%s,"run_attempt":%s}' "$RERUN_LOOKUP_ID" "$RERUN_ATTEMPT"; fi ;;
              *) printf 'Unexpected call: %s\n' "$*" >&2; exit 99 ;;
            esac
            """);
        Assert.True(File.Exists(fakeGh));
 
        var result = await RunValidatorCommandAsync(
            ["workflow", "rerun", "--repo", "microsoft/aspire", "--analysis-dir", analysis, "--working-dir", _workspace.Path],
            new Dictionary<string, string>
            {
                ["PATH"] = $"{Path.GetDirectoryName(fakeGh)}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}",
                ["GH_AW_AGENT_OUTPUT"] = output,
                ["ENABLE_RERUN"] = scenario switch
                {
                    "disabled" => "false",
                    "enabled-empty" => "",
                    "enabled-one" => "1",
                    "enabled-uppercase" => "TRUE",
                    _ => "true",
                },
                ["RERUN_SCENARIO"] = scenario,
                ["RERUN_ID"] = parsedId.ToString(CultureInfo.InvariantCulture),
                ["RERUN_LOOKUP_ID"] = scenario == "wrong-current-id" ? "99" : parsedId.ToString(CultureInfo.InvariantCulture),
                ["RERUN_ATTEMPT"] = scenario == "advanced" ? "2" : "1",
            });
        var calls = File.Exists(Path.Combine(_workspace.Path, "rerun-calls.log"))
            ? await File.ReadAllLinesAsync(Path.Combine(_workspace.Path, "rerun-calls.log")) : [];
        var runEndpoint = $"api repos/microsoft/aspire/actions/runs/{parsedId}";
        var prEndpoint = "api repos/microsoft/aspire/pulls/9007199254740993";
        if (mutation)
        {
            Assert.Equal(0, result.ExitCode);
            Assert.Equal(
                scenario == "main"
                ? new[] { runEndpoint, runEndpoint + "/rerun-failed-jobs --method POST" }
                : [prEndpoint, runEndpoint, runEndpoint + "/rerun-failed-jobs --method POST"],
                calls);
        }
        else if (scenario is "closed" or "locked" or "unknown-pr" or "lookup-failure" or "advanced")
        {
            Assert.Equal(0, result.ExitCode);
            Assert.Equal(scenario == "advanced" ? new[] { prEndpoint, runEndpoint } : [prEndpoint], calls);
        }
        else if (scenario is "wrong-current-id" or "missing-current-id" or "missing-current-attempt" or "malformed-current-attempt" or
            "run-lookup-failure" or "malformed-current-json" or "post-failure")
        {
            Assert.Equal(1, result.ExitCode);
            Assert.Equal(
                scenario == "post-failure"
                ? new[] { prEndpoint, runEndpoint, runEndpoint + "/rerun-failed-jobs --method POST" }
                : [prEndpoint, runEndpoint],
                calls);
            Assert.Contains(evidence.Run.Url, result.Output, StringComparison.Ordinal);
            if (scenario is "run-lookup-failure" or "post-failure")
            {
                Assert.Contains("HTTP 500.", result.Output, StringComparison.Ordinal);
            }
        }
        else
        {
            Assert.Equal(scenario is "disabled" or "enabled-empty" or "enabled-one" ? 0 : 1, result.ExitCode);
            Assert.Empty(calls);
        }
    }
}