Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added MongoPackRunApprovalStore for managing approval states with MongoDB.
- Introduced MongoPackRunArtifactUploader for uploading and storing artifacts.
- Created MongoPackRunLogStore to handle logging of pack run events.
- Developed MongoPackRunStateStore for persisting and retrieving pack run states.
- Implemented unit tests for MongoDB stores to ensure correct functionality.
- Added MongoTaskRunnerTestContext for setting up MongoDB test environment.
- Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -23,6 +23,7 @@ using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Tests.Testing;
using StellaOps.Cryptography;
@@ -223,6 +224,291 @@ public sealed class CommandHandlersTests
}
}
[Fact]
public async Task HandleAdviseRunAsync_WritesOutputAndSetsExitCode()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Summary.ToString(),
CacheKey = "cache-123",
PromptTemplate = "prompts/advisory/summary.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 512,
CompletionTokens = 128
},
Chunks = new[]
{
new PipelineChunkSummaryModel
{
DocumentId = "doc-1",
ChunkId = "chunk-1",
Section = "Summary",
DisplaySection = "Summary"
}
},
Vectors = new[]
{
new PipelineVectorSummaryModel
{
Query = "summary query",
Matches = new[]
{
new PipelineVectorMatchSummaryModel
{
ChunkId = "chunk-1",
Score = 0.9
}
}
}
},
Metadata = new Dictionary<string, string>
{
["profile"] = "default"
}
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Summary result",
Citations = new[]
{
new AdvisoryOutputCitationModel
{
Index = 0,
DocumentId = "doc-1",
ChunkId = "chunk-1"
}
},
Metadata = new Dictionary<string, string>
{
["confidence"] = "high"
},
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = false,
SanitizedPrompt = "Summary result",
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:aaa",
OutputHash = "sha256:bbb",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
PlanFromCache = false
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Summary,
" ADV-1 ",
null,
null,
null,
"default",
new[] { "impact", "impact " },
forceRefresh: false,
timeoutSeconds: 0,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Single(backend.AdvisoryPlanRequests);
var request = backend.AdvisoryPlanRequests[0];
Assert.Equal(AdvisoryAiTaskType.Summary, request.TaskType);
Assert.Equal("ADV-1", request.Request.AdvisoryKey);
Assert.NotNull(request.Request.PreferredSections);
Assert.Single(request.Request.PreferredSections!);
Assert.Equal("impact", request.Request.PreferredSections![0]);
Assert.Single(backend.AdvisoryOutputRequests);
Assert.Equal(planResponse.CacheKey, backend.AdvisoryOutputRequests[0].CacheKey);
Assert.Equal("default", backend.AdvisoryOutputRequests[0].Profile);
var output = testConsole.Output;
Assert.Contains("Advisory Output", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains(planResponse.CacheKey, output, StringComparison.Ordinal);
Assert.Contains("Summary result", output, StringComparison.Ordinal);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_ReturnsGuardrailExitCodeOnBlock()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Remediation.ToString(),
CacheKey = "cache-guard",
PromptTemplate = "prompts/advisory/remediation.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 256,
CompletionTokens = 64
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Blocked output",
Citations = Array.Empty<AdvisoryOutputCitationModel>(),
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = true,
SanitizedPrompt = "Blocked output",
Violations = new[]
{
new AdvisoryOutputGuardrailViolationModel
{
Code = "PROMPT_INJECTION",
Message = "Detected prompt injection attempt."
}
},
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:ccc",
OutputHash = "sha256:ddd",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T13:05:00Z", CultureInfo.InvariantCulture),
PlanFromCache = true
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Remediation,
"ADV-2",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: true,
timeoutSeconds: 0,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(65, Environment.ExitCode);
Assert.Contains("Guardrail Violations", testConsole.Output, StringComparison.OrdinalIgnoreCase);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_TimesOutWhenOutputMissing()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = new TestConsole();
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Conflict.ToString(),
CacheKey = "cache-timeout",
PromptTemplate = "prompts/advisory/conflict.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 128,
CompletionTokens = 32
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = null
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Conflict,
"ADV-3",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: false,
timeoutSeconds: 0,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(70, Environment.ExitCode);
Assert.Single(backend.AdvisoryOutputRequests);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAuthLoginAsync_UsesClientCredentialsFlow()
{
@@ -1726,10 +2012,16 @@ spec:
Assert.NotNull(backend.LastTaskRunnerSimulationRequest);
var consoleOutput = writer.ToString();
Assert.Contains("\"planHash\":\"hash-xyz789\"", consoleOutput, StringComparison.Ordinal);
using (var consoleJson = JsonDocument.Parse(consoleOutput))
{
Assert.Equal("hash-xyz789", consoleJson.RootElement.GetProperty("planHash").GetString());
}
var fileOutput = await File.ReadAllTextAsync(outputPath);
Assert.Contains("\"planHash\":\"hash-xyz789\"", fileOutput, StringComparison.Ordinal);
using (var fileJson = JsonDocument.Parse(fileOutput))
{
Assert.Equal("hash-xyz789", fileJson.RootElement.GetProperty("planHash").GetString());
}
Assert.True(backend.LastTaskRunnerSimulationRequest!.Inputs!.TryGetPropertyValue("dryRun", out var dryRunNode));
Assert.False(dryRunNode!.GetValue<bool>());
@@ -2738,6 +3030,13 @@ spec:
public EntryTraceResponseModel? EntryTraceResponse { get; set; }
public Exception? EntryTraceException { get; set; }
public string? LastEntryTraceScanId { get; private set; }
public List<(AdvisoryAiTaskType TaskType, AdvisoryPipelinePlanRequestModel Request)> AdvisoryPlanRequests { get; } = new();
public AdvisoryPipelinePlanResponseModel? AdvisoryPlanResponse { get; set; }
public Exception? AdvisoryPlanException { get; set; }
public Queue<AdvisoryPipelineOutputModel?> AdvisoryOutputQueue { get; } = new();
public AdvisoryPipelineOutputModel? AdvisoryOutputResponse { get; set; }
public Exception? AdvisoryOutputException { get; set; }
public List<(string CacheKey, AdvisoryAiTaskType TaskType, string Profile)> AdvisoryOutputRequests { get; } = new();
public Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken)
=> throw new NotImplementedException();
@@ -2890,10 +3189,52 @@ spec:
return Task.FromResult(EntryTraceResponse);
}
public Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(AdvisoryAiTaskType taskType, AdvisoryPipelinePlanRequestModel request, CancellationToken cancellationToken)
{
AdvisoryPlanRequests.Add((taskType, request));
if (AdvisoryPlanException is not null)
{
throw AdvisoryPlanException;
}
var response = AdvisoryPlanResponse ?? new AdvisoryPipelinePlanResponseModel
{
TaskType = taskType.ToString(),
CacheKey = "stub-cache-key",
PromptTemplate = "prompts/advisory/stub.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 0,
CompletionTokens = 0
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
};
return Task.FromResult(response);
}
public Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(string cacheKey, AdvisoryAiTaskType taskType, string profile, CancellationToken cancellationToken)
{
AdvisoryOutputRequests.Add((cacheKey, taskType, profile));
if (AdvisoryOutputException is not null)
{
throw AdvisoryOutputException;
}
if (AdvisoryOutputQueue.Count > 0)
{
return Task.FromResult(AdvisoryOutputQueue.Dequeue());
}
return Task.FromResult(AdvisoryOutputResponse);
}
}
private sealed class StubExecutor : IScannerExecutor
{
private sealed class StubExecutor : IScannerExecutor
{
private readonly ScannerExecutionResult _result;
public StubExecutor(ScannerExecutionResult result)