up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,66 +1,66 @@
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
using StellaOps.TaskRunner.Infrastructure.Execution;
namespace StellaOps.TaskRunner.Tests;
public sealed class FilePackRunStateStoreTests
{
[Fact]
public async Task SaveAndGetAsync_RoundTripsState()
{
var directory = CreateTempDirectory();
try
{
var store = new FilePackRunStateStore(directory);
var original = CreateState("run:primary");
await store.SaveAsync(original, CancellationToken.None);
var reloaded = await store.GetAsync("run:primary", CancellationToken.None);
Assert.NotNull(reloaded);
Assert.Equal(original.RunId, reloaded!.RunId);
Assert.Equal(original.PlanHash, reloaded.PlanHash);
Assert.Equal(original.FailurePolicy, reloaded.FailurePolicy);
Assert.Equal(original.Steps.Count, reloaded.Steps.Count);
var step = Assert.Single(reloaded.Steps);
Assert.Equal("step-a", step.Key);
Assert.Equal(original.Steps["step-a"], step.Value);
}
finally
{
TryDelete(directory);
}
}
[Fact]
public async Task ListAsync_ReturnsStatesInDeterministicOrder()
{
var directory = CreateTempDirectory();
try
{
var store = new FilePackRunStateStore(directory);
var stateB = CreateState("run-b");
var stateA = CreateState("run-a");
await store.SaveAsync(stateB, CancellationToken.None);
await store.SaveAsync(stateA, CancellationToken.None);
var states = await store.ListAsync(CancellationToken.None);
Assert.Collection(states,
first => Assert.Equal("run-a", first.RunId),
second => Assert.Equal("run-b", second.RunId));
}
finally
{
TryDelete(directory);
}
}
private static PackRunState CreateState(string runId)
{
using StellaOps.TaskRunner.Core.Planning;
using StellaOps.TaskRunner.Infrastructure.Execution;
namespace StellaOps.TaskRunner.Tests;
public sealed class FilePackRunStateStoreTests
{
[Fact]
public async Task SaveAndGetAsync_RoundTripsState()
{
var directory = CreateTempDirectory();
try
{
var store = new FilePackRunStateStore(directory);
var original = CreateState("run:primary");
await store.SaveAsync(original, CancellationToken.None);
var reloaded = await store.GetAsync("run:primary", CancellationToken.None);
Assert.NotNull(reloaded);
Assert.Equal(original.RunId, reloaded!.RunId);
Assert.Equal(original.PlanHash, reloaded.PlanHash);
Assert.Equal(original.FailurePolicy, reloaded.FailurePolicy);
Assert.Equal(original.Steps.Count, reloaded.Steps.Count);
var step = Assert.Single(reloaded.Steps);
Assert.Equal("step-a", step.Key);
Assert.Equal(original.Steps["step-a"], step.Value);
}
finally
{
TryDelete(directory);
}
}
[Fact]
public async Task ListAsync_ReturnsStatesInDeterministicOrder()
{
var directory = CreateTempDirectory();
try
{
var store = new FilePackRunStateStore(directory);
var stateB = CreateState("run-b");
var stateA = CreateState("run-a");
await store.SaveAsync(stateB, CancellationToken.None);
await store.SaveAsync(stateA, CancellationToken.None);
var states = await store.ListAsync(CancellationToken.None);
Assert.Collection(states,
first => Assert.Equal("run-a", first.RunId),
second => Assert.Equal("run-b", second.RunId));
}
finally
{
TryDelete(directory);
}
}
private static PackRunState CreateState(string runId)
{
var failurePolicy = new TaskPackPlanFailurePolicy(MaxAttempts: 3, BackoffSeconds: 30, ContinueOnError: false);
var metadata = new TaskPackPlanMetadata("sample", "1.0.0", null, Array.Empty<string>());
var parameters = new Dictionary<string, TaskPackPlanParameterValue>(StringComparer.Ordinal);
@@ -89,41 +89,41 @@ public sealed class FilePackRunStateStoreTests
["step-a"] = new PackRunStepStateRecord(
StepId: "step-a",
Kind: PackRunStepKind.Run,
Enabled: true,
ContinueOnError: false,
MaxParallel: null,
ApprovalId: null,
GateMessage: null,
Status: PackRunStepExecutionStatus.Pending,
Attempts: 1,
LastTransitionAt: DateTimeOffset.UtcNow,
NextAttemptAt: null,
Enabled: true,
ContinueOnError: false,
MaxParallel: null,
ApprovalId: null,
GateMessage: null,
Status: PackRunStepExecutionStatus.Pending,
Attempts: 1,
LastTransitionAt: DateTimeOffset.UtcNow,
NextAttemptAt: null,
StatusReason: null)
};
var timestamp = DateTimeOffset.UtcNow;
return PackRunState.Create(runId, "hash-123", plan, failurePolicy, timestamp, steps, timestamp);
}
private static string CreateTempDirectory()
{
var path = Path.Combine(Path.GetTempPath(), "stellaops-taskrunner-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
private static void TryDelete(string directory)
{
try
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
catch
{
// Swallow cleanup errors to avoid masking test assertions.
}
}
}
}
private static string CreateTempDirectory()
{
var path = Path.Combine(Path.GetTempPath(), "stellaops-taskrunner-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
private static void TryDelete(string directory)
{
try
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
catch
{
// Swallow cleanup errors to avoid masking test assertions.
}
}
}

View File

@@ -1,95 +1,95 @@
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunApprovalCoordinatorTests
{
[Fact]
public void Create_FromPlan_PopulatesApprovals()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var approvals = coordinator.GetApprovals();
Assert.Single(approvals);
Assert.Equal("security-review", approvals[0].ApprovalId);
Assert.Equal(PackRunApprovalStatus.Pending, approvals[0].Status);
}
[Fact]
public void Approve_AllowsResumeWhenLastApprovalCompletes()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var result = coordinator.Approve("security-review", "approver-1", DateTimeOffset.UtcNow);
Assert.True(result.ShouldResumeRun);
Assert.Equal(PackRunApprovalStatus.Approved, result.State.Status);
Assert.Equal("approver-1", result.State.ActorId);
}
[Fact]
public void Reject_DoesNotResumeAndMarksState()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var result = coordinator.Reject("security-review", "approver-1", DateTimeOffset.UtcNow, "Not safe");
Assert.False(result.ShouldResumeRun);
Assert.Equal(PackRunApprovalStatus.Rejected, result.State.Status);
Assert.Equal("Not safe", result.State.Summary);
}
[Fact]
public void BuildNotifications_UsesRequirements()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var notifications = coordinator.BuildNotifications(plan);
Assert.Single(notifications);
var notification = notifications[0];
Assert.Equal("security-review", notification.ApprovalId);
Assert.Contains("Packs.Approve", notification.RequiredGrants);
}
[Fact]
public void BuildPolicyNotifications_ProducesGateMetadata()
{
var plan = BuildPolicyPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var notifications = coordinator.BuildPolicyNotifications(plan);
Assert.Single(notifications);
var hint = notifications[0];
Assert.Equal("policy-check", hint.StepId);
var parameter = hint.Parameters.Single(p => p.Name == "threshold");
Assert.False(parameter.RequiresRuntimeValue);
var runtimeParam = hint.Parameters.Single(p => p.Name == "evidenceRef");
Assert.True(runtimeParam.RequiresRuntimeValue);
Assert.Equal("steps.prepare.outputs.evidence", runtimeParam.Expression);
}
private static TaskPackPlan BuildPlan()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = 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!;
}
}
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunApprovalCoordinatorTests
{
[Fact]
public void Create_FromPlan_PopulatesApprovals()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var approvals = coordinator.GetApprovals();
Assert.Single(approvals);
Assert.Equal("security-review", approvals[0].ApprovalId);
Assert.Equal(PackRunApprovalStatus.Pending, approvals[0].Status);
}
[Fact]
public void Approve_AllowsResumeWhenLastApprovalCompletes()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var result = coordinator.Approve("security-review", "approver-1", DateTimeOffset.UtcNow);
Assert.True(result.ShouldResumeRun);
Assert.Equal(PackRunApprovalStatus.Approved, result.State.Status);
Assert.Equal("approver-1", result.State.ActorId);
}
[Fact]
public void Reject_DoesNotResumeAndMarksState()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var result = coordinator.Reject("security-review", "approver-1", DateTimeOffset.UtcNow, "Not safe");
Assert.False(result.ShouldResumeRun);
Assert.Equal(PackRunApprovalStatus.Rejected, result.State.Status);
Assert.Equal("Not safe", result.State.Summary);
}
[Fact]
public void BuildNotifications_UsesRequirements()
{
var plan = BuildPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var notifications = coordinator.BuildNotifications(plan);
Assert.Single(notifications);
var notification = notifications[0];
Assert.Equal("security-review", notification.ApprovalId);
Assert.Contains("Packs.Approve", notification.RequiredGrants);
}
[Fact]
public void BuildPolicyNotifications_ProducesGateMetadata()
{
var plan = BuildPolicyPlan();
var coordinator = PackRunApprovalCoordinator.Create(plan, DateTimeOffset.UtcNow);
var notifications = coordinator.BuildPolicyNotifications(plan);
Assert.Single(notifications);
var hint = notifications[0];
Assert.Equal("policy-check", hint.StepId);
var parameter = hint.Parameters.Single(p => p.Name == "threshold");
Assert.False(parameter.RequiresRuntimeValue);
var runtimeParam = hint.Parameters.Single(p => p.Name == "evidenceRef");
Assert.True(runtimeParam.RequiresRuntimeValue);
Assert.Equal("steps.prepare.outputs.evidence", runtimeParam.Expression);
}
private static TaskPackPlan BuildPlan()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = 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!;
}
}

