feat: Implement Policy Engine Evaluation Service and Cache with unit tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

Temp commit to debug
This commit is contained in:
master
2025-11-05 07:35:53 +00:00
parent 40e7f827da
commit 9253620833
125 changed files with 18735 additions and 17215 deletions

View File

@@ -1,105 +1,105 @@
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)
{
var failurePolicy = new TaskPackPlanFailurePolicy(MaxAttempts: 3, BackoffSeconds: 30, ContinueOnError: false);
var steps = new Dictionary<string, PackRunStepStateRecord>(StringComparer.Ordinal)
{
["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,
StatusReason: null)
};
return PackRunState.Create(runId, "hash-123", failurePolicy, steps, DateTimeOffset.UtcNow);
}
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.
}
}
}
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)
{
var failurePolicy = new TaskPackPlanFailurePolicy(MaxAttempts: 3, BackoffSeconds: 30, ContinueOnError: false);
var steps = new Dictionary<string, PackRunStepStateRecord>(StringComparer.Ordinal)
{
["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,
StatusReason: null)
};
return PackRunState.Create(runId, "hash-123", failurePolicy, steps, DateTimeOffset.UtcNow);
}
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,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);
}
return PackRunState.Create("run-1", plan.Hash, graph.FailurePolicy, 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;
}
}
}
}
}
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, graph.FailurePolicy, 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;
}
}
}
}
}

View File

@@ -1,75 +1,75 @@
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);
});
}
}
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);
});
}
}

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);
}
}