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
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user