ci: consolidate platform pipelines and test prerequisites on macOS support
PR and Push Build/Test / windows-build-and-test (push) Failing after 38m25s
PR and Push Build/Test / portable-build-and-test (push) Successful in 8m9s
PR and Push Build/Test / macos-native-full (push) Skipped

This commit is contained in:
dh committed 2026-10-06 09:02:19 +02:00
1 parent 1b19b08f2e
commit 728e66dd72
76 files changed
+7477 -239

No files matched your search

@@ -34,7 +34,7 @@ public sealed class AsrDiagnosticEndpointTests
public async Task NamedProfileEndpointPassesProfileToConfiguredProvider()
{
var pipelineFactory = new CapturingProfileSpeechRecognitionPipelineFactory();
await using var factory = new WebApplicationFactory<Program>()
await using var factory = new IsolatedApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
@@ -104,7 +104,7 @@ public sealed class AsrDiagnosticEndpointTests
public async Task SpeakerIdentityMergeDiagnosticEndpointUsesRecentDaysParameter()
{
var mergeService = new CapturingSpeakerIdentityMergeService();
await using var factory = new WebApplicationFactory<Program>()
await using var factory = new IsolatedApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
@@ -140,7 +140,7 @@ public sealed class AsrDiagnosticEndpointTests
Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize)
where TProvider : class, IStreamingTranscriptionProvider
{
return new WebApplicationFactory<Program>()
return new IsolatedApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
@@ -267,4 +267,3 @@ public sealed class AsrDiagnosticEndpointTests
}
}
}
@@ -107,14 +107,9 @@ public sealed class AzureSpeechStreamingTranscriptionProviderTests
speechConfig.GetProperty(PropertyId.SpeechServiceResponse_PostProcessingOption)));
}
[Fact]
[WindowsFact("per-user environment variables")]
public void ResolveKeyFallsBackToUserEnvironment()
{
if (!OperatingSystem.IsWindows())
{
return;
}
var keyEnv = $"MEETING_ASSISTANT_AZURE_KEY_{Guid.NewGuid():N}";
try
{
@@ -0,0 +1,166 @@
using System.Net;
using System.Net.Http.Json;
using MeetingAssistant.Notifications;
using MeetingAssistant.MacOs;
using MeetingAssistant.Workflow;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace MeetingAssistant.Tests;
#if !WINDOWS
public sealed class DesktopCommunicationEndpointTests
{
[Fact]
public async Task PendingRequestCanBeReadThroughDesktopEndpoint()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
var requests = factory.Services.GetRequiredService<DesktopUserRequestStore>();
var registration = requests.Add(new DesktopUserRequestSpec(
"agent-approval", "Project association", "Allow access to project Apollo?",
TimeSpan.FromMinutes(10), [new("yes", "Yes"), new("no", "No")]), CancellationToken.None);
using var response = await client.GetAsync("/desktop/requests");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var request = Assert.Single((await response.Content.ReadFromJsonAsync<DesktopUserRequest[]>())!);
Assert.Equal(registration.Id, request.Id);
Assert.Equal("Allow access to project Apollo?", request.Message);
Assert.Equal(new[] { "yes", "no" }, request.Actions.Select(action => action.Id));
}
[Fact]
public async Task OfferedHttpResponseDeliversOneDecisionAndRejectsDuplicate()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
var requests = factory.Services.GetRequiredService<DesktopUserRequestStore>();
var calls = 0;
var registration = requests.Add(new DesktopUserRequestSpec(
"agent-approval", "Approval", "Apply proposed change?", TimeSpan.FromMinutes(10),
[new("yes", "Yes"), new("no", "No")]), CancellationToken.None,
(_, _) => { Interlocked.Increment(ref calls); return Task.CompletedTask; });
var path = $"/desktop/requests/{registration.Id}/respond";
using var accepted = await client.PostAsJsonAsync(path, new { action = "yes" });
using var duplicate = await client.PostAsJsonAsync(path, new { action = "yes" });
Assert.Equal(HttpStatusCode.OK, accepted.StatusCode);
Assert.Equal(HttpStatusCode.Gone, duplicate.StatusCode);
Assert.Equal("yes", await registration.Decision);
Assert.Equal(1, calls);
Assert.Empty((await client.GetFromJsonAsync<DesktopUserRequest[]>("/desktop/requests"))!);
}
[Fact]
public async Task ApprovedWindowAndRepeatedSnapshotsKeepOneIndependentPrompt()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
var sessions = factory.Services.GetRequiredService<MacOsWorkflowRulesEditorSessions>();
sessions.OpenWithPrompt("Add the requested workflow scenario in full.");
using var response = await client.GetAsync("/desktop/agent-windows");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var window = Assert.Single((await response.Content.ReadFromJsonAsync<MacOsAgentWindowRequest[]>())!);
Assert.EndsWith($"?session={window.Id}", window.Url);
var path = $"/diagnostics/settings-and-logs/chat?session={window.Id}";
MacOsWorkflowRulesEditorChatResponse? first = null;
for (var attempt = 0; attempt < 100; attempt++)
{
first = await client.GetFromJsonAsync<MacOsWorkflowRulesEditorChatResponse>(path);
if (!first!.IsThinking && first.Messages.Count > 0) break;
await Task.Delay(10);
}
Assert.False(first!.IsThinking);
var second = await client.GetFromJsonAsync<MacOsWorkflowRulesEditorChatResponse>(path);
Assert.Equal("Add the requested workflow scenario in full.", first.Messages[0].Content);
Assert.Equal(first.Messages, second!.Messages);
Assert.Single(factory.Services.GetRequiredService<EndpointVerificationPipeline>().Calls);
Assert.Empty((await client.GetFromJsonAsync<MacOsWorkflowRulesEditorChatResponse>(
"/diagnostics/settings-and-logs/chat"))!.Messages);
}
[Fact]
public async Task UnknownChatSessionCannotSendIntoTheDefaultConversation()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
using var response = await client.PostAsJsonAsync(
"/diagnostics/settings-and-logs/chat?session=unknown-session", new { message = "Do not route this elsewhere" });
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Empty(factory.Services.GetRequiredService<EndpointVerificationPipeline>().Calls);
Assert.Empty((await client.GetFromJsonAsync<MacOsWorkflowRulesEditorChatResponse>(
"/diagnostics/settings-and-logs/chat"))!.Messages);
}
[MacOsFact]
public async Task MacOsCompositionWaitsForApprovalAndUsesTheDesktopDecision()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
using var origin = new CancellationTokenSource();
var approval = factory.Services.GetRequiredService<AgentApprovalService>();
var requests = factory.Services.GetRequiredService<DesktopUserRequestStore>();
var decision = approval.ConfirmAsync("Test approval", "Isolated composition check", origin.Token);
try
{
var pending = Assert.Single(requests.List());
Assert.False(decision.IsCompleted);
using var response = await client.PostAsJsonAsync(
$"/desktop/requests/{pending.Id}/respond", new { action = "no" });
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.False(await decision.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.IsType<MacOsMeetingInactivityPromptService>(
factory.Services.GetRequiredService<MeetingAssistant.Recording.IMeetingInactivityPromptService>());
}
finally
{
origin.Cancel();
}
}
internal static WebApplicationFactory<Program> CreateFactory()
{
return new IsolatedApplicationFactory().WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["MeetingAssistant:FunAsr:Backend:Enabled"] = "false",
["MeetingAssistant:CalendarRecordingPrompts:Enabled"] = "false"
}));
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IWorkflowRulesEditorChatPipeline>();
services.AddSingleton<EndpointVerificationPipeline>();
services.AddSingleton<IWorkflowRulesEditorChatPipeline>(provider =>
provider.GetRequiredService<EndpointVerificationPipeline>());
});
});
}
private sealed class EndpointVerificationPipeline : IWorkflowRulesEditorChatPipeline
{
public List<string> Calls { get; } = [];
public Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage, CancellationToken cancellationToken,
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
{
Calls.Add(userMessage);
return Task.FromResult(new WorkflowRulesEditorChatResult("Verification reply"));
}
}
}
#endif
@@ -0,0 +1,198 @@
using MeetingAssistant.Notifications;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class DesktopUserRequestTests
{
[Fact]
public async Task OfferedDecisionIsListedAndDeliveredOnce()
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
var registration = store.Add(Approval(), CancellationToken.None);
var request = Assert.Single(store.List());
Assert.Equal(registration.Id, request.Id);
Assert.Equal("Associate Alpha?", request.Title);
Assert.Equal(["yes", "no"], request.Actions.Select(action => action.Id));
Assert.False(registration.Decision.IsCompleted);
Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, "yes"));
Assert.Equal("yes", await registration.Decision);
Assert.Empty(store.List());
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(request.Id, "yes"));
}
[Fact]
public async Task UnsupportedActionLeavesDecisionPending()
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
var registration = store.Add(Approval(), CancellationToken.None);
Assert.Equal(DesktopUserRequestRespondResult.InvalidAction, await store.RespondAsync(registration.Id, "delete"));
Assert.Single(store.List());
Assert.False(registration.Decision.IsCompleted);
Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(registration.Id, "no"));
Assert.Equal("no", await registration.Decision);
}
[Fact]
public async Task CanceledOriginRemovesDecisionAndRejectsLateResponse()
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
using var origin = new CancellationTokenSource();
var invoked = false;
var registration = store.Add(Approval(), origin.Token, (_, _) =>
{
invoked = true;
return Task.CompletedTask;
});
await origin.CancelAsync();
Assert.Empty(store.List());
Assert.Null(await registration.Decision.WaitAsync(TimeSpan.FromSeconds(2)));
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(registration.Id, "yes"));
Assert.False(invoked);
}
[Fact]
public async Task DeadlineResolvesUnansweredDecisionWithoutPolling()
{
using var lifetime = new TestApplicationLifetime();
var clock = new ManualDesktopRequestTimeProvider();
using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
var registration = store.Add(Approval(), CancellationToken.None);
Assert.Equal(DateTimeOffset.Parse("2026-10-03T10:10:00Z"), Assert.Single(store.List()).ExpiresAt);
clock.Advance(TimeSpan.FromMinutes(10));
Assert.True(registration.Decision.IsCompleted);
Assert.Null(await registration.Decision);
Assert.Empty(store.List());
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(registration.Id, "yes"));
}
[Fact]
public async Task KindWithdrawalPreservesOtherQuestionsAndShutdownRemovesThem()
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
var approval = store.Add(Approval(), CancellationToken.None);
var warning = store.Add(Approval() with { Kind = "meeting-inactivity" }, CancellationToken.None);
store.DismissKind("meeting-inactivity");
Assert.Equal(approval.Id, Assert.Single(store.List()).Id);
Assert.Null(await warning.Decision.WaitAsync(TimeSpan.FromSeconds(2)));
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(warning.Id, "yes"));
lifetime.StopApplication();
Assert.Empty(store.List());
Assert.Null(await approval.Decision.WaitAsync(TimeSpan.FromSeconds(2)));
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(approval.Id, "yes"));
}
[Fact]
public async Task FailedActionIsConsumedAndReportedWithoutReplayingWork()
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
var registration = store.Add(Approval(), CancellationToken.None,
(_, _) => throw new InvalidOperationException("Test action could not finish."));
Assert.Equal(DesktopUserRequestRespondResult.Failed, await store.RespondAsync(registration.Id, "yes"));
Assert.Null(await registration.Decision);
Assert.Empty(store.List());
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(registration.Id, "yes"));
}
[Fact]
public async Task PendingListExcludesDeadlinesBeforeTimerDispatch()
{
using var lifetime = new TestApplicationLifetime();
var clock = new ManualDesktopRequestTimeProvider();
using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
var registration = store.Add(Approval(), CancellationToken.None);
clock.Advance(TimeSpan.FromMinutes(10), fireTimers: false);
Assert.Empty(store.List());
Assert.Null(await registration.Decision);
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(registration.Id, "yes"));
}
[Fact]
public async Task SimultaneousPresentationsUseTheOriginLifetimeAndPerformOneAction()
{
using var lifetime = new TestApplicationLifetime();
using var origin = new CancellationTokenSource();
using var store = CreateStore(lifetime);
var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var finish = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var actionCount = 0;
CancellationToken observedToken = default;
var registration = store.Add(Approval(), origin.Token, async (_, token) =>
{
Interlocked.Increment(ref actionCount);
observedToken = token;
started.TrySetResult();
await finish.Task.WaitAsync(token);
});
var firstResponse = store.RespondAsync(registration.Id, "yes");
await started.Task.WaitAsync(TimeSpan.FromSeconds(2));
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(registration.Id, "no"));
Assert.Equal(origin.Token, observedToken);
finish.SetResult();
Assert.Equal(DesktopUserRequestRespondResult.Accepted, await firstResponse);
Assert.Equal("yes", await registration.Decision);
Assert.Equal(1, actionCount);
}
private static DesktopUserRequestSpec Approval() => new(
"agent-approval", "Associate Alpha?", "The meeting discusses Alpha.", TimeSpan.FromMinutes(10),
[new("yes", "Yes"), new("no", "No")]);
private static DesktopUserRequestStore CreateStore(TestApplicationLifetime lifetime) => new(
TimeProvider.System, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
}
internal sealed class TestApplicationLifetime : IHostApplicationLifetime, IDisposable
{
private readonly CancellationTokenSource stopping = new();
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopping => stopping.Token;
public CancellationToken ApplicationStopped => stopping.Token;
public void StopApplication() => stopping.Cancel();
public void Dispose() => stopping.Dispose();
}
internal sealed class ManualDesktopRequestTimeProvider : TimeProvider
{
private DateTimeOffset now = DateTimeOffset.Parse("2026-10-03T10:00:00Z");
private readonly List<ManualTimer> timers = [];
public override DateTimeOffset GetUtcNow() => now;
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
var timer = new ManualTimer(this, callback, state);
timers.Add(timer);
timer.Change(dueTime, period);
return timer;
}
public void Advance(TimeSpan elapsed, bool fireTimers = true)
{
now += elapsed;
if (!fireTimers) return;
foreach (var timer in timers.ToArray()) timer.FireIfDue();
}
private sealed class ManualTimer(ManualDesktopRequestTimeProvider owner, TimerCallback callback, object? state) : ITimer
{
private DateTimeOffset due;
private bool disposed;
public bool Change(TimeSpan dueTime, TimeSpan period)
{
due = dueTime == Timeout.InfiniteTimeSpan ? DateTimeOffset.MaxValue : owner.now + dueTime;
return !disposed;
}
public void FireIfDue()
{
if (disposed || due > owner.now) return;
due = DateTimeOffset.MaxValue;
callback(state);
}
public void Dispose() => disposed = true;
public ValueTask DisposeAsync() { Dispose(); return ValueTask.CompletedTask; }
}
}
@@ -11,11 +11,11 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
namespace MeetingAssistant.Tests;
public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Program>>
public sealed class HealthEndpointTests : IClassFixture<IsolatedApplicationFactory>
{
private readonly WebApplicationFactory<Program> factory;
public HealthEndpointTests(WebApplicationFactory<Program> factory)
public HealthEndpointTests(IsolatedApplicationFactory factory)
{
this.factory = factory.WithWebHostBuilder(builder =>
{
@@ -55,6 +55,7 @@ public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Pr
Assert.Equal(HostOperatingSystem.DetectCurrent(), first);
}
#if !WINDOWS
[Fact]
public async Task NeutralBuildReportsSpeakerPlaybackUnavailable()
{
@@ -65,6 +66,7 @@ public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Pr
Assert.Contains("Windows build", result);
Assert.DoesNotContain("Queued", result);
}
#endif
[Fact]
public async Task ApplicationStartupDeletesStaleTemporaryRecordings()
@@ -75,6 +77,7 @@ public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Pr
{
services.RemoveAll<IRecordedAudioStore>();
services.AddSingleton<IRecordedAudioStore, StartupCleanupRecordedAudioStore>();
services.AddHostedService<TemporaryRecordingCleanupHostedService>();
});
});
using var client = cleanupFactory.CreateClient();
@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MeetingAssistant.Tests;
public sealed class IsolatedApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Endpoint tests retain production composition but opt into each background activity they exercise.
foreach (var descriptor in services.Where(descriptor =>
descriptor.ServiceType == typeof(IHostedService) &&
(descriptor.ImplementationType?.Assembly == typeof(Program).Assembly ||
descriptor.ImplementationFactory is not null)).ToArray())
{
services.Remove(descriptor);
}
});
}
}
@@ -1,8 +1,6 @@
using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.MacOs;
using MeetingAssistant.Workflow;
using MeetingAssistant.Notifications;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -10,33 +8,17 @@ namespace MeetingAssistant.Tests;
public sealed class MacOsDesktopControlManifestTests
{
[Fact]
public void AutomaticWorkflowChangeCannotSilentlyLoseItsPromptOnMacOs()
{
IWorkflowRulesEditorWindowService service = new MacOsWorkflowRulesEditorWindowService(
Options.Create(new MeetingAssistantOptions()));
Assert.Throws<PlatformNotSupportedException>(() => service.ShowWithPrompt("Update meeting rules"));
}
[Fact]
public async Task MacOsApplicationDeclinesUnavailableAutomaticApprovals()
{
using var factory = new WebApplicationFactory<Program>();
var approval = factory.Services.GetRequiredService<AgentApprovalService>();
Assert.False(await approval.ConfirmAsync("Test approval", "Do not apply a change", CancellationToken.None));
}
#if !WINDOWS
[Fact]
public void MacOsApplicationRegistersARealInteractiveAgentWindow()
{
using var factory = new WebApplicationFactory<Program>();
using var factory = new IsolatedApplicationFactory();
var service = factory.Services.GetRequiredService<IWorkflowRulesEditorWindowService>();
Assert.IsType<MacOsWorkflowRulesEditorWindowService>(service);
}
#endif
[Fact]
public void ManifestPreservesConfiguredWindowsEquivalentShortcutsAndControlEndpoints()
@@ -4,9 +4,13 @@ public sealed class MacOsFactAttribute : FactAttribute
{
public MacOsFactAttribute()
{
#if WINDOWS
Skip = "Requires the portable macOS target, native macOS helpers and Apple system tools; run net10.0 on macOS.";
#else
if (!OperatingSystem.IsMacOS())
{
Skip = "Requires native macOS helpers and Apple system tools; run on macOS.";
}
#endif
}
}
@@ -1,15 +1,14 @@
using MeetingAssistant.Recording;
using MeetingAssistant.Speakers;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Summary;
using MeetingAssistant.Transcription;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using System.Net.Http.Json;
@@ -164,7 +163,7 @@ public sealed class MacOsMeetingAudioSourceTests
microphonePcm: Pcm16(2_000),
systemPcm: Pcm16(10_000));
var speechPipelines = new CapturingSpeechRecognitionPipelineFactory();
var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
var factory = new IsolatedApplicationFactory().WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
{
@@ -184,10 +183,7 @@ public sealed class MacOsMeetingAudioSourceTests
});
builder.ConfigureTestServices(services =>
{
// The endpoint fixture supplies its own transcription pipeline and does not warm external pyannote models.
services.Remove(services.Single(descriptor =>
descriptor.ServiceType == typeof(IHostedService) &&
descriptor.ImplementationType == typeof(PyannoteDiarizationWarmupHostedService)));
services.AddHostedService<SpeakerIdentityDatabaseInitializer>();
services.RemoveAll<IMeetingAudioSource>();
services.RemoveAll<IMacOsAudioCaptureProcessFactory>();
PortableMeetingAudioRegistration.Add(services, isMacOs: true);
@@ -1,13 +1,14 @@
using System.Net.Http.Json;
using MeetingAssistant.Calendar;
using MeetingAssistant.MacOs;
using MeetingAssistant.Notifications;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Screenshots;
using MeetingAssistant.Transcription;
using MeetingAssistant.Workflow;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Hosting;
namespace MeetingAssistant.Tests;
@@ -18,6 +19,10 @@ public sealed class MacOsMeetingIntegrationTests
{
var macServices = new ServiceCollection().AddLogging();
var unsupportedServices = new ServiceCollection().AddLogging();
using var host = new HostBuilder().Build();
macServices.AddSingleton(TimeProvider.System);
macServices.AddSingleton(host.Services.GetRequiredService<IHostApplicationLifetime>());
macServices.AddSingleton<DesktopUserRequestStore>();
MacOsMeetingIntegrationRegistration.Add(macServices, isMacOs: true);
MacOsMeetingIntegrationRegistration.Add(unsupportedServices, isMacOs: false);
@@ -96,39 +101,20 @@ public sealed class MacOsMeetingIntegrationTests
}
[Fact]
public async Task MacOsPromptAndScreenshotUseNativeClientResults()
public async Task MacOsScreenshotAndCropUseNativeClientResults()
{
var screenshot = new ActiveWindowScreenshot([137, 80, 78, 71], "Meeting deck");
var client = new StubMacOsDesktopIntegrationClient(
screenshot: screenshot,
promptResponse: MeetingStartPromptResponse.Record);
IMeetingStartPromptService prompt = new MacOsMeetingStartPromptService(
client,
NullLogger<MacOsMeetingStartPromptService>.Instance);
IActiveWindowScreenshotCapture capture = client;
IScreenshotImageCropper cropper = client;
MeetingStartPromptResponse? callbackResponse = null;
var request = new MeetingStartPromptRequest(new CalendarMeeting(
"event-1",
"Project sync",
DateTimeOffset.Now,
DateTimeOffset.Now.AddHours(1)));
await prompt.ShowPromptAsync(
request,
(response, _) =>
{
callbackResponse = response;
return Task.CompletedTask;
},
CancellationToken.None);
var captured = await capture.CaptureAsync(CancellationToken.None);
var croppedPath = await cropper.SaveCroppedScreenshotAsync(
"meeting.png",
new ScreenshotCropCoordinates(1, 2, 3, 4),
CancellationToken.None);
Assert.Equal(MeetingStartPromptResponse.Record, callbackResponse);
Assert.Equal(screenshot, captured);
Assert.Equal("meeting.png.cropped.png", croppedPath);
}
@@ -136,7 +122,7 @@ public sealed class MacOsMeetingIntegrationTests
[MacOsFact]
public async Task MacOsCapabilityEndpointReportsEnabledRealProviders()
{
await using var factory = new WebApplicationFactory<Program>();
await using var factory = new IsolatedApplicationFactory();
using var client = factory.CreateClient();
var report = await client.GetFromJsonAsync<PlatformCapabilityReport>(
@@ -0,0 +1,129 @@
using MeetingAssistant.Calendar;
using MeetingAssistant.MacOs;
using MeetingAssistant.Notifications;
using MeetingAssistant.Recording;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class MacOsUserPromptTests
{
[Theory]
[InlineData("yes", true)]
[InlineData("no", false)]
public async Task ApprovalWaitsForAnExplicitDesktopDecision(string action, bool approved)
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
IAgentApprovalPromptService prompts = new MacOsAgentApprovalPromptService(store);
var decision = prompts.ConfirmAsync(new AgentApprovalPrompt("Project requested", "Associate Alpha?",
TimeSpan.FromMinutes(10)), CancellationToken.None);
Assert.False(decision.IsCompleted);
var request = Assert.Single(store.List());
Assert.Equal("agent-approval", request.Kind);
Assert.Equal(["yes", "no"], request.Actions.Select(item => item.Id));
Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, action));
Assert.Equal(approved, await decision);
Assert.Empty(store.List());
}
[Fact]
public async Task InactivityPromptReturnsImmediatelyAndOffersPauseWithOriginLifetime()
{
using var lifetime = new TestApplicationLifetime();
using var origin = new CancellationTokenSource();
var clock = new ManualDesktopRequestTimeProvider();
using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
IMeetingInactivityPromptService prompts = new MacOsMeetingInactivityPromptService(store);
MeetingInactivityPromptResponse? received = null;
CancellationToken receivedToken = default;
var showing = prompts.ShowStopPromptAsync(
new MeetingInactivityPromptRequest(TimeSpan.FromMinutes(2), clock.GetUtcNow(), 1, TimeSpan.FromMinutes(2)),
(response, token) =>
{
received = response;
receivedToken = token;
return Task.CompletedTask;
}, origin.Token);
Assert.True(showing.IsCompletedSuccessfully);
var request = Assert.Single(store.List());
Assert.Equal("meeting-inactivity", request.Kind);
Assert.Equal(["stop", "continue", "pause"], request.Actions.Select(item => item.Id));
Assert.Equal(DateTimeOffset.Parse("2026-10-03T10:01:00Z"), request.ExpiresAt);
Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, "pause"));
Assert.Equal(MeetingInactivityPromptResponse.Pause, received);
Assert.Equal(origin.Token, receivedToken);
}
[Theory]
[InlineData(true, "attach-metadata", MeetingStartPromptResponse.AttachMetadataToCurrentMeeting)]
[InlineData(false, "skip", MeetingStartPromptResponse.Skip)]
public async Task CalendarPromptReturnsImmediatelyAndPreservesOfferedActions(bool canAttach, string action, MeetingStartPromptResponse expected)
{
using var lifetime = new TestApplicationLifetime();
var clock = new ManualDesktopRequestTimeProvider();
using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
IMeetingStartPromptService prompts = new MacOsMeetingStartPromptService(store, clock);
var meeting = new CalendarMeeting("event-1", "Alpha planning", clock.GetUtcNow(), clock.GetUtcNow().AddMinutes(2));
MeetingStartPromptResponse? received = null;
var showing = prompts.ShowPromptAsync(new MeetingStartPromptRequest(meeting, canAttach),
(response, _) => { received = response; return Task.CompletedTask; }, CancellationToken.None);
Assert.True(showing.IsCompletedSuccessfully);
var request = Assert.Single(store.List());
Assert.Equal("meeting-start", request.Kind);
Assert.Contains("Alpha planning", request.Message);
Assert.Equal(DateTimeOffset.Parse("2026-10-03T10:02:00Z"), request.ExpiresAt);
Assert.Equal(canAttach, request.Actions.Any(item => item.Id == "attach-metadata"));
Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, action));
Assert.Equal(expected, received);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ApprovalExpirationOrOriginCancellationNeverGrantsConsent(bool cancelOrigin)
{
using var lifetime = new TestApplicationLifetime();
using var origin = new CancellationTokenSource();
var clock = new ManualDesktopRequestTimeProvider();
using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
IAgentApprovalPromptService prompts = new MacOsAgentApprovalPromptService(store);
var decision = prompts.ConfirmAsync(new AgentApprovalPrompt("Workflow requested", "Update matching rule?",
TimeSpan.FromMinutes(10)), origin.Token);
var request = Assert.Single(store.List());
if (cancelOrigin)
{
await origin.CancelAsync();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => decision);
}
else
{
clock.Advance(TimeSpan.FromMinutes(10));
Assert.False(await decision);
}
Assert.Empty(store.List());
Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(request.Id, "yes"));
}
[Fact]
public async Task DismissingInactivityLeavesUnrelatedApprovalPending()
{
using var lifetime = new TestApplicationLifetime();
using var store = CreateStore(lifetime);
IAgentApprovalPromptService approvals = new MacOsAgentApprovalPromptService(store);
var decision = approvals.ConfirmAsync(new AgentApprovalPrompt("Project requested", "Associate Alpha?",
TimeSpan.FromMinutes(10)), CancellationToken.None);
var approval = Assert.Single(store.List());
IMeetingInactivityPromptService inactivity = new MacOsMeetingInactivityPromptService(store);
await inactivity.ShowStopPromptAsync(new MeetingInactivityPromptRequest(TimeSpan.FromMinutes(2),
DateTimeOffset.UtcNow, 1, TimeSpan.FromMinutes(2)), (_, _) => Task.CompletedTask, CancellationToken.None);
Assert.Equal(2, store.List().Count);
await inactivity.DismissAllAsync(CancellationToken.None);
Assert.Equal(approval.Id, Assert.Single(store.List()).Id);
await store.RespondAsync(approval.Id, "no");
Assert.False(await decision);
}
private static DesktopUserRequestStore CreateStore(TestApplicationLifetime lifetime) => new(
TimeProvider.System, lifetime, NullLogger<DesktopUserRequestStore>.Instance);
}
@@ -0,0 +1,222 @@
using MeetingAssistant.MacOs;
using MeetingAssistant.Workflow;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace MeetingAssistant.Tests;
public sealed class MacOsWorkflowRulesEditorSessionTests
{
[Fact]
public async Task ApprovedWorkflowStartsOnceInOwnSessionWithoutReplacingDefaultConversation()
{
var pipeline = new ReplyPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
Assert.True(sessions.TryGetSession(null, out var normal));
await normal.SendAsync("My existing conversation", CancellationToken.None);
const string prompt = "Given Alpha in the title,\nwhen metadata arrives, then associate project Alpha.";
sessions.OpenWithPrompt(prompt);
var window = Assert.Single(sessions.GetAgentWindows());
Assert.EndsWith($"/diagnostics/settings-and-logs?session={window.Id}", window.Url);
Assert.True(sessions.TryGetSession(window.Id, out var requested));
var snapshot = await WaitForCompletionAsync(requested);
Assert.Equal(
[new MacOsWorkflowRulesEditorMessage("user", prompt), new MacOsWorkflowRulesEditorMessage("agent", "Agent response.")],
snapshot.Messages);
for (var index = 0; index < 3; index++)
{
Assert.Equal(snapshot.Messages, requested.GetSnapshot().Messages);
Assert.False(requested.GetSnapshot().IsThinking);
Assert.Single(sessions.GetAgentWindows());
}
Assert.Equal(
[new MacOsWorkflowRulesEditorMessage("user", "My existing conversation"), new MacOsWorkflowRulesEditorMessage("agent", "Agent response.")],
normal.GetSnapshot().Messages);
Assert.NotSame(normal, requested);
}
[Fact]
public async Task ClosingAResponseConnectionKeepsAcceptedChatWorkingAndSnapshotsRemainReadable()
{
var pipeline = new BlockingPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
Assert.True(sessions.TryGetSession(null, out var session));
using var responseConnection = new CancellationTokenSource();
var response = session.SendAsync("Continue the approved change", responseConnection.Token);
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var pending = session.GetSnapshot();
Assert.True(pending.IsThinking);
Assert.Equal([new MacOsWorkflowRulesEditorMessage("user", "Continue the approved change")], pending.Messages);
responseConnection.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => response);
Assert.True(session.GetSnapshot().IsThinking);
pipeline.Release.TrySetResult();
var completed = await WaitForCompletionAsync(session);
Assert.False(completed.IsThinking);
Assert.Equal("Agent response.", completed.Messages.Last().Content);
Assert.Single(pending.Messages);
Assert.True(pending.IsThinking);
}
[Fact]
public async Task ApprovedWorkflowWindowOpensWithoutWaitingForItsAgentResponse()
{
var pipeline = new BlockingPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
IWorkflowRulesEditorWindowService editor = new MacOsWorkflowRulesEditorWindowService(
Options.Create(new MeetingAssistantOptions()), () => sessions);
try
{
await Task.Run(() => editor.ShowWithPrompt("Apply the approved workflow scenario."))
.WaitAsync(TimeSpan.FromSeconds(2));
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var window = Assert.Single(sessions.GetAgentWindows());
Assert.True(sessions.TryGetSession(window.Id, out var session));
Assert.True(session.GetSnapshot().IsThinking);
}
finally
{
pipeline.Release.TrySetResult();
}
}
[Fact]
public void ConstructingTheWindowServiceDoesNotStartOrResolveAnAgentConversation()
{
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(new ReplyPipeline(), lifetime);
var resolutions = 0;
IWorkflowRulesEditorWindowService editor = new MacOsWorkflowRulesEditorWindowService(
Options.Create(new MeetingAssistantOptions()), () =>
{
resolutions++;
return sessions;
});
Assert.Equal(0, resolutions);
Assert.Empty(sessions.GetAgentWindows());
editor.ShowWithPrompt("An explicitly approved scenario.");
Assert.Equal(1, resolutions);
Assert.Single(sessions.GetAgentWindows());
}
[Fact]
public async Task TwoApprovedWorkflowsAndTheirFollowUpsRemainIndependent()
{
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(new ReplyPipeline(), lifetime);
sessions.OpenWithPrompt("First approved scenario.");
sessions.OpenWithPrompt("Second approved scenario.");
var windows = sessions.GetAgentWindows();
Assert.Equal(2, windows.Count);
Assert.Equal(2, windows.Select(window => window.Id).Distinct().Count());
var conversations = new List<MacOsWorkflowRulesEditorSession>();
foreach (var window in windows)
{
Assert.True(sessions.TryGetSession(window.Id, out var conversation));
conversations.Add(conversation);
}
var snapshots = await Task.WhenAll(conversations.Select(WaitForCompletionAsync));
Assert.Equal(
["First approved scenario.", "Second approved scenario."],
snapshots.Select(snapshot => snapshot.Messages[0].Content).Order());
var updated = await conversations[0].SendAsync("A follow-up in just this conversation.", CancellationToken.None);
Assert.Equal(4, updated.Messages.Count);
Assert.Equal(2, conversations[1].GetSnapshot().Messages.Count);
Assert.True(sessions.TryGetSession(null, out var normal));
Assert.Empty(normal.GetSnapshot().Messages);
}
[Fact]
public async Task ApplicationShutdownCancelsBackgroundConversationAndPreventsNewWindows()
{
var pipeline = new BlockingPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
sessions.OpenWithPrompt("Apply the approved scenario.");
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var window = Assert.Single(sessions.GetAgentWindows());
Assert.True(sessions.TryGetSession(window.Id, out var conversation));
lifetime.StopApplication();
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(2));
while (conversation.GetSnapshot().IsThinking) await Task.Delay(10, deadline.Token);
Assert.Empty(sessions.GetAgentWindows());
Assert.Single(conversation.GetSnapshot().Messages);
Assert.ThrowsAny<OperationCanceledException>(() => sessions.OpenWithPrompt("A stale approval."));
}
private static MacOsWorkflowRulesEditorSessions CreateSessions(
IWorkflowRulesEditorChatPipeline pipeline,
IHostApplicationLifetime lifetime) => new(
pipeline,
Options.Create(new MeetingAssistantOptions()),
lifetime,
NullLogger<MacOsWorkflowRulesEditorSessions>.Instance);
private static async Task<MacOsWorkflowRulesEditorChatResponse> WaitForCompletionAsync(
MacOsWorkflowRulesEditorSession session)
{
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(2));
while (true)
{
var snapshot = session.GetSnapshot();
if (snapshot.Messages.Count >= 2 && !snapshot.IsThinking) return snapshot;
await Task.Delay(10, deadline.Token);
}
}
private sealed class ReplyPipeline : IWorkflowRulesEditorChatPipeline
{
public Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null) =>
Task.FromResult(new WorkflowRulesEditorChatResult("Agent response."));
}
private sealed class BlockingPipeline : IWorkflowRulesEditorChatPipeline
{
public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource Release { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public async Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
{
Started.TrySetResult();
await Release.Task.WaitAsync(cancellationToken);
return new WorkflowRulesEditorChatResult("Agent response.");
}
}
private sealed class TestApplicationLifetime : IHostApplicationLifetime, IDisposable
{
private readonly CancellationTokenSource stopping = new();
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopping => stopping.Token;
public CancellationToken ApplicationStopped => stopping.Token;
public void StopApplication() => stopping.Cancel();
public void Dispose()
{
stopping.Cancel();
stopping.Dispose();
}
}
}
@@ -1,10 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net10.0;net10.0-windows10.0.19041.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<DefineConstants>$(DefineConstants);PLATFORM_TEST_VERIFIER_LIBRARY</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<PlatformTarget>x64</PlatformTarget>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
@@ -19,8 +25,15 @@
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<!-- The desktop SDK omits these from its default implicit usings. -->
<Using Include="System.IO" />
<Using Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MeetingAssistant\MeetingAssistant.csproj" />
<ProjectReference Include="..\MeetingAssistant\MeetingAssistant.csproj" SetTargetFramework="TargetFramework=$(TargetFramework)" />
<Compile Include="..\tools\VerifyPlatformTests.cs" Link="PlatformTestVerification.cs" />
</ItemGroup>
<ItemGroup>
@@ -1,7 +1,6 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
namespace MeetingAssistant.Tests;
@@ -17,7 +16,7 @@ public sealed class MeetingWorkflowDiagnosticEndpointTests
var firstRulesPath = Path.Combine(root, "first-rules.yaml");
var secondRulesPath = Path.Combine(root, "second-rules.yaml");
await File.WriteAllTextAsync(configPath, CreateConfigJson(firstRulesPath));
await using var factory = new WebApplicationFactory<Program>()
await using var factory = new IsolatedApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
@@ -0,0 +1,377 @@
using System.Xml.Linq;
namespace MeetingAssistant.Tests;
// These are synthetic verifier fixtures, not execution evidence from a Windows host.
public sealed class PlatformTestVerificationTests
{
private const string Guard = "MeetingAssistant.Tests.TestTargetCompositionTests.TestAndApplicationTargetsUseMatchingComposition";
private const string UserEnvironment = "MeetingAssistant.Tests.AzureSpeechStreamingTranscriptionProviderTests.ResolveKeyFallsBackToUserEnvironment";
private const string Glyphs = "MeetingAssistant.Tests.TaskbarIconTests.TaskbarIconGlyphsAreVisuallyCentered";
private static readonly XNamespace Trx = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
private static readonly string[] OutlookCases =
[
"MeetingAssistant.Tests.OutlookClassicMeetingMetadataProviderTests.ExtractAgendaStopsBeforeTeamsJoinInformation",
"MeetingAssistant.Tests.OutlookClassicMeetingMetadataProviderTests.NormalizeAttendeesDeduplicatesOrganizerAndRecipientEmail",
"MeetingAssistant.Tests.OutlookClassicMeetingMetadataProviderTests.SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: \"Canceled: Project sync\")",
"MeetingAssistant.Tests.OutlookClassicMeetingMetadataProviderTests.SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: \"Cancelled: Project sync\")",
"MeetingAssistant.Tests.OutlookClassicMeetingMetadataProviderTests.SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: \"Abgesagt: Project sync\")"
];
private static readonly string[] WindowsCropCases =
[
"MeetingAssistant.Tests.WindowsScreenshotImageCropperTests.WindowsCropUsesOcrPixelCoordinates",
"MeetingAssistant.Tests.WindowsScreenshotImageCropperTests.WindowsCropPreservesExpectedPixelsAtNonzeroOrigin",
"MeetingAssistant.Tests.WindowsScreenshotImageCropperTests.WindowsCropRejectsInvalidBounds"
];
[Fact]
public void RejectsEmptyExecutionRecordsInsteadOfAcceptingSummaryCounts()
{
var trx = XDocument.Parse("""
<TestRun xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Results />
<ResultSummary outcome="Completed"><Counters total="602" passed="602" failed="0" notExecuted="0" /></ResultSummary>
</TestRun>
""");
var failure = Assert.Throws<InvalidDataException>(() =>
PlatformTestVerification.Verify("portable", "MeetingAssistant.Tests.Example.Passes", trx));
Assert.Contains("execution records", failure.Message);
}
[Fact]
public void DerivesCurrentTotalsFromExecutionRecords()
{
var (discovery, trx) = PortableFixture();
var result = PlatformTestVerification.Verify("portable", discovery, trx);
Assert.Equal(3, result.Total);
Assert.Equal(1, result.Passed);
Assert.Equal(2, result.Skipped);
Assert.Contains("Totals: 3 discovered, 3 results, 1 passed, 2 skipped, 0 failed.", result.Lines);
Assert.Contains("Windows crop cases: excluded from portable target (3 Windows-only native cases).", result.Lines);
}
[Theory]
[InlineData("Passed", "")]
[InlineData("Passed", "platform-assurance: target=windows; host=linux")]
[InlineData("Passed", "platform-assurance: target=portable; host=unknown")]
[InlineData("NotExecuted", "platform-assurance: target=portable; host=linux")]
[InlineData("Passed", "platform-assurance: target=portable; host=linux\nplatform-assurance: target=portable; host=windows")]
public void RequiresMatchingPassedCompositionGuard(string outcome, string marker)
{
var (discovery, trx) = PortableFixture();
var guard = trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == Guard);
guard.SetAttributeValue("outcome", outcome);
guard.Descendants(Trx + "StdOut").Single().Value = marker;
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("portable", discovery, trx));
Assert.Contains("composition guard", failure.Message);
}
[Theory]
[InlineData(UserEnvironment, "linux", "Passed", "")]
[InlineData(Glyphs, "macos", "Passed", "")]
[InlineData(UserEnvironment, "linux", "NotExecuted", "")]
[InlineData(Glyphs, "macos", "NotExecuted", "No explanation")]
[InlineData(UserEnvironment, "windows", "NotExecuted", "Requires Windows per-user environment variables.")]
[InlineData(Glyphs, "windows", "NotExecuted", "Requires Windows rendering.")]
public void RequiresCompatibleExecutionOrReasonedWindowsSkip(string name, string host, string outcome, string reason)
{
var (discovery, trx) = PortableFixture();
var guard = trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == Guard);
guard.Descendants(Trx + "StdOut").Single().Value = $"platform-assurance: target=portable; host={host}";
if (host == "windows")
{
foreach (var runtimeTest in trx.Descendants(Trx + "UnitTestResult").Where(record => (string?)record.Attribute("testName") != Guard))
{
runtimeTest.SetAttributeValue("outcome", "Passed");
}
}
var record = trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == name);
record.SetAttributeValue("outcome", outcome);
record.Descendants(Trx + "StdOut").Single().Value = reason;
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("portable", discovery, trx));
Assert.Contains(name, failure.Message);
}
[Fact]
public void ReportsWindowsSkipNamesAndReasonsFromTrxErrorInfo()
{
var (discovery, trx) = PortableFixture();
foreach (var record in trx.Descendants(Trx + "UnitTestResult").Where(record => (string?)record.Attribute("outcome") == "NotExecuted"))
{
var output = record.Element(Trx + "Output")!;
var reason = output.Element(Trx + "StdOut")!.Value;
output.ReplaceNodes(new XElement(Trx + "ErrorInfo", new XElement(Trx + "Message", reason)));
}
var result = PlatformTestVerification.Verify("portable", discovery, trx);
Assert.Contains($"Skipped: {UserEnvironment} — Requires Windows per-user environment variables; run on Windows.", result.Lines);
Assert.Contains($"Skipped: {Glyphs} — Requires Windows System.Drawing bitmap rendering; run on Windows.", result.Lines);
}
[Fact]
public void WindowsEvidenceNamesAllPassedOutlookAndNativeCropCases()
{
var (discovery, trx) = WindowsFixture();
var result = PlatformTestVerification.Verify("windows", discovery, trx);
Assert.Equal(11, result.Total);
Assert.Equal(11, result.Passed);
Assert.Equal(0, result.Skipped);
foreach (var name in OutlookCases)
{
Assert.Contains($"Passed: {name}", result.Lines);
}
foreach (var name in WindowsCropCases)
{
Assert.Contains($"Passed: {name}", result.Lines);
}
Assert.Contains("Test artifacts only; full Windows resource/PRI builds and native operational acceptance require separate evidence.", result.Lines);
}
[Theory]
[InlineData(0, "both")]
[InlineData(1, "both")]
[InlineData(2, "both")]
[InlineData(0, "discovery")]
[InlineData(1, "discovery")]
[InlineData(2, "discovery")]
[InlineData(0, "execution")]
[InlineData(1, "execution")]
[InlineData(2, "execution")]
public void RequiresDiscoveryAndExecutionOfEachNativeWindowsCropCase(int missingCase, string missingArtifact)
{
var (discovery, trx) = WindowsFixture();
var name = WindowsCropCases[missingCase];
if (missingArtifact is "discovery" or "both")
{
discovery = discovery.Replace(name, "", StringComparison.Ordinal);
}
if (missingArtifact is "execution" or "both")
{
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == name).Remove();
var counters = trx.Descendants(Trx + "Counters").Single();
counters.SetAttributeValue("total", 10);
counters.SetAttributeValue("executed", 10);
counters.SetAttributeValue("passed", 10);
}
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("windows", discovery, trx));
Assert.Contains(name, failure.Message);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
public void ReasonedWindowsCropSkipsDoNotCountAsNativeExecutionEvidence(int skippedCase)
{
var (discovery, trx) = WindowsFixture();
var name = WindowsCropCases[skippedCase];
var record = trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == name);
record.SetAttributeValue("outcome", "NotExecuted");
record.Descendants(Trx + "StdOut").Single().Value = "Requires Windows System.Drawing bitmap rendering.";
var counters = trx.Descendants(Trx + "Counters").Single();
counters.SetAttributeValue("executed", 10);
counters.SetAttributeValue("passed", 10);
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("windows", discovery, trx));
Assert.Contains(name, failure.Message);
Assert.Contains("Passed", failure.Message);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void RequiresEachExistingOutlookCase(int missingCase)
{
var (discovery, trx) = WindowsFixture();
var name = OutlookCases[missingCase];
discovery = discovery.Replace(name, "", StringComparison.Ordinal);
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == name).Remove();
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("windows", discovery, trx));
Assert.Contains(name, failure.Message);
}
[Theory]
[InlineData("NotExecuted")]
[InlineData("Failed")]
public void RequiresExecutedPassedOutlookAssertions(string outcome)
{
var (discovery, trx) = WindowsFixture();
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == OutlookCases[0])
.SetAttributeValue("outcome", outcome);
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("windows", discovery, trx));
Assert.Contains(OutlookCases[0], failure.Message);
}
[Fact]
public void PortableEvidenceRejectsOutlookCasesEvenWithWindowsRuntime()
{
var (discovery, trx) = WindowsFixture();
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == Guard)
.Descendants(Trx + "StdOut").Single().Value = "platform-assurance: target=portable; host=windows";
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("portable", discovery, trx));
Assert.Contains("Outlook", failure.Message);
}
[Theory]
[InlineData("linux")]
[InlineData("windows")]
public void PortableEvidenceRejectsWindowsCropClassRegardlessOfRuntimeHost(string host)
{
const string cropCase = "MeetingAssistant.Tests.WindowsScreenshotImageCropperTests.AdditionalNativeCropCase";
var (discovery, trx) = PortableFixture();
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == Guard)
.Descendants(Trx + "StdOut").Single().Value = $"platform-assurance: target=portable; host={host}";
if (host == "windows")
{
foreach (var record in trx.Descendants(Trx + "UnitTestResult"))
{
record.SetAttributeValue("outcome", "Passed");
}
}
discovery += '\n' + cropCase;
trx.Root!.Element(Trx + "Results")!.Add(Result(cropCase, "Passed", ""));
var counters = trx.Descendants(Trx + "Counters").Single();
counters.SetAttributeValue("total", 4);
counters.SetAttributeValue("executed", host == "windows" ? 4 : 2);
counters.SetAttributeValue("passed", host == "windows" ? 4 : 2);
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("portable", discovery, trx));
Assert.Contains("Windows crop", failure.Message);
}
[Theory]
[InlineData("duplicate discovery")]
[InlineData("duplicate execution")]
[InlineData("missing execution")]
[InlineData("unlisted execution")]
[InlineData("failed execution")]
[InlineData("unrecognized outcome")]
[InlineData("empty discovery")]
public void RejectsIncompleteDuplicateOrFailedArtifactSets(string corruption)
{
const string example = "MeetingAssistant.Tests.Example.Passes";
var (discovery, trx) = PortableFixture();
var record = Result(example, "Passed", "");
trx.Root!.Element(Trx + "Results")!.Add(record);
discovery += '\n' + example;
var counters = trx.Descendants(Trx + "Counters").Single();
counters.SetAttributeValue("total", 4);
counters.SetAttributeValue("executed", 2);
counters.SetAttributeValue("passed", 2);
switch (corruption)
{
case "duplicate discovery": discovery += '\n' + example; break;
case "duplicate execution": record.AddAfterSelf(new XElement(record)); break;
case "missing execution": record.Remove(); break;
case "unlisted execution": discovery = discovery.Replace(example, "", StringComparison.Ordinal); break;
case "failed execution": record.SetAttributeValue("outcome", "Failed"); break;
case "unrecognized outcome": record.SetAttributeValue("outcome", "Aborted"); break;
case "empty discovery": discovery = ""; break;
}
Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("portable", discovery, trx));
}
[Theory]
[InlineData("total", "602")]
[InlineData("executed", "602")]
[InlineData("passed", "602")]
[InlineData("failed", "1")]
[InlineData("total", null)]
[InlineData("total", "invalid")]
[InlineData("aborted", "1")]
public void RejectsInconsistentSummaryCounters(string counter, string? value)
{
var (discovery, trx) = PortableFixture();
trx.Descendants(Trx + "Counters").Single().SetAttributeValue(counter, value);
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("portable", discovery, trx));
Assert.Contains("summary", failure.Message);
}
[Fact]
public void WindowsTargetRequiresWindowsRuntimeEvidence()
{
var (discovery, trx) = WindowsFixture();
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == Guard)
.Descendants(Trx + "StdOut").Single().Value = "platform-assurance: target=windows; host=linux";
foreach (var record in trx.Descendants(Trx + "UnitTestResult")
.Where(record => (string?)record.Attribute("testName") is UserEnvironment or Glyphs))
{
record.SetAttributeValue("outcome", "NotExecuted");
record.Descendants(Trx + "StdOut").Single().Value = "Requires Windows runtime.";
}
var counters = trx.Descendants(Trx + "Counters").Single();
counters.SetAttributeValue("executed", 9);
counters.SetAttributeValue("passed", 9);
var failure = Assert.Throws<InvalidDataException>(() => PlatformTestVerification.Verify("windows", discovery, trx));
Assert.Contains("Windows runtime", failure.Message);
}
private static (string Discovery, XDocument Results) WindowsFixture()
{
var (discovery, trx) = PortableFixture();
foreach (var record in trx.Descendants(Trx + "UnitTestResult"))
{
record.SetAttributeValue("outcome", "Passed");
record.Descendants(Trx + "StdOut").Single().Value = "";
}
trx.Descendants(Trx + "UnitTestResult").Single(record => (string?)record.Attribute("testName") == Guard)
.Descendants(Trx + "StdOut").Single().Value = "platform-assurance: target=windows; host=windows";
trx.Root!.Element(Trx + "Results")!.Add(OutlookCases.Select(name => Result(name, "Passed", "")));
trx.Root!.Element(Trx + "Results")!.Add(WindowsCropCases.Select(name => Result(name, "Passed", "")));
var counters = trx.Descendants(Trx + "Counters").Single();
counters.SetAttributeValue("total", 11);
counters.SetAttributeValue("executed", 11);
counters.SetAttributeValue("passed", 11);
return (discovery + '\n' + string.Join('\n', OutlookCases) + '\n' + string.Join('\n', WindowsCropCases), trx);
}
private static (string Discovery, XDocument Results) PortableFixture()
{
var results = new[]
{
Result(Guard, "Passed", "platform-assurance: target=portable; host=linux"),
Result(UserEnvironment, "NotExecuted", "Requires Windows per-user environment variables; run on Windows."),
Result(Glyphs, "NotExecuted", "Requires Windows System.Drawing bitmap rendering; run on Windows.")
};
return ("The following Tests are available:\n" + string.Join('\n', new[] { Guard, UserEnvironment, Glyphs }),
new XDocument(new XElement(Trx + "TestRun",
new XElement(Trx + "Results", results),
new XElement(Trx + "ResultSummary", new XAttribute("outcome", "Completed"),
new XElement(Trx + "Counters", new XAttribute("total", 3), new XAttribute("executed", 1),
new XAttribute("passed", 1), new XAttribute("failed", 0), new XAttribute("notExecuted", 0))))));
}
private static XElement Result(string name, string outcome, string output) =>
new(Trx + "UnitTestResult", new XAttribute("testName", name), new XAttribute("outcome", outcome),
new XElement(Trx + "Output", new XElement(Trx + "StdOut", output)));
}
@@ -1,7 +1,6 @@
using MeetingAssistant.Speakers;
using Microsoft.Data.Sqlite;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -25,7 +24,7 @@ public sealed class SpeakerRecognitionFeatureSelectionTests
Directory.CreateDirectory(directory);
try
{
await using var factory = new WebApplicationFactory<Program>()
await using var factory = new IsolatedApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
+1 -6
View File
@@ -177,15 +177,10 @@ public sealed class TaskbarIconTests
MeetingTaskbarExitPolicy.RequiresConfirmation(state));
}
[Fact]
[WindowsFact("System.Drawing bitmap rendering")]
[SupportedOSPlatform("windows")]
public void TaskbarIconGlyphsAreVisuallyCentered()
{
if (!OperatingSystem.IsWindows())
{
return;
}
foreach (var state in new[]
{
RecordingProcessState.Idle,
@@ -0,0 +1,72 @@
using System.Net;
using System.Net.Http.Json;
using System.Reflection;
using System.Runtime.Versioning;
using MeetingAssistant.MacOs;
using MeetingAssistant.Notifications;
using MeetingAssistant.Recording;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class TestTargetCompositionTests(ITestOutputHelper output)
{
[Fact]
public async Task TestAndApplicationTargetsUseMatchingComposition()
{
var testAssembly = typeof(TestTargetCompositionTests).Assembly;
var applicationAssembly = typeof(Program).Assembly;
var testFramework = testAssembly.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName;
var applicationFramework = applicationAssembly.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName;
var testPlatform = testAssembly.GetCustomAttribute<TargetPlatformAttribute>()?.PlatformName;
var applicationPlatform = applicationAssembly.GetCustomAttribute<TargetPlatformAttribute>()?.PlatformName;
output.WriteLine($"Test framework: {testFramework}; platform: {testPlatform ?? "portable"}");
output.WriteLine($"Application framework: {applicationFramework}; platform: {applicationPlatform ?? "portable"}");
Assert.Equal(".NETCoreApp,Version=v10.0", testFramework);
Assert.Equal(testFramework, applicationFramework);
Assert.Equal(testPlatform, applicationPlatform);
#if WINDOWS
Assert.Equal("Windows10.0.19041.0", testPlatform, ignoreCase: true);
#else
Assert.Null(testPlatform);
#endif
await using var factory = new IsolatedApplicationFactory();
using var client = factory.CreateClient();
using var health = await client.GetAsync("/health");
var capabilities = await client.GetFromJsonAsync<PlatformCapabilityReport>("/diagnostics/platform-capabilities");
using var requests = await client.GetAsync("/desktop/requests");
Assert.Equal(HttpStatusCode.OK, health.StatusCode);
Assert.NotNull(capabilities);
#if WINDOWS
Assert.Equal("OutlookClassicMeetingMetadataProvider", capabilities.MeetingMetadataProvider);
Assert.Equal("WpfWorkflowRulesEditorWindowService", capabilities.WorkflowEditorProvider);
Assert.IsType<WindowsAgentApprovalPromptService>(factory.Services.GetRequiredService<IAgentApprovalPromptService>());
Assert.IsType<WindowsMeetingInactivityPromptService>(factory.Services.GetRequiredService<IMeetingInactivityPromptService>());
Assert.Equal(HttpStatusCode.NotFound, requests.StatusCode);
const string target = "windows";
#else
Assert.Equal("MacOsWorkflowRulesEditorWindowService", capabilities.WorkflowEditorProvider);
Assert.Equal(HttpStatusCode.OK, requests.StatusCode);
if (OperatingSystem.IsMacOS())
{
Assert.Equal("MacOsMeetingMetadataProvider", capabilities.MeetingMetadataProvider);
Assert.IsType<MacOsAgentApprovalPromptService>(factory.Services.GetRequiredService<IAgentApprovalPromptService>());
Assert.IsType<MacOsMeetingInactivityPromptService>(factory.Services.GetRequiredService<IMeetingInactivityPromptService>());
}
else
{
Assert.Equal("NoopMeetingMetadataProvider", capabilities.MeetingMetadataProvider);
Assert.IsType<NoopAgentApprovalPromptService>(factory.Services.GetRequiredService<IAgentApprovalPromptService>());
Assert.IsType<NoopMeetingInactivityPromptService>(factory.Services.GetRequiredService<IMeetingInactivityPromptService>());
}
const string target = "portable";
#endif
var host = OperatingSystem.IsWindows() ? "windows" :
OperatingSystem.IsMacOS() ? "macos" : OperatingSystem.IsLinux() ? "linux" : "other";
output.WriteLine($"platform-assurance: target={target}; host={host}");
}
}
@@ -0,0 +1,12 @@
namespace MeetingAssistant.Tests;
public sealed class WindowsFactAttribute : FactAttribute
{
public WindowsFactAttribute(string requiredCapability)
{
if (!OperatingSystem.IsWindows())
{
Skip = $"Requires Windows {requiredCapability}; run on Windows.";
}
}
}
@@ -0,0 +1,109 @@
#if WINDOWS
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using MeetingAssistant.Screenshots;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class WindowsScreenshotImageCropperTests(ITestOutputHelper output)
{
[WindowsFact("System.Drawing screenshot cropping")]
public Task WindowsCropUsesOcrPixelCoordinates() =>
AssertCropAsync(new ScreenshotCropCoordinates(0, 0, 2, 2));
[WindowsFact("System.Drawing screenshot cropping")]
public Task WindowsCropPreservesExpectedPixelsAtNonzeroOrigin() =>
AssertCropAsync(new ScreenshotCropCoordinates(1, 1, 3, 2));
[WindowsFact("System.Drawing screenshot cropping")]
public async Task WindowsCropRejectsInvalidBounds()
{
using var fixture = new ImageFixture();
await using var factory = new IsolatedApplicationFactory();
using var client = factory.CreateClient();
var cropper = GetWindowsCropper(factory);
ScreenshotCropCoordinates[] invalidBounds =
[
new(-1, 0, 1, 1), new(0, -1, 1, 1),
new(0, 0, 0, 1), new(0, 0, 1, 0),
new(0, 0, -1, 1), new(0, 0, 1, -1),
new(4, 0, 1, 1), new(0, 3, 1, 1),
new(3, 0, 2, 1), new(0, 2, 1, 2)
];
foreach (var bounds in invalidBounds)
{
var failure = await Assert.ThrowsAsync<InvalidDataException>(() =>
cropper.SaveCroppedScreenshotAsync(fixture.InputPath, bounds, CancellationToken.None));
Assert.Contains("outside image bounds 4x3", failure.Message);
Assert.False(File.Exists(fixture.CroppedPath));
output.WriteLine($"Rejected {bounds.X},{bounds.Y},{bounds.Width},{bounds.Height}; no output created.");
}
}
private async Task AssertCropAsync(ScreenshotCropCoordinates bounds)
{
using var fixture = new ImageFixture();
await using var factory = new IsolatedApplicationFactory();
using var client = factory.CreateClient();
var cropper = GetWindowsCropper(factory);
var path = await cropper.SaveCroppedScreenshotAsync(
fixture.InputPath, bounds, CancellationToken.None);
Assert.Equal(fixture.CroppedPath, path);
using var cropped = new Bitmap(path!);
Assert.Equal(bounds.Width, cropped.Width);
Assert.Equal(bounds.Height, cropped.Height);
for (var y = 0; y < cropped.Height; y++)
{
for (var x = 0; x < cropped.Width; x++)
{
Assert.Equal(ImageFixture.PixelAt(bounds.X + x, bounds.Y + y).ToArgb(),
cropped.GetPixel(x, y).ToArgb());
}
}
output.WriteLine($"Verified every pixel of {bounds.Width}x{bounds.Height} at original origin {bounds.X},{bounds.Y}.");
}
private IScreenshotImageCropper GetWindowsCropper(IsolatedApplicationFactory factory)
{
var cropper = factory.Services.GetRequiredService<IScreenshotImageCropper>();
Assert.IsType<WindowsScreenshotImageCropper>(cropper);
output.WriteLine($"Registered cropper: {cropper.GetType().Name}; host: {RuntimeInformation.OSDescription}; " +
$"architecture: {RuntimeInformation.ProcessArchitecture}; runtime: {RuntimeInformation.FrameworkDescription}; " +
"target: net10.0-windows10.0.19041.0.");
return cropper;
}
private sealed class ImageFixture : IDisposable
{
private readonly string root = Path.Combine(Path.GetTempPath(), $"meeting-assistant-windows-crop-{Guid.NewGuid():N}");
public ImageFixture()
{
Directory.CreateDirectory(root);
using var image = new Bitmap(4, 3, PixelFormat.Format32bppArgb);
for (var y = 0; y < image.Height; y++)
{
for (var x = 0; x < image.Width; x++)
{
image.SetPixel(x, y, PixelAt(x, y));
}
}
image.Save(InputPath, ImageFormat.Png);
}
public string InputPath => Path.Combine(root, "input.png");
public string CroppedPath => Path.Combine(root, "input-cropped.png");
// Each original coordinate has a distinct opaque color; no resampling tolerance hides a shifted origin.
public static Color PixelAt(int x, int y) => Color.FromArgb(255, 30 + x * 50, 20 + y * 70, 10 + x * 15 + y * 5);
public void Dispose() => Directory.Delete(root, recursive: true);
}
}
#endif