up
This commit is contained in:
@@ -1304,10 +1304,60 @@ internal static class CommandFactory
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
var batchOptions = CreateAdvisoryOptions();
|
||||
var batchKeys = new Argument<string[]>("advisory-keys")
|
||||
{
|
||||
Description = "One or more advisory identifiers.",
|
||||
Arity = ArgumentArity.OneOrMore
|
||||
};
|
||||
var batch = new Command("batch", "Run Advisory AI over multiple advisories with a single invocation.");
|
||||
batch.Add(batchKeys);
|
||||
batch.Add(batchOptions.Output);
|
||||
batch.Add(batchOptions.AdvisoryKey);
|
||||
batch.Add(batchOptions.ArtifactId);
|
||||
batch.Add(batchOptions.ArtifactPurl);
|
||||
batch.Add(batchOptions.PolicyVersion);
|
||||
batch.Add(batchOptions.Profile);
|
||||
batch.Add(batchOptions.Sections);
|
||||
batch.Add(batchOptions.ForceRefresh);
|
||||
batch.Add(batchOptions.TimeoutSeconds);
|
||||
batch.Add(batchOptions.Format);
|
||||
batch.SetAction((parseResult, _) =>
|
||||
{
|
||||
var advisoryKeys = parseResult.GetValue(batchKeys) ?? Array.Empty<string>();
|
||||
var artifactId = parseResult.GetValue(batchOptions.ArtifactId);
|
||||
var artifactPurl = parseResult.GetValue(batchOptions.ArtifactPurl);
|
||||
var policyVersion = parseResult.GetValue(batchOptions.PolicyVersion);
|
||||
var profile = parseResult.GetValue(batchOptions.Profile) ?? "default";
|
||||
var sections = parseResult.GetValue(batchOptions.Sections) ?? Array.Empty<string>();
|
||||
var forceRefresh = parseResult.GetValue(batchOptions.ForceRefresh);
|
||||
var timeoutSeconds = parseResult.GetValue(batchOptions.TimeoutSeconds) ?? 120;
|
||||
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(batchOptions.Format));
|
||||
var outputDirectory = parseResult.GetValue(batchOptions.Output);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleAdviseBatchAsync(
|
||||
services,
|
||||
AdvisoryAiTaskType.Summary,
|
||||
advisoryKeys,
|
||||
artifactId,
|
||||
artifactPurl,
|
||||
policyVersion,
|
||||
profile,
|
||||
sections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
outputFormat,
|
||||
outputDirectory,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
advise.Add(run);
|
||||
advise.Add(summarize);
|
||||
advise.Add(explain);
|
||||
advise.Add(remediate);
|
||||
advise.Add(batch);
|
||||
return advise;
|
||||
}
|
||||
|
||||
|
||||
@@ -593,6 +593,92 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleAdviseBatchAsync(
|
||||
IServiceProvider services,
|
||||
AdvisoryAiTaskType taskType,
|
||||
IReadOnlyList<string> advisoryKeys,
|
||||
string? artifactId,
|
||||
string? artifactPurl,
|
||||
string? policyVersion,
|
||||
string profile,
|
||||
IReadOnlyList<string> preferredSections,
|
||||
bool forceRefresh,
|
||||
int timeoutSeconds,
|
||||
AdvisoryOutputFormat outputFormat,
|
||||
string? outputDirectory,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (advisoryKeys.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one advisory key is required.", nameof(advisoryKeys));
|
||||
}
|
||||
|
||||
var outputDir = string.IsNullOrWhiteSpace(outputDirectory) ? null : Path.GetFullPath(outputDirectory!);
|
||||
if (outputDir is not null)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
}
|
||||
|
||||
var results = new List<(string Advisory, int ExitCode)>();
|
||||
var overallExit = 0;
|
||||
|
||||
foreach (var key in advisoryKeys)
|
||||
{
|
||||
var sanitized = string.IsNullOrWhiteSpace(key) ? "unknown" : key.Trim();
|
||||
var ext = outputFormat switch
|
||||
{
|
||||
AdvisoryOutputFormat.Json => ".json",
|
||||
AdvisoryOutputFormat.Markdown => ".md",
|
||||
_ => ".txt"
|
||||
};
|
||||
|
||||
var outputPath = outputDir is null ? null : Path.Combine(outputDir, $"{SanitizeFileName(sanitized)}-{taskType.ToString().ToLowerInvariant()}{ext}");
|
||||
|
||||
Environment.ExitCode = 0; // reset per advisory to capture individual result
|
||||
|
||||
await HandleAdviseRunAsync(
|
||||
services,
|
||||
taskType,
|
||||
sanitized,
|
||||
artifactId,
|
||||
artifactPurl,
|
||||
policyVersion,
|
||||
profile,
|
||||
preferredSections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
outputFormat,
|
||||
outputPath,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
|
||||
var code = Environment.ExitCode;
|
||||
results.Add((sanitized, code));
|
||||
overallExit = overallExit == 0 ? code : overallExit; // retain first non-zero if any
|
||||
}
|
||||
|
||||
if (results.Count > 1)
|
||||
{
|
||||
var table = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Title("[bold]Advisory Batch[/]");
|
||||
table.AddColumn("Advisory");
|
||||
table.AddColumn("Task");
|
||||
table.AddColumn("Exit Code");
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
var exitText = result.ExitCode == 0 ? "[green]0[/]" : $"[red]{result.ExitCode}[/]";
|
||||
table.AddRow(Markup.Escape(result.Advisory), taskType.ToString(), exitText);
|
||||
}
|
||||
|
||||
AnsiConsole.Console.Write(table);
|
||||
}
|
||||
|
||||
Environment.ExitCode = overallExit;
|
||||
}
|
||||
|
||||
public static async Task HandleSourcesIngestAsync(
|
||||
IServiceProvider services,
|
||||
bool dryRun,
|
||||
|
||||
@@ -779,6 +779,124 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseBatchAsync_RunsAllAdvisories()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
var testConsole = new TestConsole();
|
||||
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.Console = testConsole;
|
||||
|
||||
var planResponse = new AdvisoryPipelinePlanResponseModel
|
||||
{
|
||||
TaskType = "Summary",
|
||||
CacheKey = "batch-plan",
|
||||
PromptTemplate = "prompts/advisory/summary.liquid",
|
||||
Budget = new AdvisoryTaskBudgetModel { PromptTokens = 64, CompletionTokens = 32 },
|
||||
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
|
||||
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var outputs = new Queue<AdvisoryPipelineOutputModel?>(new[]
|
||||
{
|
||||
new AdvisoryPipelineOutputModel
|
||||
{
|
||||
CacheKey = "k1",
|
||||
TaskType = "Summary",
|
||||
Profile = "default",
|
||||
Prompt = "P1",
|
||||
Response = "Body one",
|
||||
Citations = new[] { new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-1", ChunkId = "c-1" } },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Guardrail = new AdvisoryOutputGuardrailModel
|
||||
{
|
||||
Blocked = false,
|
||||
SanitizedPrompt = "P1",
|
||||
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
},
|
||||
Provenance = new AdvisoryOutputProvenanceModel
|
||||
{
|
||||
InputDigest = "sha256:1",
|
||||
OutputHash = "sha256:1out",
|
||||
Signatures = Array.Empty<string>()
|
||||
},
|
||||
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
|
||||
PlanFromCache = false
|
||||
},
|
||||
new AdvisoryPipelineOutputModel
|
||||
{
|
||||
CacheKey = "k2",
|
||||
TaskType = "Summary",
|
||||
Profile = "default",
|
||||
Prompt = "P2",
|
||||
Response = "Body two",
|
||||
Citations = new[] { new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-2", ChunkId = "c-2" } },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Guardrail = new AdvisoryOutputGuardrailModel
|
||||
{
|
||||
Blocked = false,
|
||||
SanitizedPrompt = "P2",
|
||||
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
},
|
||||
Provenance = new AdvisoryOutputProvenanceModel
|
||||
{
|
||||
InputDigest = "sha256:2",
|
||||
OutputHash = "sha256:2out",
|
||||
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,
|
||||
AdvisoryOutputQueue = outputs
|
||||
};
|
||||
|
||||
var provider = BuildServiceProvider(backend);
|
||||
using var tempDir = new TempDirectory();
|
||||
|
||||
await CommandHandlers.HandleAdviseBatchAsync(
|
||||
provider,
|
||||
AdvisoryAiTaskType.Summary,
|
||||
new[] { "ADV-1", "ADV-2" },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"default",
|
||||
Array.Empty<string>(),
|
||||
forceRefresh: false,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Markdown,
|
||||
outputDirectory: tempDir.Path,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
var file1 = Path.Combine(tempDir.Path, "ADV-1-summary.md");
|
||||
var file2 = Path.Combine(tempDir.Path, "ADV-2-summary.md");
|
||||
Assert.True(File.Exists(file1));
|
||||
Assert.True(File.Exists(file2));
|
||||
Assert.Contains("Body one", await File.ReadAllTextAsync(file1));
|
||||
Assert.Contains("Body two", await File.ReadAllTextAsync(file2));
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.Contains("Advisory Batch", testConsole.Output, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations()
|
||||
{
|
||||
@@ -976,7 +1094,198 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForRemediation()
|
||||
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,
|
||||
outputFormat: AdvisoryOutputFormat.Table,
|
||||
outputPath: null,
|
||||
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_WritesMarkdownWithCitations_ForExplain()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
var testConsole = new TestConsole();
|
||||
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.Console = testConsole;
|
||||
|
||||
var planResponse = new AdvisoryPipelinePlanResponseModel
|
||||
{
|
||||
TaskType = "Conflict",
|
||||
CacheKey = "plan-conflict",
|
||||
PromptTemplate = "prompts/advisory/conflict.liquid",
|
||||
Budget = new AdvisoryTaskBudgetModel
|
||||
{
|
||||
PromptTokens = 128,
|
||||
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 = "Sanitized prompt",
|
||||
Response = "Rendered conflict body.",
|
||||
Citations = new[]
|
||||
{
|
||||
new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-42", ChunkId = "chunk-42" }
|
||||
},
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Guardrail = new AdvisoryOutputGuardrailModel
|
||||
{
|
||||
Blocked = false,
|
||||
SanitizedPrompt = "Sanitized prompt",
|
||||
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
},
|
||||
Provenance = new AdvisoryOutputProvenanceModel
|
||||
{
|
||||
InputDigest = "sha256:conflict-in",
|
||||
OutputHash = "sha256:conflict-out",
|
||||
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);
|
||||
var outputPath = Path.GetTempFileName();
|
||||
|
||||
await CommandHandlers.HandleAdviseRunAsync(
|
||||
provider,
|
||||
AdvisoryAiTaskType.Conflict,
|
||||
"ADV-42",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"default",
|
||||
Array.Empty<string>(),
|
||||
forceRefresh: false,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Markdown,
|
||||
outputPath: outputPath,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
var markdown = await File.ReadAllTextAsync(outputPath);
|
||||
Assert.Contains("Conflict", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Rendered conflict body", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("doc-42", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("chunk-42", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Citations", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.Contains("Conflict", testConsole.Output, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(AdvisoryAiTaskType.Conflict, backend.AdvisoryPlanRequests.Last().TaskType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForRemediationTask()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
@@ -1073,99 +1382,6 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
}
|
||||
|
||||
[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,
|
||||
outputFormat: AdvisoryOutputFormat.Table,
|
||||
outputPath: null,
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user