View File

@@ -1,68 +1,68 @@
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunExecutionGraphBuilderTests
{
[Fact]
public void Build_GeneratesParallelMetadata()
{
var manifest = TestManifests.Load(TestManifests.Parallel);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
var builder = new PackRunExecutionGraphBuilder();
var graph = builder.Build(plan);
Assert.Equal(2, graph.FailurePolicy.MaxAttempts);
Assert.Equal(10, graph.FailurePolicy.BackoffSeconds);
var parallel = Assert.Single(graph.Steps);
Assert.Equal(PackRunStepKind.Parallel, parallel.Kind);
Assert.True(parallel.Enabled);
Assert.Equal(2, parallel.MaxParallel);
Assert.True(parallel.ContinueOnError);
Assert.Equal(2, parallel.Children.Count);
Assert.All(parallel.Children, child => Assert.Equal(PackRunStepKind.Run, child.Kind));
}
[Fact]
public void Build_PreservesMapIterationsAndDisabledSteps()
{
var planner = new TaskPackPlanner();
var builder = new PackRunExecutionGraphBuilder();
// Map iterations
var mapManifest = TestManifests.Load(TestManifests.Map);
var inputs = new Dictionary<string, JsonNode?>
{
["targets"] = new JsonArray("alpha", "beta", "gamma")
};
var mapPlan = planner.Plan(mapManifest, inputs).Plan!;
var mapGraph = builder.Build(mapPlan);
var mapStep = Assert.Single(mapGraph.Steps);
Assert.Equal(PackRunStepKind.Map, mapStep.Kind);
Assert.Equal(3, mapStep.Children.Count);
Assert.All(mapStep.Children, child => Assert.Equal(PackRunStepKind.Run, child.Kind));
// Disabled conditional step
var conditionalManifest = TestManifests.Load(TestManifests.Sample);
var conditionalInputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(true)
};
var conditionalPlan = planner.Plan(conditionalManifest, conditionalInputs).Plan!;
var conditionalGraph = builder.Build(conditionalPlan);
var applyStep = conditionalGraph.Steps.Single(step => step.Id == "apply-step");
Assert.False(applyStep.Enabled);
}
}
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunExecutionGraphBuilderTests
{
[Fact]
public void Build_GeneratesParallelMetadata()
{
var manifest = TestManifests.Load(TestManifests.Parallel);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
var builder = new PackRunExecutionGraphBuilder();
var graph = builder.Build(plan);
Assert.Equal(2, graph.FailurePolicy.MaxAttempts);
Assert.Equal(10, graph.FailurePolicy.BackoffSeconds);
var parallel = Assert.Single(graph.Steps);
Assert.Equal(PackRunStepKind.Parallel, parallel.Kind);
Assert.True(parallel.Enabled);
Assert.Equal(2, parallel.MaxParallel);
Assert.True(parallel.ContinueOnError);
Assert.Equal(2, parallel.Children.Count);
Assert.All(parallel.Children, child => Assert.Equal(PackRunStepKind.Run, child.Kind));
}
[Fact]
public void Build_PreservesMapIterationsAndDisabledSteps()
{
var planner = new TaskPackPlanner();
var builder = new PackRunExecutionGraphBuilder();
// Map iterations
var mapManifest = TestManifests.Load(TestManifests.Map);
var inputs = new Dictionary<string, JsonNode?>
{
["targets"] = new JsonArray("alpha", "beta", "gamma")
};
var mapPlan = planner.Plan(mapManifest, inputs).Plan!;
var mapGraph = builder.Build(mapPlan);
var mapStep = Assert.Single(mapGraph.Steps);
Assert.Equal(PackRunStepKind.Map, mapStep.Kind);
Assert.Equal(3, mapStep.Children.Count);
Assert.All(mapStep.Children, child => Assert.Equal(PackRunStepKind.Run, child.Kind));
// Disabled conditional step
var conditionalManifest = TestManifests.Load(TestManifests.Sample);
var conditionalInputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(true)
};
var conditionalPlan = planner.Plan(conditionalManifest, conditionalInputs).Plan!;
var conditionalGraph = builder.Build(conditionalPlan);
var applyStep = conditionalGraph.Steps.Single(step => step.Id == "apply-step");
Assert.False(applyStep.Enabled);
}
}

