using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.TaskRunner.Core.Execution; using StellaOps.TaskRunner.Core.Planning; using StellaOps.TaskRunner.Infrastructure.Execution; using StellaOps.TaskRunner.Worker.Services; namespace StellaOps.TaskRunner.Tests; public sealed class BundleIngestionStepExecutorTests { [Fact] public async Task ExecuteAsync_ValidBundle_CopiesAndSucceeds() { using var temp = new TempDirectory(); var source = Path.Combine(temp.Path, "bundle.tgz"); await File.WriteAllTextAsync(source, "bundle-data"); var checksum = "3e25960a79dbc69b674cd4ec67a72c62b3aa32b1d4d216177a5ffcc6f46673b5"; // sha256 of "bundle-data" var options = Options.Create(new PackRunWorkerOptions { ArtifactsPath = temp.Path }); var executor = new BundleIngestionStepExecutor(options, NullLogger.Instance); var step = CreateStep("builtin:bundle.ingest", new Dictionary { ["path"] = Value(source), ["checksum"] = Value(checksum) }); var result = await executor.ExecuteAsync(step, step.Parameters, CancellationToken.None); Assert.True(result.Succeeded); var staged = Path.Combine(temp.Path, "bundles", checksum, "bundle.tgz"); Assert.True(File.Exists(staged)); Assert.Equal(await File.ReadAllBytesAsync(source), await File.ReadAllBytesAsync(staged)); var metadataPath = Path.Combine(temp.Path, "bundles", checksum, "metadata.json"); Assert.True(File.Exists(metadataPath)); var metadata = await File.ReadAllTextAsync(metadataPath); Assert.Contains(checksum, metadata, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task ExecuteAsync_ChecksumMismatch_Fails() { using var temp = new TempDirectory(); var source = Path.Combine(temp.Path, "bundle.tgz"); await File.WriteAllTextAsync(source, "bundle-data"); var options = Options.Create(new PackRunWorkerOptions { ArtifactsPath = temp.Path }); var executor = new BundleIngestionStepExecutor(options, NullLogger.Instance); var step = CreateStep("builtin:bundle.ingest", new Dictionary { ["path"] = Value(source), ["checksum"] = Value("deadbeef") }); var result = await executor.ExecuteAsync(step, step.Parameters, CancellationToken.None); Assert.False(result.Succeeded); Assert.Contains("Checksum mismatch", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task ExecuteAsync_MissingChecksum_Fails() { using var temp = new TempDirectory(); var source = Path.Combine(temp.Path, "bundle.tgz"); await File.WriteAllTextAsync(source, "bundle-data"); var options = Options.Create(new PackRunWorkerOptions { ArtifactsPath = temp.Path }); var executor = new BundleIngestionStepExecutor(options, NullLogger.Instance); var step = CreateStep("builtin:bundle.ingest", new Dictionary { ["path"] = Value(source) }); var result = await executor.ExecuteAsync(step, step.Parameters, CancellationToken.None); Assert.False(result.Succeeded); Assert.Contains("Checksum is required", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task ExecuteAsync_UnknownUses_NoOpSuccess() { var executor = new BundleIngestionStepExecutor( Options.Create(new PackRunWorkerOptions { ArtifactsPath = Path.GetTempPath() }), NullLogger.Instance); var step = CreateStep("builtin:noop", new Dictionary()); var result = await executor.ExecuteAsync(step, step.Parameters, CancellationToken.None); Assert.True(result.Succeeded); } private static TaskPackPlanParameterValue Value(string literal) => new(JsonValue.Create(literal), null, null, false); private static PackRunExecutionStep CreateStep(string uses, IReadOnlyDictionary parameters) => new( id: "ingest", templateId: "ingest", kind: PackRunStepKind.Run, enabled: true, uses: uses, parameters: parameters, approvalId: null, gateMessage: null, maxParallel: null, continueOnError: false, children: PackRunExecutionStep.EmptyChildren); private sealed class TempDirectory : IDisposable { public TempDirectory() { Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString("n")); Directory.CreateDirectory(Path); } public string Path { get; } public void Dispose() { if (Directory.Exists(Path)) { Directory.Delete(Path, recursive: true); } } } }