using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.TaskRunner.Core.Execution; using StellaOps.TaskRunner.Core.Planning; using StellaOps.TaskRunner.Storage.Postgres; using StellaOps.TaskRunner.Storage.Postgres.Repositories; using StellaOps.Infrastructure.Postgres.Options; using Xunit; namespace StellaOps.TaskRunner.Storage.Postgres.Tests; [Collection(TaskRunnerPostgresCollection.Name)] public sealed class PostgresPackRunStateStoreTests : IAsyncLifetime { private readonly TaskRunnerPostgresFixture _fixture; private readonly PostgresPackRunStateStore _store; private readonly TaskRunnerDataSource _dataSource; public PostgresPackRunStateStoreTests(TaskRunnerPostgresFixture fixture) { _fixture = fixture; var options = Options.Create(new PostgresOptions { ConnectionString = fixture.ConnectionString, SchemaName = TaskRunnerDataSource.DefaultSchemaName, AutoMigrate = false }); _dataSource = new TaskRunnerDataSource(options, NullLogger.Instance); _store = new PostgresPackRunStateStore(_dataSource, NullLogger.Instance); } public async Task InitializeAsync() { await _fixture.TruncateAllTablesAsync(); } public async Task DisposeAsync() { await _dataSource.DisposeAsync(); } [Fact] public async Task GetAsync_ReturnsNullForUnknownRunId() { // Act var result = await _store.GetAsync("nonexistent-run-id", CancellationToken.None); // Assert result.Should().BeNull(); } [Fact] public async Task SaveAndGet_RoundTripsState() { // Arrange var runId = "run-" + Guid.NewGuid().ToString("N")[..8]; var state = CreateState(runId); // Act await _store.SaveAsync(state, CancellationToken.None); var fetched = await _store.GetAsync(runId, CancellationToken.None); // Assert fetched.Should().NotBeNull(); fetched!.RunId.Should().Be(runId); fetched.PlanHash.Should().Be("sha256:plan123"); fetched.Plan.Metadata.Name.Should().Be("test-pack"); fetched.Steps.Should().HaveCount(1); } [Fact] public async Task SaveAsync_UpdatesExistingState() { // Arrange var runId = "run-" + Guid.NewGuid().ToString("N")[..8]; var state1 = CreateState(runId, "sha256:hash1"); var state2 = CreateState(runId, "sha256:hash2"); // Act await _store.SaveAsync(state1, CancellationToken.None); await _store.SaveAsync(state2, CancellationToken.None); var fetched = await _store.GetAsync(runId, CancellationToken.None); // Assert fetched.Should().NotBeNull(); fetched!.PlanHash.Should().Be("sha256:hash2"); } [Fact] public async Task ListAsync_ReturnsAllStates() { // Arrange var state1 = CreateState("run-list-1"); var state2 = CreateState("run-list-2"); await _store.SaveAsync(state1, CancellationToken.None); await _store.SaveAsync(state2, CancellationToken.None); // Act var states = await _store.ListAsync(CancellationToken.None); // Assert states.Should().HaveCountGreaterOrEqualTo(2); states.Select(s => s.RunId).Should().Contain("run-list-1", "run-list-2"); } private static PackRunState CreateState(string runId, string planHash = "sha256:plan123") { var now = DateTimeOffset.UtcNow; var metadata = new TaskPackPlanMetadata( Name: "test-pack", Version: "1.0.0", Description: "Test pack for integration tests", Tags: ["test"]); var plan = new TaskPackPlan( metadata: metadata, inputs: new Dictionary(), steps: [], hash: planHash, approvals: [], secrets: [], outputs: [], failurePolicy: null); var failurePolicy = new TaskPackPlanFailurePolicy( MaxAttempts: 3, BackoffSeconds: 30, ContinueOnError: false); var stepState = new PackRunStepStateRecord( StepId: "step-1", Kind: PackRunStepKind.Run, Enabled: true, ContinueOnError: false, MaxParallel: null, ApprovalId: null, GateMessage: null, Status: PackRunStepExecutionStatus.Pending, Attempts: 0, LastTransitionAt: null, NextAttemptAt: null, StatusReason: null); var steps = new Dictionary(StringComparer.Ordinal) { ["step-1"] = stepState }; return new PackRunState( RunId: runId, PlanHash: planHash, Plan: plan, FailurePolicy: failurePolicy, RequestedAt: now, CreatedAt: now, UpdatedAt: now, Steps: steps, TenantId: "test-tenant"); } }