File: TestTriggerMap\SelectTestsWorkflowTests.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 Xunit;
 
namespace Infrastructure.Tests.TestTriggerMap;
 
/// <summary>
/// Guards on the CI wiring that surrounds the SelectTests engine but lives in YAML rather than C#:
/// the <c>run-full-ci</c> label kill switch (computed in <c>.github/workflows/tests.yml</c>, consumed by
/// <c>.github/actions/select-tests/action.yml</c>) and the selection-comment posting in
/// <c>tests.yml</c>. Neither is exercised by the CLI tests, yet both are easy to silently regress
/// (loosen the kill switch, or revert the comment to update-in-place), so they are pinned here.
/// </summary>
public sealed class SelectTestsWorkflowTests
{
    // The kill switch is a maintainer-only PR label, not a PR-body token. The action must consume it
    // as a plain boolean (forceAll) and must NOT re-introduce body scanning (a grep over an untrusted
    // PR description -- the injection surface this design deliberately removed).
    [Fact]
    public void SelectTestsActionGatesForceAllOnBooleanInputNotPrBody()
    {
        var action = File.ReadAllText(SelectTestsActionPath);
 
        Assert.Contains("forceAll:", action);
        Assert.Contains("FORCE_ALL: ${{ inputs.forceAll }}", action);
        Assert.Contains("[ \"$FORCE_ALL\" = \"true\" ]", action);
 
        // No body-scanning kill switch: the PR body must not flow into the action at all.
        Assert.DoesNotContain("prBody", action);
        Assert.DoesNotContain("PR_BODY", action);
        Assert.DoesNotContain("full ci", action);
    }
 
    // tests.yml must compute forceAll from the presence of the 'run-full-ci' label on the PR, read from
    // the event-payload snapshot. If the label name or the contains() expression drifts, the kill
    // switch silently stops working (it would just never force-all), so pin the exact wiring.
    [Fact]
    public void TestsWorkflowComputesForceAllFromFullCiLabel()
    {
        var testsYml = File.ReadAllText(TestsWorkflowPath);
 
        Assert.Contains(
            "forceAll: ${{ contains(github.event.pull_request.labels.*.name, 'run-full-ci') }}",
            testsYml);
    }
 
    // The comment_selection job posts one comment per pushed commit (createComment for a new commit,
    // updateComment for a re-run of the same commit) and collapses superseded comments with
    // minimizeComment -- it must never delete. This guard fails if deletion is introduced or the
    // head-commit link (also the idempotency key) is dropped.
    [Fact]
    public void CommentSelectionJobIsIdempotentPerCommitAndCollapsesSuperseded()
    {
        var job = ExtractCommentSelectionJob();
 
        Assert.Contains("github.rest.issues.createComment", job);
        Assert.Contains("github.rest.issues.updateComment", job);
        Assert.Contains("minimizeComment", job);
        Assert.DoesNotContain("deleteComment", job);
 
        // Minimization is gated on this run being the PR's live head (a stale re-run must not collapse
        // a newer commit's comment), resolved via pulls.get.
        Assert.Contains("github.rest.pulls.get", job);
 
        // The marker is read back to find prior comments; the head SHA links the commit and keys
        // create-vs-update.
        Assert.Contains("<!-- select-tests-comment -->", job);
        Assert.Contains("pull_request?.head?.sha", job);
        Assert.Contains("/commit/", job);
    }
 
    // The selector diffs head against the merge-base of base..head, so it must be handed the PR's REAL
    // head (pull_request.head.sha). github.sha is the synthetic refs/pull/N/merge commit, regenerated
    // asynchronously as the base advances; feeding it lets base-branch churn leak into the diff and
    // over-select (microsoft/aspire#18377). Pin the real-head wiring -- the caller's explicit input AND the
    // action's own default -- and forbid a revert to github.sha in either, so a future caller relying on
    // the default can't silently reintroduce the bug.
    [Fact]
    public void TestsWorkflowPassesPrHeadShaNotMergeRefToSelector()
    {
        var testsYml = File.ReadAllText(TestsWorkflowPath);
 
        Assert.Contains("headSha: ${{ github.event.pull_request.head.sha }}", testsYml);
        Assert.DoesNotContain("headSha: ${{ github.sha }}", testsYml);
 
        // The action's headSha default must also be the real head, not the synthetic merge ref.
        var action = File.ReadAllText(SelectTestsActionPath);
        Assert.Contains("default: ${{ github.event.pull_request.head.sha }}", action);
        Assert.DoesNotContain("default: ${{ github.sha }}", action);
    }
 