View File

@@ -1,150 +1,150 @@
using System;
using System.Collections.Generic;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunGateStateUpdaterTests
{
private static readonly DateTimeOffset RequestedAt = DateTimeOffset.UnixEpoch;
private static readonly DateTimeOffset UpdateTimestamp = DateTimeOffset.UnixEpoch.AddMinutes(5);
[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);
}
[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);
}
[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<string, System.Text.Json.Nodes.JsonNode?>
{
["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<string, PackRunStepStateRecord>(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);
}
using System;
using System.Collections.Generic;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunGateStateUpdaterTests
{
private static readonly DateTimeOffset RequestedAt = DateTimeOffset.UnixEpoch;
private static readonly DateTimeOffset UpdateTimestamp = DateTimeOffset.UnixEpoch.AddMinutes(5);
[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);
}
[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);
}
[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<string, System.Text.Json.Nodes.JsonNode?>
{
["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<string, PackRunStepStateRecord>(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<PackRunExecutionStep> EnumerateSteps(IReadOnlyList<PackRunExecutionStep> steps)
{
foreach (var step in steps)
{
yield return step;
if (step.Children.Count > 0)
{
foreach (var child in EnumerateSteps(step.Children))
{
yield return child;
}
}
}
}
}
}
private static IEnumerable<PackRunExecutionStep> EnumerateSteps(IReadOnlyList<PackRunExecutionStep> steps)
{
foreach (var step in steps)
{
yield return step;
if (step.Children.Count > 0)
{
foreach (var child in EnumerateSteps(step.Children))
{
yield return child;
}
}
}
}
}

View File

@@ -1,85 +1,85 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
using System.Text.Json.Nodes;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunProcessorTests
{
[Fact]
public async Task ProcessNewRunAsync_PersistsApprovalsAndPublishesNotifications()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest, new Dictionary<string, JsonNode?> { ["dryRun"] = JsonValue.Create(false) }).Plan!;
var context = new PackRunExecutionContext("run-123", plan, DateTimeOffset.UtcNow);
var store = new TestApprovalStore();
var publisher = new TestNotificationPublisher();
var processor = new PackRunProcessor(store, publisher, NullLogger<PackRunProcessor>.Instance);
var result = await processor.ProcessNewRunAsync(context, CancellationToken.None);
Assert.False(result.ShouldResumeImmediately);
var saved = Assert.Single(store.Saved);
Assert.Equal("security-review", saved.ApprovalId);
Assert.Single(publisher.Approvals);
Assert.Empty(publisher.Policies);
}
[Fact]
public async Task ProcessNewRunAsync_NoApprovals_ResumesImmediately()
{
var manifest = TestManifests.Load(TestManifests.Output);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var context = new PackRunExecutionContext("run-456", plan, DateTimeOffset.UtcNow);
var store = new TestApprovalStore();
var publisher = new TestNotificationPublisher();
var processor = new PackRunProcessor(store, publisher, NullLogger<PackRunProcessor>.Instance);
var result = await processor.ProcessNewRunAsync(context, CancellationToken.None);
Assert.True(result.ShouldResumeImmediately);
Assert.Empty(store.Saved);
Assert.Empty(publisher.Approvals);
}
private sealed class TestApprovalStore : IPackRunApprovalStore
{
public List<PackRunApprovalState> Saved { get; } = new();
public Task<IReadOnlyList<PackRunApprovalState>> GetAsync(string runId, CancellationToken cancellationToken)
=> Task.FromResult((IReadOnlyList<PackRunApprovalState>)Saved);
public Task SaveAsync(string runId, IReadOnlyList<PackRunApprovalState> approvals, CancellationToken cancellationToken)
{
Saved.Clear();
Saved.AddRange(approvals);
return Task.CompletedTask;
}
public Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class TestNotificationPublisher : IPackRunNotificationPublisher
{
public List<ApprovalNotification> Approvals { get; } = new();
public List<PolicyGateNotification> Policies { get; } = new();
public Task PublishApprovalRequestedAsync(string runId, ApprovalNotification notification, CancellationToken cancellationToken)
{
Approvals.Add(notification);
return Task.CompletedTask;
}
public Task PublishPolicyGatePendingAsync(string runId, PolicyGateNotification notification, CancellationToken cancellationToken)
{
Policies.Add(notification);
return Task.CompletedTask;
}
}
}
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
using System.Text.Json.Nodes;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunProcessorTests
{
[Fact]
public async Task ProcessNewRunAsync_PersistsApprovalsAndPublishesNotifications()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest, new Dictionary<string, JsonNode?> { ["dryRun"] = JsonValue.Create(false) }).Plan!;
var context = new PackRunExecutionContext("run-123", plan, DateTimeOffset.UtcNow);
var store = new TestApprovalStore();
var publisher = new TestNotificationPublisher();
var processor = new PackRunProcessor(store, publisher, NullLogger<PackRunProcessor>.Instance);
var result = await processor.ProcessNewRunAsync(context, CancellationToken.None);
Assert.False(result.ShouldResumeImmediately);
var saved = Assert.Single(store.Saved);
Assert.Equal("security-review", saved.ApprovalId);
Assert.Single(publisher.Approvals);
Assert.Empty(publisher.Policies);
}
[Fact]
public async Task ProcessNewRunAsync_NoApprovals_ResumesImmediately()
{
var manifest = TestManifests.Load(TestManifests.Output);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var context = new PackRunExecutionContext("run-456", plan, DateTimeOffset.UtcNow);
var store = new TestApprovalStore();
var publisher = new TestNotificationPublisher();
var processor = new PackRunProcessor(store, publisher, NullLogger<PackRunProcessor>.Instance);
var result = await processor.ProcessNewRunAsync(context, CancellationToken.None);
Assert.True(result.ShouldResumeImmediately);
Assert.Empty(store.Saved);
Assert.Empty(publisher.Approvals);
}
private sealed class TestApprovalStore : IPackRunApprovalStore
{
public List<PackRunApprovalState> Saved { get; } = new();
public Task<IReadOnlyList<PackRunApprovalState>> GetAsync(string runId, CancellationToken cancellationToken)
=> Task.FromResult((IReadOnlyList<PackRunApprovalState>)Saved);
public Task SaveAsync(string runId, IReadOnlyList<PackRunApprovalState> approvals, CancellationToken cancellationToken)
{
Saved.Clear();
Saved.AddRange(approvals);
return Task.CompletedTask;
}
public Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class TestNotificationPublisher : IPackRunNotificationPublisher
{
public List<ApprovalNotification> Approvals { get; } = new();
public List<PolicyGateNotification> Policies { get; } = new();
public Task PublishApprovalRequestedAsync(string runId, ApprovalNotification notification, CancellationToken cancellationToken)
{
Approvals.Add(notification);
return Task.CompletedTask;
}
public Task PublishPolicyGatePendingAsync(string runId, PolicyGateNotification notification, CancellationToken cancellationToken)
{
Policies.Add(notification);
return Task.CompletedTask;
}
}
}

