elsa-core/test/unit/Elsa.Diagnostics.ConsoleLogs.UnitTests/ConsoleLogsRegistrationTests.cs
Sipke Schoorstra 8e301d4e1e
[codex] Scope console logs to workflow instances (#7535)
* Avoid null endpoint DTO metadata in tests

* Enforce console logs hub read permission

* Remove unused console logs hub import

* Support mapped endpoint metadata in auth tests

* Reduce console log capture throughput impact

* Address Copilot console logs review

* Refactor task scheduling to support tenant-level background work and enhance logging functionality.

* Introduce ConsoleStreamHook for stdout/stderr tee and enhance logging validation. Adjust test cases and startup warnings for distributed lock provider usage.

* Refactor console logging pipeline with capture optimization and new ConsoleLogsHost; update tests accordingly.

* Add Ansi SGR parser for console logs and associated unit tests

* Remove ANSI color renderings and parsers; integrate ConsoleLogScopeAccessor for improved logging context with workflow instance ID support.

* Address console logs code quality feedback

* Address PR review feedback

* Preserve console logs extension points

* Stabilize console logs host lifecycle

* Address final automated review comments

* Tighten console log capture shutdown

* Address console log review feedback

* Address follow-up review feedback

* Cover final review feedback

* Avoid recursive console provider initialization

* Guard console host lease shutdown

* Preserve console log scope and provider lifetime

* Correlate console log scope fallback

* Tighten console scope correlation

* Expose host services during provider construction

* Redact ANSI-normalized console lines
2026-05-25 11:49:51 +02:00

213 lines
9.3 KiB
C#

using Elsa.Diagnostics.ConsoleLogs.Contracts;
using Elsa.Diagnostics.ConsoleLogs.Extensions;
using Elsa.Diagnostics.ConsoleLogs.Providers.InMemory;
using Elsa.Diagnostics.ConsoleLogs.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Elsa.Diagnostics.ConsoleLogs.UnitTests;
[Collection(ConsoleHostStateCollection.Name)]
public class ConsoleLogsRegistrationTests : IAsyncLifetime
{
public async Task InitializeAsync()
{
await ConsoleLogsHost.ShutdownAsync();
ConsoleStreamHook.Uninstall();
}
public async Task DisposeAsync()
{
await ConsoleLogsHost.ShutdownAsync();
ConsoleStreamHook.Uninstall();
}
[Fact]
public async Task AddConsoleLogsServices_ResolvesHostOwnedSingletons()
{
var services = new ServiceCollection();
services.AddConsoleLogsServices();
await using var serviceProvider = services.BuildServiceProvider();
Assert.IsType<InMemoryConsoleLogProvider>(serviceProvider.GetRequiredService<IConsoleLogProvider>());
Assert.IsType<ConsoleLogSourceRegistry>(serviceProvider.GetRequiredService<IConsoleLogSourceRegistry>());
Assert.IsType<ConsoleLogRedactor>(serviceProvider.GetRequiredService<IConsoleLogRedactor>());
Assert.Same(ConsoleLogsHost.ScopeAccessor, serviceProvider.GetRequiredService<ConsoleLogScopeAccessor>());
Assert.Contains(serviceProvider.GetServices<ILoggerProvider>(), x => ReferenceEquals(x, ConsoleLogsHost.ScopeAccessor));
Assert.Contains(serviceProvider.GetServices<IHostedService>(), x => x.GetType() == typeof(ConsoleLogsHostedService));
#pragma warning disable CS0618
Assert.IsType<ConsoleLogCaptureAdapter>(serviceProvider.GetRequiredService<IConsoleLogCapture>());
#pragma warning restore CS0618
// Same instance must be visible across separate shell containers — console output is process-wide.
var services2 = new ServiceCollection();
services2.AddConsoleLogsServices();
await using var serviceProvider2 = services2.BuildServiceProvider();
Assert.Same(serviceProvider.GetRequiredService<IConsoleLogProvider>(), serviceProvider2.GetRequiredService<IConsoleLogProvider>());
Assert.Same(serviceProvider.GetRequiredService<IConsoleLogSourceRegistry>(), serviceProvider2.GetRequiredService<IConsoleLogSourceRegistry>());
}
[Fact]
public async Task AddConsoleLogsServices_UsesCustomProviderForCapturePipeline()
{
var provider = new CustomProvider();
var services = new ServiceCollection();
services.AddSingleton<IConsoleLogProvider>(provider);
services.AddConsoleLogsServices();
await using var serviceProvider = services.BuildServiceProvider();
_ = serviceProvider.GetRequiredService<ConsoleLogScopeAccessor>();
foreach (var hostedService in serviceProvider.GetServices<IHostedService>())
await hostedService.StartAsync(CancellationToken.None);
Assert.Same(provider, serviceProvider.GetRequiredService<IConsoleLogProvider>());
Assert.Same(provider, ConsoleLogsHost.Provider);
foreach (var hostedService in serviceProvider.GetServices<IHostedService>())
await hostedService.StopAsync(CancellationToken.None);
}
[Fact]
public async Task AddConsoleLogsServices_UsesCustomProviderRegisteredAfterConsoleLogsServices()
{
var provider = new CustomProvider();
var services = new ServiceCollection();
services.AddConsoleLogsServices();
services.AddSingleton<IConsoleLogProvider>(provider);
await using var serviceProvider = services.BuildServiceProvider();
foreach (var hostedService in serviceProvider.GetServices<IHostedService>())
await hostedService.StartAsync(CancellationToken.None);
Assert.Same(provider, serviceProvider.GetRequiredService<IConsoleLogProvider>());
Assert.Same(provider, ConsoleLogsHost.Provider);
foreach (var hostedService in serviceProvider.GetServices<IHostedService>())
await hostedService.StopAsync(CancellationToken.None);
}
[Fact]
public async Task AddConsoleLogsServices_CreatesCustomProviderWithHostOwnedDependencies()
{
var services = new ServiceCollection();
services.AddConsoleLogsServices();
services.AddSingleton<IConsoleLogProvider, ProviderWithHostDependencies>();
await using var serviceProvider = services.BuildServiceProvider();
foreach (var hostedService in serviceProvider.GetServices<IHostedService>())
await hostedService.StartAsync(CancellationToken.None);
var provider = Assert.IsType<ProviderWithHostDependencies>(ConsoleLogsHost.Provider);
Assert.Same(serviceProvider.GetRequiredService<IConsoleLogProvider>(), provider);
Assert.Same(ConsoleLogsHost.Options, provider.Options);
Assert.Same(ConsoleLogsHost.SourceRegistry, provider.SourceRegistry);
Assert.Same(ConsoleLogsHost.Redactor, provider.Redactor);
Assert.Same(ConsoleLogsHost.Formatter, provider.Formatter);
Assert.Same(ConsoleLogsHost.ScopeAccessor, provider.ScopeAccessor);
foreach (var hostedService in serviceProvider.GetServices<IHostedService>())
await hostedService.StopAsync(CancellationToken.None);
}
[Fact]
public async Task ShutdownAsync_ResetsReferenceLeases()
{
ConsoleLogsHost.AddReference();
ConsoleLogsHost.EnsureInitialized();
await ConsoleLogsHost.ShutdownAsync();
ConsoleLogsHost.AddReference();
ConsoleLogsHost.EnsureInitialized();
await ConsoleLogsHost.ReleaseReferenceAsync();
var provider = new CustomProvider();
Assert.True(ConsoleLogsHost.ConfigureProvider((_, _) => provider));
Assert.Same(provider, ConsoleLogsHost.Provider);
}
[Fact]
public async Task AddConsoleLogsHost_RegistersHostedFlushService()
{
var services = new ServiceCollection();
services.AddConsoleLogsHost();
await using var serviceProvider = services.BuildServiceProvider();
Assert.Contains(serviceProvider.GetServices<IHostedService>(), x => x.GetType() == typeof(ConsoleLogsHostedService));
Assert.Contains(serviceProvider.GetServices<ILoggerProvider>(), x => ReferenceEquals(x, ConsoleLogsHost.ScopeAccessor));
}
[Fact]
public void AddConsoleLogsHost_AppliesConfigurationWhenCalledBeforeFirstAccess()
{
var services = new ServiceCollection();
services.AddConsoleLogsHost(options => options.RecentLogCapacity = 17);
Assert.Equal(17, ConsoleLogsHost.Options.Value.RecentLogCapacity);
}
[Fact]
public void AddConsoleLogsHost_DoesNotInitializeHostDuringRegistration()
{
var services = new ServiceCollection();
services.AddConsoleLogsHost();
Assert.True(ConsoleLogsHost.Configure(options => options.RecentLogCapacity = 23));
Assert.Equal(23, ConsoleLogsHost.Options.Value.RecentLogCapacity);
}
private sealed class CustomProvider : IConsoleLogProvider
{
public ValueTask PublishAsync(ConsoleLogLine line, CancellationToken cancellationToken = default) => ValueTask.CompletedTask;
public ValueTask<RecentConsoleLogsResult> GetRecentAsync(ConsoleLogFilter filter, CancellationToken cancellationToken = default) =>
ValueTask.FromResult(new RecentConsoleLogsResult([]));
public async IAsyncEnumerable<ConsoleLogStreamItem> SubscribeAsync(ConsoleLogFilter filter, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
yield break;
}
public ValueTask<IReadOnlyCollection<ConsoleLogSource>> ListSourcesAsync(CancellationToken cancellationToken = default) =>
ValueTask.FromResult<IReadOnlyCollection<ConsoleLogSource>>([]);
}
private sealed class ProviderWithHostDependencies(
IOptions<ConsoleLogsOptions> options,
IConsoleLogSourceRegistry sourceRegistry,
IConsoleLogRedactor redactor,
ConsoleLineFormatter formatter,
ConsoleLogScopeAccessor scopeAccessor) : IConsoleLogProvider
{
public IOptions<ConsoleLogsOptions> Options { get; } = options;
public IConsoleLogSourceRegistry SourceRegistry { get; } = sourceRegistry;
public IConsoleLogRedactor Redactor { get; } = redactor;
public ConsoleLineFormatter Formatter { get; } = formatter;
public ConsoleLogScopeAccessor ScopeAccessor { get; } = scopeAccessor;
public ValueTask PublishAsync(ConsoleLogLine line, CancellationToken cancellationToken = default) => ValueTask.CompletedTask;
public ValueTask<RecentConsoleLogsResult> GetRecentAsync(ConsoleLogFilter filter, CancellationToken cancellationToken = default) =>
ValueTask.FromResult(new RecentConsoleLogsResult([]));
public async IAsyncEnumerable<ConsoleLogStreamItem> SubscribeAsync(ConsoleLogFilter filter, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.CompletedTask;
yield break;
}
public ValueTask<IReadOnlyCollection<ConsoleLogSource>> ListSourcesAsync(CancellationToken cancellationToken = default) =>
ValueTask.FromResult<IReadOnlyCollection<ConsoleLogSource>>([]);
}
}