CLI entry trace warning capture and sprint status sync

This commit is contained in:
master
2025-11-06 19:18:34 +00:00
parent b190563d80
commit e5ffcd6535
8 changed files with 157 additions and 70 deletions

View File

@@ -197,30 +197,29 @@ public sealed class CommandHandlersTests
public async Task HandleScanEntryTraceAsync_WarnsWhenResultMissing()
{
var originalExit = Environment.ExitCode;
var console = new TestConsole();
var originalConsole = AnsiConsole.Console;
var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null));
var provider = BuildServiceProvider(backend);
AnsiConsole.Console = console;
var loggerProvider = new TestLoggerProvider();
var provider = BuildServiceProvider(backend, loggerProvider: loggerProvider);
try
{
await CommandHandlers.HandleScanEntryTraceAsync(
var output = await CaptureTestConsoleAsync(console => CommandHandlers.HandleScanEntryTraceAsync(
provider,
"scan-missing",
includeNdjson: false,
verbose: false,
cancellationToken: CancellationToken.None);
cancellationToken: CancellationToken.None));
Assert.Equal(1, Environment.ExitCode);
Assert.Equal("scan-missing", backend.LastEntryTraceScanId);
Assert.Contains("No EntryTrace data", console.Output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("No EntryTrace data", output.Combined, StringComparison.OrdinalIgnoreCase);
var warning = Assert.Single(loggerProvider.Entries.Where(entry => entry.Level == LogLevel.Warning));
Assert.Contains("No EntryTrace data", warning.Message, StringComparison.OrdinalIgnoreCase);
}
finally
{
Environment.ExitCode = originalExit;
AnsiConsole.Console = originalConsole;
}
}
@@ -2543,11 +2542,19 @@ spec:
IScannerInstaller? installer = null,
StellaOpsCliOptions? options = null,
IStellaOpsTokenClient? tokenClient = null,
IConcelierObservationsClient? concelierClient = null)
IConcelierObservationsClient? concelierClient = null,
ILoggerProvider? loggerProvider = null)
{
var services = new ServiceCollection();
services.AddSingleton(backend);
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)));
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Debug);
if (loggerProvider is not null)
{
builder.AddProvider(loggerProvider);
}
}));
services.AddSingleton(new VerbosityState());
services.AddHttpClient();
var resolvedOptions = options ?? new StellaOpsCliOptions
@@ -2571,6 +2578,70 @@ spec:
return services.BuildServiceProvider();
}
private static async Task<CapturedConsoleOutput> CaptureTestConsoleAsync(Func<TestConsole, Task> action)
{
var testConsole = new TestConsole();
var originalConsole = AnsiConsole.Console;
var originalOut = Console.Out;
using var writer = new StringWriter();
try
{
AnsiConsole.Console = testConsole;
Console.SetOut(writer);
await action(testConsole).ConfigureAwait(false);
return new CapturedConsoleOutput(testConsole.Output.ToString(), writer.ToString());
}
finally
{
Console.SetOut(originalOut);
AnsiConsole.Console = originalConsole;
}
}
private sealed record CapturedConsoleOutput(string SpectreBuffer, string PlainBuffer)
{
public string Combined => string.Concat(SpectreBuffer, PlainBuffer);
}
private sealed class TestLoggerProvider : ILoggerProvider
{
private readonly List<LogEntry> _entries = new();
public IReadOnlyList<LogEntry> Entries => _entries;
public ILogger CreateLogger(string categoryName) => new TestLogger(categoryName, _entries);
public void Dispose()
{
}
private sealed class TestLogger : ILogger
{
private readonly string _category;
private readonly List<LogEntry> _entries;
public TestLogger(string category, List<LogEntry> entries)
{
_category = category;
_entries = entries;
}
public IDisposable? BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
_entries.Add(new LogEntry(logLevel, _category, eventId, message, exception));
}
}
public sealed record LogEntry(LogLevel Level, string Category, EventId EventId, string Message, Exception? Exception);
}
private static IScannerExecutor CreateDefaultExecutor()
{
var tempResultsFile = Path.GetTempFileName();