View File

@@ -1,142 +1,142 @@
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Execution.Simulation;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunSimulationEngineTests
{
[Fact]
public void Simulate_IdentifiesGateStatuses()
{
var manifest = TestManifests.Load(TestManifests.PolicyGate);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var gate = result.Steps.Single(step => step.Kind == PackRunStepKind.GatePolicy);
Assert.Equal(PackRunSimulationStatus.RequiresPolicy, gate.Status);
var run = result.Steps.Single(step => step.Kind == PackRunStepKind.Run);
Assert.Equal(PackRunSimulationStatus.Pending, run.Status);
}
[Fact]
public void Simulate_MarksDisabledStepsAndOutputs()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(true)
};
var plan = planner.Plan(manifest, inputs).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var applyStep = result.Steps.Single(step => step.Id == "apply-step");
Assert.Equal(PackRunSimulationStatus.Skipped, applyStep.Status);
Assert.Empty(result.Outputs);
Assert.Equal(PackRunExecutionGraph.DefaultFailurePolicy.MaxAttempts, result.FailurePolicy.MaxAttempts);
Assert.Equal(PackRunExecutionGraph.DefaultFailurePolicy.BackoffSeconds, result.FailurePolicy.BackoffSeconds);
}
[Fact]
public void Simulate_ProjectsOutputsAndRuntimeFlags()
{
var manifest = TestManifests.Load(TestManifests.Output);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var step = Assert.Single(result.Steps);
Assert.Equal(PackRunStepKind.Run, step.Kind);
Assert.Collection(result.Outputs,
bundle =>
{
Assert.Equal("bundlePath", bundle.Name);
Assert.False(bundle.RequiresRuntimeValue);
},
evidence =>
{
Assert.Equal("evidenceModel", evidence.Name);
Assert.True(evidence.RequiresRuntimeValue);
});
}
[Fact]
public void Simulate_LoopStep_SetsWillIterateStatus()
{
var manifest = TestManifests.Load(TestManifests.Loop);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["targets"] = new JsonArray { "a", "b", "c" }
};
var result = planner.Plan(manifest, inputs);
Assert.Empty(result.Errors);
Assert.NotNull(result.Plan);
var engine = new PackRunSimulationEngine();
var simResult = engine.Simulate(result.Plan);
var loopStep = simResult.Steps.Single(s => s.Kind == PackRunStepKind.Loop);
Assert.Equal(PackRunSimulationStatus.WillIterate, loopStep.Status);
Assert.Equal("process-loop", loopStep.Id);
Assert.NotNull(loopStep.LoopInfo);
Assert.Equal("target", loopStep.LoopInfo.Iterator);
Assert.Equal("idx", loopStep.LoopInfo.Index);
Assert.Equal(100, loopStep.LoopInfo.MaxIterations);
Assert.Equal("collect", loopStep.LoopInfo.AggregationMode);
}
[Fact]
public void Simulate_ConditionalStep_SetsWillBranchStatus()
{
var manifest = TestManifests.Load(TestManifests.Conditional);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["environment"] = JsonValue.Create("production")
};
var result = planner.Plan(manifest, inputs);
Assert.Empty(result.Errors);
Assert.NotNull(result.Plan);
var engine = new PackRunSimulationEngine();
var simResult = engine.Simulate(result.Plan);
var conditionalStep = simResult.Steps.Single(s => s.Kind == PackRunStepKind.Conditional);
Assert.Equal(PackRunSimulationStatus.WillBranch, conditionalStep.Status);
Assert.Equal("env-branch", conditionalStep.Id);
Assert.NotNull(conditionalStep.ConditionalInfo);
Assert.Equal(2, conditionalStep.ConditionalInfo.Branches.Count);
Assert.True(conditionalStep.ConditionalInfo.OutputUnion);
}
[Fact]
public void Simulate_PolicyGateStep_HasPolicyInfo()
{
var manifest = TestManifests.Load(TestManifests.PolicyGate);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var policyStep = result.Steps.Single(s => s.Kind == PackRunStepKind.GatePolicy);
Assert.Equal(PackRunSimulationStatus.RequiresPolicy, policyStep.Status);
Assert.NotNull(policyStep.PolicyInfo);
Assert.Equal("security-hold", policyStep.PolicyInfo.PolicyId);
Assert.Equal("abort", policyStep.PolicyInfo.FailureAction);
}
}
using System.Text.Json.Nodes;
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Execution.Simulation;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunSimulationEngineTests
{
[Fact]
public void Simulate_IdentifiesGateStatuses()
{
var manifest = TestManifests.Load(TestManifests.PolicyGate);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var gate = result.Steps.Single(step => step.Kind == PackRunStepKind.GatePolicy);
Assert.Equal(PackRunSimulationStatus.RequiresPolicy, gate.Status);
var run = result.Steps.Single(step => step.Kind == PackRunStepKind.Run);
Assert.Equal(PackRunSimulationStatus.Pending, run.Status);
}
[Fact]
public void Simulate_MarksDisabledStepsAndOutputs()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(true)
};
var plan = planner.Plan(manifest, inputs).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var applyStep = result.Steps.Single(step => step.Id == "apply-step");
Assert.Equal(PackRunSimulationStatus.Skipped, applyStep.Status);
Assert.Empty(result.Outputs);
Assert.Equal(PackRunExecutionGraph.DefaultFailurePolicy.MaxAttempts, result.FailurePolicy.MaxAttempts);
Assert.Equal(PackRunExecutionGraph.DefaultFailurePolicy.BackoffSeconds, result.FailurePolicy.BackoffSeconds);
}
[Fact]
public void Simulate_ProjectsOutputsAndRuntimeFlags()
{
var manifest = TestManifests.Load(TestManifests.Output);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var step = Assert.Single(result.Steps);
Assert.Equal(PackRunStepKind.Run, step.Kind);
Assert.Collection(result.Outputs,
bundle =>
{
Assert.Equal("bundlePath", bundle.Name);
Assert.False(bundle.RequiresRuntimeValue);
},
evidence =>
{
Assert.Equal("evidenceModel", evidence.Name);
Assert.True(evidence.RequiresRuntimeValue);
});
}
[Fact]
public void Simulate_LoopStep_SetsWillIterateStatus()
{
var manifest = TestManifests.Load(TestManifests.Loop);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["targets"] = new JsonArray { "a", "b", "c" }
};
var result = planner.Plan(manifest, inputs);
Assert.Empty(result.Errors);
Assert.NotNull(result.Plan);
var engine = new PackRunSimulationEngine();
var simResult = engine.Simulate(result.Plan);
var loopStep = simResult.Steps.Single(s => s.Kind == PackRunStepKind.Loop);
Assert.Equal(PackRunSimulationStatus.WillIterate, loopStep.Status);
Assert.Equal("process-loop", loopStep.Id);
Assert.NotNull(loopStep.LoopInfo);
Assert.Equal("target", loopStep.LoopInfo.Iterator);
Assert.Equal("idx", loopStep.LoopInfo.Index);
Assert.Equal(100, loopStep.LoopInfo.MaxIterations);
Assert.Equal("collect", loopStep.LoopInfo.AggregationMode);
}
[Fact]
public void Simulate_ConditionalStep_SetsWillBranchStatus()
{
var manifest = TestManifests.Load(TestManifests.Conditional);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["environment"] = JsonValue.Create("production")
};
var result = planner.Plan(manifest, inputs);
Assert.Empty(result.Errors);
Assert.NotNull(result.Plan);
var engine = new PackRunSimulationEngine();
var simResult = engine.Simulate(result.Plan);
var conditionalStep = simResult.Steps.Single(s => s.Kind == PackRunStepKind.Conditional);
Assert.Equal(PackRunSimulationStatus.WillBranch, conditionalStep.Status);
Assert.Equal("env-branch", conditionalStep.Id);
Assert.NotNull(conditionalStep.ConditionalInfo);
Assert.Equal(2, conditionalStep.ConditionalInfo.Branches.Count);
Assert.True(conditionalStep.ConditionalInfo.OutputUnion);
}
[Fact]
public void Simulate_PolicyGateStep_HasPolicyInfo()
{
var manifest = TestManifests.Load(TestManifests.PolicyGate);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var engine = new PackRunSimulationEngine();
var result = engine.Simulate(plan);
var policyStep = result.Steps.Single(s => s.Kind == PackRunStepKind.GatePolicy);
Assert.Equal(PackRunSimulationStatus.RequiresPolicy, policyStep.Status);
Assert.NotNull(policyStep.PolicyInfo);
Assert.Equal("security-hold", policyStep.PolicyInfo.PolicyId);
Assert.Equal("abort", policyStep.PolicyInfo.FailureAction);
}
}

