File: AnalyzeCiFailureCohesionTests.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.Text;
using System.Text.Json.Nodes;
using AnalyzeCiFailure;
using Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
/// <summary>Pins lifecycle facts, completion aliases, exact report bytes and early-failure finalization.</summary>
public sealed partial class AnalyzeCiFailureWorkflowTests
{
    [Fact]
    public async Task EarlyClientFactoryFailureRetainsUnknownFactsAndOriginalError()
    {
        using var invocation = new AnalysisInvocation();
        _invocationDirectories.Add(invocation.RootDirectory);
        var output = Path.Combine(_workspace.Path, "workflow-output");
        var error = new ValidationException("client factory failed");
 
        var exitCode = await EvidencePreparation.CollectAsync(
            _workspace.Path,
            "microsoft/aspire",
            invocation,
            "123",
            runAttemptText: null,
            output,
            () => throw error,
            CancellationToken.None);
 
        Assert.Equal(1, exitCode);
        var outcome = TransientJson.Read<AnalysisOutcome>(await File.ReadAllTextAsync(invocation.Evidence.Outcome));
        Assert.Null(outcome.HasRelevantFailures);
        Assert.Null(outcome.RunId);
        Assert.Same(error, invocation.Capture.Failure);
        Assert.Equal("client factory failed", Assert.Single(outcome.Problems).Message);
        Assert.True(File.Exists(Path.Combine(invocation.RecoveryDirectory("collection"), "manifest.json")));
        Assert.Equal("analysis_ready=false\ncollection_status=failed\n", await File.ReadAllTextAsync(output));
    }
 
    [Fact]
    public async Task InvalidDecodedReplayRetainsRunFactsWithoutAuthorizingEvidence()
    {
        var input = new EvidencePaths(Path.Combine(_workspace.Path, "input"));
        Directory.CreateDirectory(input.RootDirectory);
        var bundle = AnalyzeCiFailureFixtures.CreateEvidence(RunScope.Main) with { Jobs = [] };
        await File.WriteAllTextAsync(input.Bundle, TransientJson.Write(bundle));
        using var invocation = new AnalysisInvocation();
        _invocationDirectories.Add(invocation.RootDirectory);
 
        await Assert.ThrowsAsync<ValidationException>(() => EvidencePreparation.ReplayAsync(input, invocation, CancellationToken.None));
 
        var outcome = TransientJson.Read<AnalysisOutcome>(await File.ReadAllTextAsync(invocation.Replay.Outcome));
        Assert.True(outcome.HasRelevantFailures);
        Assert.Equal(bundle.Run.Id, outcome.RunId);
        Assert.Equal(bundle.Run.Attempt, outcome.RunAttempt);
        Assert.Equal(bundle.Run.HeadSha, outcome.HeadSha);
        Assert.Equal(bundle.Run.Url, outcome.RunUrl);
        Assert.Equal("failed", outcome.Status);
        Assert.False(File.Exists(invocation.Replay.Bundle));
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData(false)]
    [InlineData(true)]
    public void RenamedFailureFlagKeepsCompleteOutcomeWireShape(bool? relevantFailures)
    {
        var outcome = new AnalysisProgress { HasRelevantFailures = relevantFailures }.CreateOutcome("ready");
        var json = JsonNode.Parse(TransientJson.Write(outcome))!.AsObject();
        Assert.Equal(
            ["schema_version", "status", "has_work", "run_id", "run_attempt", "head_sha", "run_url", "run_scope", "subject_pr_number", "problems"],
            json.Select(property => property.Key));
        Assert.Equal(relevantFailures, json["has_work"]?.GetValue<bool>());
    }
 
