ci: validate macOS managed behavior on Ubuntu and stabilize shutdown
PR and Push Build/Test / build-and-test (pull_request) Blocked by required conditions
PR and Push Build/Test / portable-build-and-test (pull_request) Blocked by required conditions

This commit is contained in:
dh
2026-10-03 11:30:29 +02:00
parent bd35ebc4c8
commit 1164c26846
10 changed files with 201 additions and 72 deletions
@@ -22,11 +22,6 @@ public sealed class MacOsDesktopControlManifestTests
[Fact]
public async Task MacOsApplicationDeclinesUnavailableAutomaticApprovals()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
using var factory = new WebApplicationFactory<Program>();
var approval = factory.Services.GetRequiredService<AgentApprovalService>();
@@ -36,11 +31,6 @@ public sealed class MacOsDesktopControlManifestTests
[Fact]
public void MacOsApplicationRegistersARealInteractiveAgentWindow()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
using var factory = new WebApplicationFactory<Program>();
var service = factory.Services.GetRequiredService<IWorkflowRulesEditorWindowService>();
@@ -0,0 +1,12 @@
namespace MeetingAssistant.Tests;
public sealed class MacOsFactAttribute : FactAttribute
{
public MacOsFactAttribute()
{
if (!OperatingSystem.IsMacOS())
{
Skip = "Requires native macOS helpers and Apple system tools; run on macOS.";
}
}
}
@@ -9,6 +9,7 @@ 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;
@@ -19,18 +20,15 @@ public sealed class MacOsMeetingAudioSourceTests
[Fact]
public void MacOsServiceLauncherDefinesStablePrivacyIdentity()
{
if (!OperatingSystem.IsMacOS())
var repositoryDirectory = new DirectoryInfo(AppContext.BaseDirectory);
while (repositoryDirectory is not null &&
!File.Exists(Path.Combine(repositoryDirectory.FullName, "MeetingAssistant", "MeetingAssistant.csproj")))
{
return;
repositoryDirectory = repositoryDirectory.Parent;
}
var projectDirectory = Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory,
"..",
"..",
"..",
"..",
"MeetingAssistant"));
Assert.NotNull(repositoryDirectory);
var projectDirectory = Path.Combine(repositoryDirectory.FullName, "MeetingAssistant");
var launcherDirectory = Path.Combine(projectDirectory, "Native", "MacOsLauncher");
var infoPlistPath = Path.Combine(launcherDirectory, "Info.plist");
var launcherSourcePath = Path.Combine(launcherDirectory, "main.swift");
@@ -54,14 +52,9 @@ public sealed class MacOsMeetingAudioSourceTests
StringComparison.Ordinal);
}
[Fact]
[MacOsFact]
public void NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var configuration = new DirectoryInfo(AppContext.BaseDirectory)
.Parent?.Name ?? "Debug";
var appPath = Path.GetFullPath(Path.Combine(
@@ -191,6 +184,10 @@ 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.RemoveAll<IMeetingAudioSource>();
services.RemoveAll<IMacOsAudioCaptureProcessFactory>();
PortableMeetingAudioRegistration.Add(services, isMacOs: true);
@@ -133,14 +133,9 @@ public sealed class MacOsMeetingIntegrationTests
Assert.Equal("meeting.png.cropped.png", croppedPath);
}
[Fact]
[MacOsFact]
public async Task MacOsCapabilityEndpointReportsEnabledRealProviders()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
await using var factory = new WebApplicationFactory<Program>();
using var client = factory.CreateClient();
@@ -163,14 +158,9 @@ public sealed class MacOsMeetingIntegrationTests
Assert.True(report.WhisperLocalDiarizationEnabled);
}
[Fact]
[MacOsFact]
public async Task NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var runner = new ProcessCommandRunner(
NullLogger<ProcessCommandRunner>.Instance);
@@ -190,14 +180,9 @@ public sealed class MacOsMeetingIntegrationTests
.ToArray());
}
[Fact]
[MacOsFact]
public async Task NativeHelperCropsPngUsingOcrPixelCoordinates()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var testRoot = Path.Combine(
Path.GetTempPath(),
$"meeting-assistant-native-crop-{Guid.NewGuid():N}");
@@ -268,14 +253,9 @@ public sealed class MacOsMeetingIntegrationTests
Assert.Equal(TimeSpan.FromHours(23), client.CalendarTo - client.CalendarFrom);
}
[Fact]
[MacOsFact]
public async Task CalendarClientFallsBackToCalendarAutomationWhenEventKitIsDenied()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var runner = new SequenceCommandRunner(
new CommandResult(
MacOsIntegrationException.CalendarPermissionDeniedExitCode,
@@ -3,11 +3,92 @@ using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.Transcription;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Runtime.ExceptionServices;
namespace MeetingAssistant.Tests;
public sealed class PyannoteDiarizationWarmupHostedServiceTests
{
[Fact]
public async Task HostedServiceStopsSafelyWhenShutdownRequestsOverlap()
{
const int iterations = 64;
const int shutdownCallers = 8;
using var applicationLifetime = new CancellationTokenSource();
var finalizer = new PyannoteTranscriptFinalizer(
new BlockingCommandRunner(),
Options.Create(new MeetingAssistantOptions()),
NullLogger<PyannoteTranscriptFinalizer>.Instance);
using var callersReady = new Barrier(shutdownCallers + 1);
var shutdownErrors = new ConcurrentQueue<ExceptionDispatchInfo>();
PyannoteDiarizationWarmupHostedService? service = null;
var shutdownTasks = Enumerable.Range(0, shutdownCallers)
.Select(_ => Task.Factory.StartNew(() =>
{
try
{
for (var iteration = 0; iteration < iterations; iteration++)
{
WaitForShutdownCallers();
try
{
service!.StopAsync(CancellationToken.None)
.WaitAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
}
catch (Exception exception)
{
shutdownErrors.Enqueue(ExceptionDispatchInfo.Capture(exception));
}
WaitForShutdownCallers();
if (!shutdownErrors.IsEmpty)
{
break;
}
}
}
catch (Exception exception)
{
shutdownErrors.Enqueue(ExceptionDispatchInfo.Capture(exception));
}
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default))
.ToArray();
try
{
for (var iteration = 0; iteration < iterations; iteration++)
{
service = new PyannoteDiarizationWarmupHostedService(
finalizer,
new FakeLaunchProfileOptionsProvider(new MeetingAssistantOptions()),
NullLogger<PyannoteDiarizationWarmupHostedService>.Instance);
await service.StartAsync(applicationLifetime.Token);
WaitForShutdownCallers();
WaitForShutdownCallers();
if (!shutdownErrors.IsEmpty)
{
break;
}
}
}
finally
{
await Task.WhenAll(shutdownTasks).WaitAsync(TimeSpan.FromSeconds(10));
}
if (shutdownErrors.TryPeek(out var shutdownError))
{
shutdownError.Throw();
}
void WaitForShutdownCallers()
{
Assert.True(
callersReady.SignalAndWait(TimeSpan.FromSeconds(5)),
"Timed out coordinating overlapping shutdown requests.");
}
}
[Fact]
public async Task HostedServiceWarmsEnabledValidationRuntimeWithoutBlockingStartup()
{