Add PHP Analyzer Plugin and Composer Lock Data Handling
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented the PhpAnalyzerPlugin to analyze PHP projects. - Created ComposerLockData class to represent data from composer.lock files. - Developed ComposerLockReader to load and parse composer.lock files asynchronously. - Introduced ComposerPackage class to encapsulate package details. - Added PhpPackage class to represent PHP packages with metadata and evidence. - Implemented PhpPackageCollector to gather packages from ComposerLockData. - Created PhpLanguageAnalyzer to perform analysis and emit results. - Added capability signals for known PHP frameworks and CMS. - Developed unit tests for the PHP language analyzer and its components. - Included sample composer.lock and expected output for testing. - Updated project files for the new PHP analyzer library and tests.
This commit is contained in:
@@ -1155,72 +1155,29 @@ internal static class CommandFactory
|
||||
var advise = new Command("advise", "Interact with Advisory AI pipelines.");
|
||||
_ = options;
|
||||
|
||||
var run = new Command("run", "Generate Advisory AI output for the specified task.");
|
||||
var taskArgument = new Argument<string>("task")
|
||||
var runOptions = CreateAdvisoryOptions();
|
||||
var runTaskArgument = new Argument<string>("task")
|
||||
{
|
||||
Description = "Task to run (summary, conflict, remediation)."
|
||||
};
|
||||
run.Add(taskArgument);
|
||||
|
||||
var advisoryKeyOption = new Option<string>("--advisory-key")
|
||||
{
|
||||
Description = "Advisory identifier to summarise (required).",
|
||||
Required = true
|
||||
};
|
||||
var artifactIdOption = new Option<string?>("--artifact-id")
|
||||
{
|
||||
Description = "Optional artifact identifier to scope SBOM context."
|
||||
};
|
||||
var artifactPurlOption = new Option<string?>("--artifact-purl")
|
||||
{
|
||||
Description = "Optional package URL to scope dependency context."
|
||||
};
|
||||
var policyVersionOption = new Option<string?>("--policy-version")
|
||||
{
|
||||
Description = "Policy revision to evaluate (defaults to current)."
|
||||
};
|
||||
var profileOption = new Option<string?>("--profile")
|
||||
{
|
||||
Description = "Advisory AI execution profile (default, fips-local, etc.)."
|
||||
};
|
||||
var sectionOption = new Option<string[]>("--section")
|
||||
{
|
||||
Description = "Preferred context sections to emphasise (repeatable).",
|
||||
Arity = ArgumentArity.ZeroOrMore
|
||||
};
|
||||
sectionOption.AllowMultipleArgumentsPerToken = true;
|
||||
|
||||
var forceRefreshOption = new Option<bool>("--force-refresh")
|
||||
{
|
||||
Description = "Bypass cached plan/output and recompute."
|
||||
};
|
||||
|
||||
var timeoutOption = new Option<int?>("--timeout")
|
||||
{
|
||||
Description = "Seconds to wait for generated output before timing out (0 = single attempt)."
|
||||
};
|
||||
timeoutOption.Arity = ArgumentArity.ZeroOrOne;
|
||||
|
||||
run.Add(advisoryKeyOption);
|
||||
run.Add(artifactIdOption);
|
||||
run.Add(artifactPurlOption);
|
||||
run.Add(policyVersionOption);
|
||||
run.Add(profileOption);
|
||||
run.Add(sectionOption);
|
||||
run.Add(forceRefreshOption);
|
||||
run.Add(timeoutOption);
|
||||
var run = new Command("run", "Generate Advisory AI output for the specified task.");
|
||||
run.Add(runTaskArgument);
|
||||
AddAdvisoryOptions(run, runOptions);
|
||||
|
||||
run.SetAction((parseResult, _) =>
|
||||
{
|
||||
var taskValue = parseResult.GetValue(taskArgument);
|
||||
var advisoryKey = parseResult.GetValue(advisoryKeyOption) ?? string.Empty;
|
||||
var artifactId = parseResult.GetValue(artifactIdOption);
|
||||
var artifactPurl = parseResult.GetValue(artifactPurlOption);
|
||||
var policyVersion = parseResult.GetValue(policyVersionOption);
|
||||
var profile = parseResult.GetValue(profileOption) ?? "default";
|
||||
var sections = parseResult.GetValue(sectionOption) ?? Array.Empty<string>();
|
||||
var forceRefresh = parseResult.GetValue(forceRefreshOption);
|
||||
var timeoutSeconds = parseResult.GetValue(timeoutOption) ?? 120;
|
||||
var taskValue = parseResult.GetValue(runTaskArgument);
|
||||
var advisoryKey = parseResult.GetValue(runOptions.AdvisoryKey) ?? string.Empty;
|
||||
var artifactId = parseResult.GetValue(runOptions.ArtifactId);
|
||||
var artifactPurl = parseResult.GetValue(runOptions.ArtifactPurl);
|
||||
var policyVersion = parseResult.GetValue(runOptions.PolicyVersion);
|
||||
var profile = parseResult.GetValue(runOptions.Profile) ?? "default";
|
||||
var sections = parseResult.GetValue(runOptions.Sections) ?? Array.Empty<string>();
|
||||
var forceRefresh = parseResult.GetValue(runOptions.ForceRefresh);
|
||||
var timeoutSeconds = parseResult.GetValue(runOptions.TimeoutSeconds) ?? 120;
|
||||
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(runOptions.Format));
|
||||
var outputPath = parseResult.GetValue(runOptions.Output);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
if (!Enum.TryParse<AdvisoryAiTaskType>(taskValue, ignoreCase: true, out var taskType))
|
||||
@@ -1239,17 +1196,164 @@ internal static class CommandFactory
|
||||
sections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
outputFormat,
|
||||
outputPath,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
var summarizeOptions = CreateAdvisoryOptions();
|
||||
var summarize = new Command("summarize", "Summarize an advisory with JSON/Markdown outputs and citations.");
|
||||
AddAdvisoryOptions(summarize, summarizeOptions);
|
||||
summarize.SetAction((parseResult, _) =>
|
||||
{
|
||||
var advisoryKey = parseResult.GetValue(summarizeOptions.AdvisoryKey) ?? string.Empty;
|
||||
var artifactId = parseResult.GetValue(summarizeOptions.ArtifactId);
|
||||
var artifactPurl = parseResult.GetValue(summarizeOptions.ArtifactPurl);
|
||||
var policyVersion = parseResult.GetValue(summarizeOptions.PolicyVersion);
|
||||
var profile = parseResult.GetValue(summarizeOptions.Profile) ?? "default";
|
||||
var sections = parseResult.GetValue(summarizeOptions.Sections) ?? Array.Empty<string>();
|
||||
var forceRefresh = parseResult.GetValue(summarizeOptions.ForceRefresh);
|
||||
var timeoutSeconds = parseResult.GetValue(summarizeOptions.TimeoutSeconds) ?? 120;
|
||||
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(summarizeOptions.Format));
|
||||
var outputPath = parseResult.GetValue(summarizeOptions.Output);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleAdviseRunAsync(
|
||||
services,
|
||||
AdvisoryAiTaskType.Summary,
|
||||
advisoryKey,
|
||||
artifactId,
|
||||
artifactPurl,
|
||||
policyVersion,
|
||||
profile,
|
||||
sections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
outputFormat,
|
||||
outputPath,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
advise.Add(run);
|
||||
advise.Add(summarize);
|
||||
return advise;
|
||||
}
|
||||
|
||||
private static AdvisoryCommandOptions CreateAdvisoryOptions()
|
||||
{
|
||||
var advisoryKey = new Option<string>("--advisory-key")
|
||||
{
|
||||
Description = "Advisory identifier to summarise (required).",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var artifactId = new Option<string?>("--artifact-id")
|
||||
{
|
||||
Description = "Optional artifact identifier to scope SBOM context."
|
||||
};
|
||||
|
||||
var artifactPurl = new Option<string?>("--artifact-purl")
|
||||
{
|
||||
Description = "Optional package URL to scope dependency context."
|
||||
};
|
||||
|
||||
var policyVersion = new Option<string?>("--policy-version")
|
||||
{
|
||||
Description = "Policy revision to evaluate (defaults to current)."
|
||||
};
|
||||
|
||||
var profile = new Option<string?>("--profile")
|
||||
{
|
||||
Description = "Advisory AI execution profile (default, fips-local, etc.)."
|
||||
};
|
||||
|
||||
var sections = new Option<string[]>("--section")
|
||||
{
|
||||
Description = "Preferred context sections to emphasise (repeatable).",
|
||||
Arity = ArgumentArity.ZeroOrMore
|
||||
};
|
||||
sections.AllowMultipleArgumentsPerToken = true;
|
||||
|
||||
var forceRefresh = new Option<bool>("--force-refresh")
|
||||
{
|
||||
Description = "Bypass cached plan/output and recompute."
|
||||
};
|
||||
|
||||
var timeoutSeconds = new Option<int?>("--timeout")
|
||||
{
|
||||
Description = "Seconds to wait for generated output before timing out (0 = single attempt)."
|
||||
};
|
||||
timeoutSeconds.Arity = ArgumentArity.ZeroOrOne;
|
||||
|
||||
var format = new Option<string?>("--format")
|
||||
{
|
||||
Description = "Output format: table (default), json, or markdown."
|
||||
};
|
||||
|
||||
var output = new Option<string?>("--output")
|
||||
{
|
||||
Description = "File path to write advisory output when using json/markdown formats."
|
||||
};
|
||||
|
||||
return new AdvisoryCommandOptions(
|
||||
advisoryKey,
|
||||
artifactId,
|
||||
artifactPurl,
|
||||
policyVersion,
|
||||
profile,
|
||||
sections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
format,
|
||||
output);
|
||||
}
|
||||
|
||||
private static void AddAdvisoryOptions(Command command, AdvisoryCommandOptions options)
|
||||
{
|
||||
command.Add(options.AdvisoryKey);
|
||||
command.Add(options.ArtifactId);
|
||||
command.Add(options.ArtifactPurl);
|
||||
command.Add(options.PolicyVersion);
|
||||
command.Add(options.Profile);
|
||||
command.Add(options.Sections);
|
||||
command.Add(options.ForceRefresh);
|
||||
command.Add(options.TimeoutSeconds);
|
||||
command.Add(options.Format);
|
||||
command.Add(options.Output);
|
||||
}
|
||||
|
||||
private static AdvisoryOutputFormat ParseAdvisoryOutputFormat(string? formatValue)
|
||||
{
|
||||
var normalized = string.IsNullOrWhiteSpace(formatValue)
|
||||
? "table"
|
||||
: formatValue!.Trim().ToLowerInvariant();
|
||||
|
||||
return normalized switch
|
||||
{
|
||||
"json" => AdvisoryOutputFormat.Json,
|
||||
"markdown" => AdvisoryOutputFormat.Markdown,
|
||||
"md" => AdvisoryOutputFormat.Markdown,
|
||||
_ => AdvisoryOutputFormat.Table
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record AdvisoryCommandOptions(
|
||||
Option<string> AdvisoryKey,
|
||||
Option<string?> ArtifactId,
|
||||
Option<string?> ArtifactPurl,
|
||||
Option<string?> PolicyVersion,
|
||||
Option<string?> Profile,
|
||||
Option<string[]> Sections,
|
||||
Option<bool> ForceRefresh,
|
||||
Option<int?> TimeoutSeconds,
|
||||
Option<string?> Format,
|
||||
Option<string?> Output);
|
||||
|
||||
private static Command BuildVulnCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var vuln = new Command("vuln", "Explore vulnerability observations and overlays.");
|
||||
{
|
||||
var vuln = new Command("vuln", "Explore vulnerability observations and overlays.");
|
||||
|
||||
var observations = new Command("observations", "List raw advisory observations for overlay consumers.");
|
||||
|
||||
|
||||
@@ -448,6 +448,8 @@ internal static class CommandHandlers
|
||||
IReadOnlyList<string> preferredSections,
|
||||
bool forceRefresh,
|
||||
int timeoutSeconds,
|
||||
AdvisoryOutputFormat outputFormat,
|
||||
string? outputPath,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -542,7 +544,14 @@ internal static class CommandHandlers
|
||||
activity?.SetTag("stellaops.cli.advisory.cache_hit", output.PlanFromCache);
|
||||
logger.LogInformation("Advisory output ready (cache key {CacheKey}).", output.CacheKey);
|
||||
|
||||
RenderAdvisoryOutput(output);
|
||||
var rendered = RenderAdvisoryOutput(output, outputFormat);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath) && rendered is not null)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(outputPath!);
|
||||
await File.WriteAllTextAsync(fullPath, rendered, cancellationToken).ConfigureAwait(false);
|
||||
logger.LogInformation("Advisory output written to {Path}.", fullPath);
|
||||
}
|
||||
|
||||
if (output.Guardrail.Blocked)
|
||||
{
|
||||
@@ -6326,7 +6335,113 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderAdvisoryOutput(AdvisoryPipelineOutputModel output)
|
||||
private static string? RenderAdvisoryOutput(AdvisoryPipelineOutputModel output, AdvisoryOutputFormat format)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
AdvisoryOutputFormat.Json => RenderAdvisoryOutputJson(output),
|
||||
AdvisoryOutputFormat.Markdown => RenderAdvisoryOutputMarkdown(output),
|
||||
_ => RenderAdvisoryOutputTable(output)
|
||||
};
|
||||
}
|
||||
|
||||
private static string RenderAdvisoryOutputJson(AdvisoryPipelineOutputModel output)
|
||||
{
|
||||
return JsonSerializer.Serialize(output, new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
}
|
||||
|
||||
private static string RenderAdvisoryOutputMarkdown(AdvisoryPipelineOutputModel output)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"# Advisory {output.TaskType} ({output.Profile})");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine($"- Cache Key: `{output.CacheKey}`");
|
||||
builder.AppendLine($"- Generated: {output.GeneratedAtUtc.ToString(\"O\", CultureInfo.InvariantCulture)}");
|
||||
builder.AppendLine($"- Plan From Cache: {(output.PlanFromCache ? \"yes\" : \"no\")}");
|
||||
builder.AppendLine($"- Guardrail Blocked: {(output.Guardrail.Blocked ? \"yes\" : \"no\")}");
|
||||
builder.AppendLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(output.Response))
|
||||
{
|
||||
builder.AppendLine("## Response");
|
||||
builder.AppendLine(output.Response.Trim());
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(output.Prompt))
|
||||
{
|
||||
builder.AppendLine("## Prompt (sanitized)");
|
||||
builder.AppendLine(output.Prompt.Trim());
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
if (output.Citations.Count > 0)
|
||||
{
|
||||
builder.AppendLine("## Citations");
|
||||
foreach (var citation in output.Citations.OrderBy(c => c.Index))
|
||||
{
|
||||
builder.AppendLine($"- [{citation.Index}] {citation.DocumentId} :: {citation.ChunkId}");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
if (output.Metadata.Count > 0)
|
||||
{
|
||||
builder.AppendLine("## Output Metadata");
|
||||
foreach (var entry in output.Metadata.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.AppendLine($"- **{entry.Key}**: {entry.Value}");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
if (output.Guardrail.Metadata.Count > 0)
|
||||
{
|
||||
builder.AppendLine("## Guardrail Metadata");
|
||||
foreach (var entry in output.Guardrail.Metadata.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.AppendLine($"- **{entry.Key}**: {entry.Value}");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
if (output.Guardrail.Violations.Count > 0)
|
||||
{
|
||||
builder.AppendLine("## Guardrail Violations");
|
||||
foreach (var violation in output.Guardrail.Violations)
|
||||
{
|
||||
builder.AppendLine($"- `{violation.Code}`: {violation.Message}");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.AppendLine("## Provenance");
|
||||
builder.AppendLine($"- Input Digest: `{output.Provenance.InputDigest}`");
|
||||
builder.AppendLine($"- Output Hash: `{output.Provenance.OutputHash}`");
|
||||
|
||||
if (output.Provenance.Signatures.Count > 0)
|
||||
{
|
||||
foreach (var signature in output.Provenance.Signatures)
|
||||
{
|
||||
builder.AppendLine($"- Signature: `{signature}`");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendLine("- Signature: none");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string? RenderAdvisoryOutputTable(AdvisoryPipelineOutputModel output)
|
||||
{
|
||||
var console = AnsiConsole.Console;
|
||||
|
||||
@@ -6428,6 +6543,8 @@ internal static class CommandHandlers
|
||||
provenance.AddRow("Signatures", signatures);
|
||||
|
||||
console.Write(provenance);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Table CreateKeyValueTable(string title, IReadOnlyDictionary<string, string> entries)
|
||||
|
||||
@@ -11,6 +11,13 @@ internal enum AdvisoryAiTaskType
|
||||
Remediation
|
||||
}
|
||||
|
||||
internal enum AdvisoryOutputFormat
|
||||
{
|
||||
Table,
|
||||
Json,
|
||||
Markdown
|
||||
}
|
||||
|
||||
internal sealed class AdvisoryPipelinePlanRequestModel
|
||||
{
|
||||
public AdvisoryAiTaskType TaskType { get; init; }
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
| Task ID | State | Notes |
|
||||
| --- | --- | --- |
|
||||
| `SCANNER-CLI-0001` | DONE (2025-11-12) | Ruby verbs now consume the persisted `RubyPackageInventory`, warn when inventories are missing, and docs/tests were refreshed per Sprint 138. |
|
||||
| `CLI-AIAI-31-001` | DOING (2025-11-22) | Building `stella advise summarize` with JSON/Markdown outputs and citation rendering (Sprint 0201 CLI I). |
|
||||
|
||||
@@ -749,6 +749,8 @@ public sealed class CommandHandlersTests
|
||||
new[] { "impact", "impact " },
|
||||
forceRefresh: false,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Table,
|
||||
outputPath: null,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
@@ -777,6 +779,104 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
using var tempDir = new TempDirectory();
|
||||
var outputPath = Path.Combine(tempDir.Path, "advisory.md");
|
||||
var testConsole = new TestConsole();
|
||||
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.Console = testConsole;
|
||||
|
||||
var planResponse = new AdvisoryPipelinePlanResponseModel
|
||||
{
|
||||
TaskType = AdvisoryAiTaskType.Summary.ToString(),
|
||||
CacheKey = "cache-markdown",
|
||||
PromptTemplate = "prompts/advisory/summary.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 = "Sanitized prompt",
|
||||
Response = "Rendered summary body.",
|
||||
Citations = new[]
|
||||
{
|
||||
new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-9", ChunkId = "chunk-9" }
|
||||
},
|
||||
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:markdown-in",
|
||||
OutputHash = "sha256:markdown-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);
|
||||
|
||||
await CommandHandlers.HandleAdviseRunAsync(
|
||||
provider,
|
||||
AdvisoryAiTaskType.Summary,
|
||||
"ADV-4",
|
||||
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("Citations", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("doc-9", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("chunk-9", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(File.Exists(outputPath));
|
||||
Assert.Contains("Rendered summary body", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.Contains("Citations", testConsole.Output, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_ReturnsGuardrailExitCodeOnBlock()
|
||||
{
|
||||
@@ -855,6 +955,8 @@ public sealed class CommandHandlersTests
|
||||
Array.Empty<string>(),
|
||||
forceRefresh: true,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Table,
|
||||
outputPath: null,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
@@ -913,6 +1015,8 @@ public sealed class CommandHandlersTests
|
||||
Array.Empty<string>(),
|
||||
forceRefresh: false,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Table,
|
||||
outputPath: null,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user