View File

@@ -1,66 +1,66 @@
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunStepStateMachineTests
{
private static readonly TaskPackPlanFailurePolicy RetryTwicePolicy = new(MaxAttempts: 3, BackoffSeconds: 5, ContinueOnError: false);
[Fact]
public void Start_FromPending_SetsRunning()
{
var state = PackRunStepStateMachine.Create();
var started = PackRunStepStateMachine.Start(state, DateTimeOffset.UnixEpoch);
Assert.Equal(PackRunStepExecutionStatus.Running, started.Status);
Assert.Equal(0, started.Attempts);
}
[Fact]
public void CompleteSuccess_IncrementsAttempts()
{
var state = PackRunStepStateMachine.Create();
var running = PackRunStepStateMachine.Start(state, DateTimeOffset.UnixEpoch);
var completed = PackRunStepStateMachine.CompleteSuccess(running, DateTimeOffset.UnixEpoch.AddSeconds(1));
Assert.Equal(PackRunStepExecutionStatus.Succeeded, completed.Status);
Assert.Equal(1, completed.Attempts);
Assert.Null(completed.NextAttemptAt);
}
[Fact]
public void RegisterFailure_SchedulesRetryUntilMaxAttempts()
{
var state = PackRunStepStateMachine.Create();
var running = PackRunStepStateMachine.Start(state, DateTimeOffset.UnixEpoch);
var firstFailure = PackRunStepStateMachine.RegisterFailure(running, DateTimeOffset.UnixEpoch.AddSeconds(2), RetryTwicePolicy);
Assert.Equal(PackRunStepFailureOutcome.Retry, firstFailure.Outcome);
Assert.Equal(PackRunStepExecutionStatus.Pending, firstFailure.State.Status);
Assert.Equal(1, firstFailure.State.Attempts);
Assert.Equal(DateTimeOffset.UnixEpoch.AddSeconds(7), firstFailure.State.NextAttemptAt);
var restarted = PackRunStepStateMachine.Start(firstFailure.State, DateTimeOffset.UnixEpoch.AddSeconds(7));
var secondFailure = PackRunStepStateMachine.RegisterFailure(restarted, DateTimeOffset.UnixEpoch.AddSeconds(9), RetryTwicePolicy);
Assert.Equal(PackRunStepFailureOutcome.Retry, secondFailure.Outcome);
Assert.Equal(2, secondFailure.State.Attempts);
var finalStart = PackRunStepStateMachine.Start(secondFailure.State, DateTimeOffset.UnixEpoch.AddSeconds(9 + RetryTwicePolicy.BackoffSeconds));
var terminalFailure = PackRunStepStateMachine.RegisterFailure(finalStart, DateTimeOffset.UnixEpoch.AddSeconds(20), RetryTwicePolicy);
Assert.Equal(PackRunStepFailureOutcome.Abort, terminalFailure.Outcome);
Assert.Equal(PackRunStepExecutionStatus.Failed, terminalFailure.State.Status);
Assert.Equal(3, terminalFailure.State.Attempts);
Assert.Null(terminalFailure.State.NextAttemptAt);
}
[Fact]
public void Skip_FromPending_SetsSkipped()
{
var state = PackRunStepStateMachine.Create();
var skipped = PackRunStepStateMachine.Skip(state, DateTimeOffset.UnixEpoch.AddHours(1));
Assert.Equal(PackRunStepExecutionStatus.Skipped, skipped.Status);
Assert.Equal(0, skipped.Attempts);
}
}
using StellaOps.TaskRunner.Core.Execution;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class PackRunStepStateMachineTests
{
private static readonly TaskPackPlanFailurePolicy RetryTwicePolicy = new(MaxAttempts: 3, BackoffSeconds: 5, ContinueOnError: false);
[Fact]
public void Start_FromPending_SetsRunning()
{
var state = PackRunStepStateMachine.Create();
var started = PackRunStepStateMachine.Start(state, DateTimeOffset.UnixEpoch);
Assert.Equal(PackRunStepExecutionStatus.Running, started.Status);
Assert.Equal(0, started.Attempts);
}
[Fact]
public void CompleteSuccess_IncrementsAttempts()
{
var state = PackRunStepStateMachine.Create();
var running = PackRunStepStateMachine.Start(state, DateTimeOffset.UnixEpoch);
var completed = PackRunStepStateMachine.CompleteSuccess(running, DateTimeOffset.UnixEpoch.AddSeconds(1));
Assert.Equal(PackRunStepExecutionStatus.Succeeded, completed.Status);
Assert.Equal(1, completed.Attempts);
Assert.Null(completed.NextAttemptAt);
}
[Fact]
public void RegisterFailure_SchedulesRetryUntilMaxAttempts()
{
var state = PackRunStepStateMachine.Create();
var running = PackRunStepStateMachine.Start(state, DateTimeOffset.UnixEpoch);
var firstFailure = PackRunStepStateMachine.RegisterFailure(running, DateTimeOffset.UnixEpoch.AddSeconds(2), RetryTwicePolicy);
Assert.Equal(PackRunStepFailureOutcome.Retry, firstFailure.Outcome);
Assert.Equal(PackRunStepExecutionStatus.Pending, firstFailure.State.Status);
Assert.Equal(1, firstFailure.State.Attempts);
Assert.Equal(DateTimeOffset.UnixEpoch.AddSeconds(7), firstFailure.State.NextAttemptAt);
var restarted = PackRunStepStateMachine.Start(firstFailure.State, DateTimeOffset.UnixEpoch.AddSeconds(7));
var secondFailure = PackRunStepStateMachine.RegisterFailure(restarted, DateTimeOffset.UnixEpoch.AddSeconds(9), RetryTwicePolicy);
Assert.Equal(PackRunStepFailureOutcome.Retry, secondFailure.Outcome);
Assert.Equal(2, secondFailure.State.Attempts);
var finalStart = PackRunStepStateMachine.Start(secondFailure.State, DateTimeOffset.UnixEpoch.AddSeconds(9 + RetryTwicePolicy.BackoffSeconds));
var terminalFailure = PackRunStepStateMachine.RegisterFailure(finalStart, DateTimeOffset.UnixEpoch.AddSeconds(20), RetryTwicePolicy);
Assert.Equal(PackRunStepFailureOutcome.Abort, terminalFailure.Outcome);
Assert.Equal(PackRunStepExecutionStatus.Failed, terminalFailure.State.Status);
Assert.Equal(3, terminalFailure.State.Attempts);
Assert.Null(terminalFailure.State.NextAttemptAt);
}
[Fact]
public void Skip_FromPending_SetsSkipped()
{
var state = PackRunStepStateMachine.Create();
var skipped = PackRunStepStateMachine.Skip(state, DateTimeOffset.UnixEpoch.AddHours(1));
Assert.Equal(PackRunStepExecutionStatus.Skipped, skipped.Status);
Assert.Equal(0, skipped.Attempts);
}
}

