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
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:
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user