using System; using System.Collections.Generic; using StellaOps.TaskRunner.Core.Execution; using StellaOps.TaskRunner.Core.Planning; using StellaOps.TestKit; namespace StellaOps.TaskRunner.Tests; public sealed class PackRunGateStateUpdaterTests { private static readonly DateTimeOffset RequestedAt = DateTimeOffset.UnixEpoch; private static readonly DateTimeOffset UpdateTimestamp = DateTimeOffset.UnixEpoch.AddMinutes(5); [Trait("Category", TestCategories.Unit)] [Fact] public void Apply_ApprovedGate_ClearsReasonAndSucceeds() { var plan = BuildApprovalPlan(); var graph = new PackRunExecutionGraphBuilder().Build(plan); var state = CreateInitialState(plan, graph); var coordinator = PackRunApprovalCoordinator.Create(plan, RequestedAt); coordinator.Approve("security-review", "approver-1", UpdateTimestamp); var result = PackRunGateStateUpdater.Apply(state, graph, coordinator, UpdateTimestamp); Assert.False(result.HasBlockingFailure); Assert.Equal(UpdateTimestamp, result.State.UpdatedAt); var gate = result.State.Steps["approval"]; Assert.Equal(PackRunStepExecutionStatus.Succeeded, gate.Status); Assert.Null(gate.StatusReason); Assert.Equal(UpdateTimestamp, gate.LastTransitionAt); } [Trait("Category", TestCategories.Unit)] [Fact] public void Apply_RejectedGate_FlagsFailure() { var plan = BuildApprovalPlan(); var graph = new PackRunExecutionGraphBuilder().Build(plan); var state = CreateInitialState(plan, graph); var coordinator = PackRunApprovalCoordinator.Create(plan, RequestedAt); coordinator.Reject("security-review", "approver-1", UpdateTimestamp, "not-safe"); var result = PackRunGateStateUpdater.Apply(state, graph, coordinator, UpdateTimestamp); Assert.True(result.HasBlockingFailure); Assert.Equal(UpdateTimestamp, result.State.UpdatedAt); var gate = result.State.Steps["approval"]; Assert.Equal(PackRunStepExecutionStatus.Failed, gate.Status); Assert.StartsWith("approval-rejected", gate.StatusReason, StringComparison.Ordinal); Assert.Equal(UpdateTimestamp, gate.LastTransitionAt); } [Trait("Category", TestCategories.Unit)] [Fact] public void Apply_PolicyGate_ClearsPendingReason() { var plan = BuildPolicyPlan(); var graph = new PackRunExecutionGraphBuilder().Build(plan); var state = CreateInitialState(plan, graph); var coordinator = PackRunApprovalCoordinator.Create(plan, RequestedAt); var result = PackRunGateStateUpdater.Apply(state, graph, coordinator, UpdateTimestamp); Assert.False(result.HasBlockingFailure); var gate = result.State.Steps["policy-check"]; Assert.Equal(PackRunStepExecutionStatus.Succeeded, gate.Status); Assert.Null(gate.StatusReason); Assert.Equal(UpdateTimestamp, gate.LastTransitionAt); var prepare = result.State.Steps["prepare"]; Assert.Equal(PackRunStepExecutionStatus.Pending, prepare.Status); Assert.Null(prepare.StatusReason); } private static TaskPackPlan BuildApprovalPlan() { var manifest = TestManifests.Load(TestManifests.Sample); var planner = new TaskPackPlanner(); var inputs = new Dictionary { ["dryRun"] = System.Text.Json.Nodes.JsonValue.Create(false) }; return planner.Plan(manifest, inputs).Plan!; } private static TaskPackPlan BuildPolicyPlan() { var manifest = TestManifests.Load(TestManifests.PolicyGate); var planner = new TaskPackPlanner(); return planner.Plan(manifest).Plan!; } private static PackRunState CreateInitialState(TaskPackPlan plan, PackRunExecutionGraph graph) { var steps = new Dictionary(StringComparer.Ordinal); foreach (var step in EnumerateSteps(graph.Steps)) { var status = PackRunStepExecutionStatus.Pending; string? reason = null; if (!step.Enabled) { status = PackRunStepExecutionStatus.Skipped; reason = "disabled"; } else if (step.Kind == PackRunStepKind.GateApproval) { reason = "requires-approval"; } else if (step.Kind == PackRunStepKind.GatePolicy) { reason = "requires-policy"; } steps[step.Id] = new PackRunStepStateRecord( step.Id, step.Kind, step.Enabled, step.ContinueOnError, step.MaxParallel, step.ApprovalId, step.GateMessage, status, Attempts: 0, LastTransitionAt: null, NextAttemptAt: null, StatusReason: reason); } return PackRunState.Create("run-1", plan.Hash, plan, graph.FailurePolicy, RequestedAt, steps, RequestedAt); } private static IEnumerable EnumerateSteps(IReadOnlyList steps) { foreach (var step in steps) { yield return step; if (step.Children.Count > 0) { foreach (var child in EnumerateSteps(step.Children)) { yield return child; } } } } }