View File

@@ -3,39 +3,39 @@ using System.Linq;
using System.Text.Json.Nodes;
using StellaOps.AirGap.Policy;
using StellaOps.TaskRunner.Core.Planning;
namespace StellaOps.TaskRunner.Tests;
public sealed class TaskPackPlannerTests
{
[Fact]
public void Plan_WithSequentialSteps_ComputesDeterministicHash()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(false)
};
var resultA = planner.Plan(manifest, inputs);
Assert.True(resultA.Success);
var plan = resultA.Plan!;
Assert.Equal(3, plan.Steps.Count);
Assert.Equal("plan-step", plan.Steps[0].Id);
Assert.Equal("plan-step", plan.Steps[0].TemplateId);
Assert.Equal("run", plan.Steps[0].Type);
Assert.Equal("gate.approval", plan.Steps[1].Type);
Assert.Equal("security-review", plan.Steps[1].ApprovalId);
Assert.Equal("run", plan.Steps[2].Type);
Assert.True(plan.Steps[2].Enabled);
Assert.Single(plan.Approvals);
Assert.Equal("security-review", plan.Approvals[0].Id);
Assert.False(string.IsNullOrWhiteSpace(plan.Hash));
var resultB = planner.Plan(manifest, inputs);
Assert.True(resultB.Success);
namespace StellaOps.TaskRunner.Tests;
public sealed class TaskPackPlannerTests
{
[Fact]
public void Plan_WithSequentialSteps_ComputesDeterministicHash()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(false)
};
var resultA = planner.Plan(manifest, inputs);
Assert.True(resultA.Success);
var plan = resultA.Plan!;
Assert.Equal(3, plan.Steps.Count);
Assert.Equal("plan-step", plan.Steps[0].Id);
Assert.Equal("plan-step", plan.Steps[0].TemplateId);
Assert.Equal("run", plan.Steps[0].Type);
Assert.Equal("gate.approval", plan.Steps[1].Type);
Assert.Equal("security-review", plan.Steps[1].ApprovalId);
Assert.Equal("run", plan.Steps[2].Type);
Assert.True(plan.Steps[2].Enabled);
Assert.Single(plan.Approvals);
Assert.Equal("security-review", plan.Approvals[0].Id);
Assert.False(string.IsNullOrWhiteSpace(plan.Hash));
var resultB = planner.Plan(manifest, inputs);
Assert.True(resultB.Success);
Assert.Equal(plan.Hash, resultB.Plan!.Hash);
}
@@ -54,108 +54,108 @@ public sealed class TaskPackPlannerTests
Assert.True(hex.All(c => Uri.IsHexDigit(c)), "Hash contains non-hex characters.");
}
[Fact]
public void Plan_WhenConditionEvaluatesFalse_DisablesStep()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(true)
};
var result = planner.Plan(manifest, inputs);
Assert.True(result.Success);
Assert.False(result.Plan!.Steps[2].Enabled);
}
[Fact]
public void Plan_WithStepReferences_MarksParametersAsRuntime()
{
var manifest = TestManifests.Load(TestManifests.StepReference);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
Assert.Equal(2, plan.Steps.Count);
var referenceParameters = plan.Steps[1].Parameters!;
Assert.True(referenceParameters["sourceSummary"].RequiresRuntimeValue);
Assert.Equal("steps.prepare.outputs.summary", referenceParameters["sourceSummary"].Expression);
}
[Fact]
public void Plan_WithMapStep_ExpandsIterations()
{
var manifest = TestManifests.Load(TestManifests.Map);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["targets"] = new JsonArray("alpha", "beta", "gamma")
};
var result = planner.Plan(manifest, inputs);
Assert.True(result.Success);
var plan = result.Plan!;
var mapStep = plan.Steps.Single(s => s.Type == "map");
Assert.Equal(3, mapStep.Children!.Count);
Assert.All(mapStep.Children!, child => Assert.Equal("echo-step", child.TemplateId));
Assert.Equal(3, mapStep.Parameters!["iterationCount"].Value!.GetValue<int>());
Assert.Equal("alpha", mapStep.Children![0].Parameters!["item"].Value!.GetValue<string>());
}
[Fact]
public void CollectApprovalRequirements_GroupsGates()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var requirements = TaskPackPlanInsights.CollectApprovalRequirements(plan);
Assert.Single(requirements);
var requirement = requirements[0];
Assert.Equal("security-review", requirement.ApprovalId);
Assert.Contains("Packs.Approve", requirement.Grants);
Assert.Equal(plan.Steps[1].Id, requirement.StepIds.Single());
var notifications = TaskPackPlanInsights.CollectNotificationHints(plan);
Assert.Contains(notifications, hint => hint.Type == "approval-request" && hint.StepId == plan.Steps[1].Id);
}
[Fact]
public void Plan_WithSecretReference_RecordsSecretMetadata()
{
var manifest = TestManifests.Load(TestManifests.Secret);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
Assert.Single(plan.Secrets);
Assert.Equal("apiKey", plan.Secrets[0].Name);
var param = plan.Steps[0].Parameters!["token"];
Assert.True(param.RequiresRuntimeValue);
Assert.Equal("secrets.apiKey", param.Expression);
}
[Fact]
[Fact]
public void Plan_WhenConditionEvaluatesFalse_DisablesStep()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["dryRun"] = JsonValue.Create(true)
};
var result = planner.Plan(manifest, inputs);
Assert.True(result.Success);
Assert.False(result.Plan!.Steps[2].Enabled);
}
[Fact]
public void Plan_WithStepReferences_MarksParametersAsRuntime()
{
var manifest = TestManifests.Load(TestManifests.StepReference);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
Assert.Equal(2, plan.Steps.Count);
var referenceParameters = plan.Steps[1].Parameters!;
Assert.True(referenceParameters["sourceSummary"].RequiresRuntimeValue);
Assert.Equal("steps.prepare.outputs.summary", referenceParameters["sourceSummary"].Expression);
}
[Fact]
public void Plan_WithMapStep_ExpandsIterations()
{
var manifest = TestManifests.Load(TestManifests.Map);
var planner = new TaskPackPlanner();
var inputs = new Dictionary<string, JsonNode?>
{
["targets"] = new JsonArray("alpha", "beta", "gamma")
};
var result = planner.Plan(manifest, inputs);
Assert.True(result.Success);
var plan = result.Plan!;
var mapStep = plan.Steps.Single(s => s.Type == "map");
Assert.Equal(3, mapStep.Children!.Count);
Assert.All(mapStep.Children!, child => Assert.Equal("echo-step", child.TemplateId));
Assert.Equal(3, mapStep.Parameters!["iterationCount"].Value!.GetValue<int>());
Assert.Equal("alpha", mapStep.Children![0].Parameters!["item"].Value!.GetValue<string>());
}
[Fact]
public void CollectApprovalRequirements_GroupsGates()
{
var manifest = TestManifests.Load(TestManifests.Sample);
var planner = new TaskPackPlanner();
var plan = planner.Plan(manifest).Plan!;
var requirements = TaskPackPlanInsights.CollectApprovalRequirements(plan);
Assert.Single(requirements);
var requirement = requirements[0];
Assert.Equal("security-review", requirement.ApprovalId);
Assert.Contains("Packs.Approve", requirement.Grants);
Assert.Equal(plan.Steps[1].Id, requirement.StepIds.Single());
var notifications = TaskPackPlanInsights.CollectNotificationHints(plan);
Assert.Contains(notifications, hint => hint.Type == "approval-request" && hint.StepId == plan.Steps[1].Id);
}
[Fact]
public void Plan_WithSecretReference_RecordsSecretMetadata()
{
var manifest = TestManifests.Load(TestManifests.Secret);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
Assert.Single(plan.Secrets);
Assert.Equal("apiKey", plan.Secrets[0].Name);
var param = plan.Steps[0].Parameters!["token"];
Assert.True(param.RequiresRuntimeValue);
Assert.Equal("secrets.apiKey", param.Expression);
}
[Fact]
public void Plan_WithOutputs_ProjectsResolvedValues()
{
var manifest = TestManifests.Load(TestManifests.Output);
var planner = new TaskPackPlanner();
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
Assert.Equal(2, plan.Outputs.Count);
var bundle = plan.Outputs.First(o => o.Name == "bundlePath");
Assert.NotNull(bundle.Path);
Assert.False(bundle.Path!.RequiresRuntimeValue);
Assert.Equal("artifacts/report.txt", bundle.Path.Value!.GetValue<string>());
var result = planner.Plan(manifest);
Assert.True(result.Success);
var plan = result.Plan!;
Assert.Equal(2, plan.Outputs.Count);
var bundle = plan.Outputs.First(o => o.Name == "bundlePath");
Assert.NotNull(bundle.Path);
Assert.False(bundle.Path!.RequiresRuntimeValue);
Assert.Equal("artifacts/report.txt", bundle.Path.Value!.GetValue<string>());
var evidence = plan.Outputs.First(o => o.Name == "evidenceModel");
Assert.NotNull(evidence.Expression);
Assert.True(evidence.Expression!.RequiresRuntimeValue);
@@ -211,8 +211,8 @@ public sealed class TaskPackPlannerTests
Assert.False(result.Success);
Assert.Contains(result.Errors, error => error.Message.Contains("example.com", StringComparison.OrdinalIgnoreCase));
}
[Fact]
[Fact]
public void Plan_WhenRequiredInputMissing_ReturnsError()
{
var manifest = TestManifests.Load(TestManifests.RequiredInput);