    [Theory]
    [InlineData("true", "success", "ready", "success", 0)]
    [InlineData("true", "skipped", "ready", "success", 1)]
    [InlineData("true", "skipped", "degraded", "success", 0)]
    [InlineData("true", "failure", "degraded", "success", 0)]
    [InlineData("true", "skipped", "degraded", "failure", 1)]
    [InlineData("true", "success", "failed", "failure", 0)]
    [InlineData("true", "success", "failed", "cancelled", 0)]
    [InlineData("false", "failure", "failed", "failure", 0)]
    [InlineData(null, "failure", "failed", "failure", 0)]
    [InlineData("", "failure", "failed", "failure", 0)]
    [InlineData("invalid", "success", "degraded", "success", 1)]
    public async Task CompletionCommandAndLegacyAliasPreserveResultPolicy(
        string? relevantFailures,
        string publication,
        string status,
        string collection,
        int expected)
    {
        foreach (var command in new[] { "check-completion", "require-publication" })
        {
            var result = await RunValidatorCommandAsync(
                ["workflow", command],
                new Dictionary<string, string>
                {
                    ["HAS_WORK"] = relevantFailures ?? "",
                    ["PUBLICATION_RESULT"] = publication,
                    ["COLLECTION_STATUS"] = status,
                    ["COLLECTION_RESULT"] = collection,
                });
            Assert.Equal(expected, result.ExitCode);
            if (expected == 1 && relevantFailures == "true")
            {
                Assert.Contains($"collection status: {status}", result.Output, StringComparison.Ordinal);
                Assert.Contains($"collection result: {collection}", result.Output, StringComparison.Ordinal);
                Assert.Contains($"publication result: {publication}", result.Output, StringComparison.Ordinal);
            }
        }
    }
 
    [Theory]
    [InlineData("check-completion")]
    [InlineData("require-publication")]
    public async Task CompletionHelpDoesNotAcquireCredentials(string command)
    {
        var result = await RunValidatorCommandAsync(["workflow", command, "--help"]);
        Assert.Equal(0, result.ExitCode);
        Assert.Empty(Directory.GetFileSystemEntries(_workspace.Path));
    }
 
    [Theory]
    [InlineData("populated")]
    [InlineData("empty")]
    [RequiresTools(["bash", "git"])]
    public async Task LimitedReportUsesExactUtf8LayoutAndAppendsWithoutRewritingSummary(string summaryKind)
    {
        var summary = Path.Combine(_workspace.Path, "summary.md");
        var prefix = new byte[] { 0xef, 0xbb, 0xbf, 0x41, 0x0d, 0x0a };
        await File.WriteAllBytesAsync(summary, prefix);
        var environment = new Dictionary<string, string>
        {
            ["GITHUB_STEP_SUMMARY"] = summaryKind == "empty" ? "" : summary
        };
        var result = await RunCollectionAsync("runner-missing-diagnostics", environment, useWorkflowAdapter: true);
        Assert.True(result.ExitCode == 0, result.Output);
        var expected = """
            # Limited CI analysis
 
            [Source CI run, attempt 2](https://github.com/microsoft/aspire/actions/runs/123/attempts/2)
 
            Structured test results are unavailable. This is a limited-evidence outcome, not a complete diagnosis.
 
            - Captured failed jobs: 1
            - Captured structured test failures: 0
            - Confirmed runner-loss jobs: 0
            - Artifact acquisition/selection problems: 1
            - Test-result parsing problems: 0
 
            No causal history, issues, or PR analysis comments were published. Reruns are not authorized.
 
            Inspect the `ci-failure-evidence`, collection recovery, and collection diagnostics artifacts for source job annotations and acquisition details.
            """.ReplaceLineEndings("\n") + "\n";
        var bytes = Encoding.UTF8.GetBytes(expected);
        Assert.Equal(bytes, await File.ReadAllBytesAsync(Path.Combine(EvidenceDirectory, "limited-analysis.md")));
        Assert.Equal(
            summaryKind == "populated" ? prefix.Concat(bytes).ToArray() : prefix,
            await File.ReadAllBytesAsync(summary));
    }
}