    // On a PR the action diffs from the merge-base of base..head, which the shallow CI checkout can't
    // see until it is deepened. The step must deepen BOTH endpoints until `git merge-base` resolves and,
    // if it never does within a bounded number of fetches, degrade to --force-all (run ALL) rather than
    // fail the PR -- a missing merge-base must not block PRs while the wiring is fixed. Pin the loop, its
    // termination bound, the warning, and the --force-all fallback so a regression can't turn it back into
    // a hard failure or an unbounded fetch.
    [Fact]
    public void SelectTestsActionDeepensUntilMergeBaseReachableThenFallsBackToAll()
    {
        var action = File.ReadAllText(SelectTestsActionPath);
 
        // The deepen loop gates on the two endpoints' merge-base actually resolving locally.
        Assert.Contains("until git merge-base \"$PR_BASE_SHA\" \"$HEAD_SHA\"", action);
        // Each iteration re-fetches both endpoints at a growing depth.
        Assert.Contains("git fetch --no-tags --depth=\"$depth\" origin \"$PR_BASE_SHA\" \"$HEAD_SHA\"", action);
        // Bounded so a pathological history can't fetch forever.
        Assert.Contains("-ge 4096", action);
        // An unresolved merge-base warns and degrades to run-all -- it must NOT be a hard ::error::/exit.
        Assert.Contains("::warning::Could not find a merge-base", action);
        Assert.DoesNotContain("::error::Could not find a merge-base", action);
 
        // The unresolved-merge-base path must DEGRADE to run-all, never hard-fail the PR. A bare
        // Contains("args+=(--force-all)") is non-discriminating: that string also appears in the
        // kill-switch and non-PR branches, so it would still pass if THIS branch were turned into an
        // `exit 1`. Scope the assertions to the merge-base resolution + fallback region -- past the
        // base-fetch guards that legitimately `exit 1` -- so a regression to a hard failure is caught.
        var resolveStart = action.IndexOf("merge_base_found=true", StringComparison.Ordinal);
        Assert.True(resolveStart >= 0, "merge-base resolution region not found in action.yml");
        // The closing `fi` of the `if [ "$merge_base_found" = "true" ]` gate, matched at its 10-space
        // indent so the loop's inner `fi` (12 spaces) and the outer branch `fi` (8 spaces) don't match.
        var resolveEnd = action.IndexOf("\n          fi", resolveStart, StringComparison.Ordinal);
        Assert.True(resolveEnd > resolveStart, "merge_base_found gate is not closed as expected");
        var mergeBaseRegion = action[resolveStart..resolveEnd];
 
        // Give-up path breaks out of the loop rather than exiting ...
        Assert.Contains("break", mergeBaseRegion);
        // ... and the gate degrades to run-all, recording the reason for the audit summary.
        Assert.Contains("--force-all --force-all-reason", mergeBaseRegion);
        // Nothing in the resolution or fallback may hard-fail the PR.
        Assert.DoesNotContain("exit", mergeBaseRegion);
    }
 
    private static string SelectTestsActionPath
        => Path.Combine(RepoRoot.Path, ".github", "actions", "select-tests", "action.yml");
 
    private static string TestsWorkflowPath
        => Path.Combine(RepoRoot.Path, ".github", "workflows", "tests.yml");
 
    private static string ExtractCommentSelectionJob()
    {
        var testsYml = File.ReadAllText(TestsWorkflowPath);
 
        var start = testsYml.IndexOf("comment_selection:", StringComparison.Ordinal);
        Assert.True(start >= 0, $"Expected a comment_selection job in {TestsWorkflowPath}.");
 
        // Bound the slice at the next top-level job so assertions can't match other jobs' scripts.
        var end = testsYml.IndexOf("build_packages:", start, StringComparison.Ordinal);
        Assert.True(end > start, $"Expected the comment_selection job to precede build_packages in {TestsWorkflowPath}.");
 
        return testsYml[start..end];
    }
}