using System.Text.Json.Nodes; using StellaOps.TaskRunner.Core.Execution; using StellaOps.TaskRunner.Core.Planning; using StellaOps.TaskRunner.Infrastructure.Execution; using StellaOps.TestKit; namespace StellaOps.TaskRunner.Tests; public sealed class FilePackRunStateStoreTests { [Trait("Category", TestCategories.Unit)] [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); } } [Trait("Category", TestCategories.Unit)] [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()); var parameters = new Dictionary(StringComparer.Ordinal); var stepPlan = new TaskPackPlanStep( Id: "step-a", TemplateId: "run/image", Name: "Run step", Type: "run", Enabled: true, Uses: "builtin/run", Parameters: parameters, ApprovalId: null, GateMessage: null, Children: Array.Empty()); var plan = new TaskPackPlan( metadata, new Dictionary(StringComparer.Ordinal), new[] { stepPlan }, "hash-123", Array.Empty(), Array.Empty(), Array.Empty(), failurePolicy); var steps = new Dictionary(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) }; 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. } } }