- Implemented PolicyPackSelectorComponent for selecting policy packs. - Added unit tests for component behavior, including API success and error handling. - Introduced monaco-workers type declarations for editor workers. - Created acceptance tests for guardrails with stubs for AT1–AT10. - Established SCA Failure Catalogue Fixtures for regression testing. - Developed plugin determinism harness with stubs for PL1–PL10. - Added scripts for evidence upload and verification processes.
259 lines
9.3 KiB
C#
259 lines
9.3 KiB
C#
using System;
|
|
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);
|
|
Assert.Equal(plan.Hash, resultB.Plan!.Hash);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlanHash_IsPrefixedSha256Digest()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.Sample);
|
|
var planner = new TaskPackPlanner();
|
|
|
|
var result = planner.Plan(manifest);
|
|
Assert.True(result.Success);
|
|
var hash = result.Plan!.Hash;
|
|
Assert.StartsWith("sha256:", hash, StringComparison.Ordinal);
|
|
Assert.Equal(71, hash.Length); // "sha256:" + 64 hex characters
|
|
var hex = hash.Substring("sha256:".Length);
|
|
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]
|
|
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 evidence = plan.Outputs.First(o => o.Name == "evidenceModel");
|
|
Assert.NotNull(evidence.Expression);
|
|
Assert.True(evidence.Expression!.RequiresRuntimeValue);
|
|
Assert.Equal("steps.generate.outputs.evidence", evidence.Expression.Expression);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_WithFailurePolicy_PopulatesPlanFailure()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.FailurePolicy);
|
|
var planner = new TaskPackPlanner();
|
|
|
|
var result = planner.Plan(manifest);
|
|
Assert.True(result.Success);
|
|
var plan = result.Plan!;
|
|
Assert.NotNull(plan.FailurePolicy);
|
|
Assert.Equal(4, plan.FailurePolicy!.MaxAttempts);
|
|
Assert.Equal(30, plan.FailurePolicy.BackoffSeconds);
|
|
Assert.False(plan.FailurePolicy.ContinueOnError);
|
|
}
|
|
|
|
[Fact]
|
|
public void PolicyGateHints_IncludeRuntimeMetadata()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.PolicyGate);
|
|
var planner = new TaskPackPlanner();
|
|
|
|
var plan = planner.Plan(manifest).Plan!;
|
|
var hints = TaskPackPlanInsights.CollectPolicyGateHints(plan);
|
|
Assert.Single(hints);
|
|
var hint = hints[0];
|
|
Assert.Equal("policy-check", hint.StepId);
|
|
var threshold = hint.Parameters.Single(p => p.Name == "threshold");
|
|
Assert.False(threshold.RequiresRuntimeValue);
|
|
Assert.Null(threshold.Expression);
|
|
var evidence = hint.Parameters.Single(p => p.Name == "evidenceRef");
|
|
Assert.True(evidence.RequiresRuntimeValue);
|
|
Assert.Equal("steps.prepare.outputs.evidence", evidence.Expression);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_SealedMode_BlocksUndeclaredEgress()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.EgressBlocked);
|
|
var options = new EgressPolicyOptions
|
|
{
|
|
Mode = EgressPolicyMode.Sealed
|
|
};
|
|
var planner = new TaskPackPlanner(new EgressPolicy(options));
|
|
|
|
var result = planner.Plan(manifest);
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, error => error.Message.Contains("example.com", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_WhenRequiredInputMissing_ReturnsError()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.RequiredInput);
|
|
var planner = new TaskPackPlanner();
|
|
|
|
var result = planner.Plan(manifest);
|
|
Assert.False(result.Success);
|
|
Assert.NotEmpty(result.Errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_SealedMode_AllowsDeclaredEgress()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.EgressAllowed);
|
|
var options = new EgressPolicyOptions
|
|
{
|
|
Mode = EgressPolicyMode.Sealed
|
|
};
|
|
options.AddAllowRule("mirror.internal", 443, EgressTransport.Https);
|
|
|
|
var planner = new TaskPackPlanner(new EgressPolicy(options));
|
|
|
|
var result = planner.Plan(manifest);
|
|
|
|
Assert.True(result.Success);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plan_SealedMode_RuntimeUrlWithoutDeclaration_ReturnsError()
|
|
{
|
|
var manifest = TestManifests.Load(TestManifests.EgressRuntime);
|
|
var options = new EgressPolicyOptions
|
|
{
|
|
Mode = EgressPolicyMode.Sealed
|
|
};
|
|
var planner = new TaskPackPlanner(new EgressPolicy(options));
|
|
|
|
var result = planner.Plan(manifest);
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, error => error.Path.StartsWith("spec.steps[0]", StringComparison.Ordinal));
|
|
}
|
|
}
|