Public Access
ci: validate macOS managed behavior on Ubuntu and stabilize shutdown
This commit is contained in:
@@ -48,13 +48,13 @@ jobs:
|
||||
echo "WINE_BIN=${WINE_BIN}" >> "${GITHUB_ENV}"
|
||||
"${WINE_BIN}" --version
|
||||
|
||||
- name: Restore (Windows target)
|
||||
- name: Restore (portable tests for Windows/Wine)
|
||||
run: |
|
||||
dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
|
||||
-p:EnableWindowsTargeting=true \
|
||||
-r win-x64
|
||||
|
||||
- name: Build (Windows target)
|
||||
- name: Build (portable tests for Windows/Wine)
|
||||
run: |
|
||||
dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
|
||||
-c Release \
|
||||
@@ -77,37 +77,47 @@ jobs:
|
||||
echo "WIN_DOTNET_DIR=${WIN_DOTNET_DIR}" >> "${GITHUB_ENV}"
|
||||
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" --info
|
||||
|
||||
- name: Build Windows desktop target via Wine
|
||||
run: |
|
||||
rm -f MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll
|
||||
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" build MeetingAssistant/MeetingAssistant.csproj \
|
||||
-c Release \
|
||||
-f net10.0-windows10.0.19041.0 \
|
||||
-r win-x64 \
|
||||
-p:EnableWindowsTargeting=true \
|
||||
--nologo
|
||||
test -s MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll
|
||||
|
||||
- name: Run tests via Wine (Windows dotnet host)
|
||||
run: |
|
||||
rm -f artifacts/tests/wine.trx
|
||||
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
|
||||
-c Release \
|
||||
-r win-x64 \
|
||||
--no-build \
|
||||
--logger "trx;LogFileName=wine.trx" \
|
||||
--results-directory artifacts/tests \
|
||||
--nologo
|
||||
test -s artifacts/tests/wine.trx
|
||||
|
||||
- name: Show test output folder on failure
|
||||
if: failure()
|
||||
run: |
|
||||
find artifacts/tests -type f -print || true
|
||||
find MeetingAssistant.Tests -type d -name TestResults -print || true
|
||||
find MeetingAssistant.Tests -type f -path "*/TestResults/*" -maxdepth 5 -print || true
|
||||
|
||||
macos-build-and-test:
|
||||
runs-on: macos
|
||||
portable-build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
TZ: "Europe/Berlin"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Verify native macOS runner
|
||||
run: |
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
echo "The macos job requires a native Gitea runner registered with the macos:host label."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
@@ -118,7 +128,7 @@ jobs:
|
||||
dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
|
||||
-p:EnableWindowsTargeting=true
|
||||
|
||||
- name: Build (portable target and macOS helpers)
|
||||
- name: Build (portable target including macOS managed adapters)
|
||||
run: |
|
||||
dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
|
||||
-c Release \
|
||||
@@ -127,24 +137,22 @@ jobs:
|
||||
-p:EnableWindowsTargeting=true \
|
||||
--nologo
|
||||
|
||||
- name: Verify macOS native helpers
|
||||
run: |
|
||||
test -x MeetingAssistant/bin/Release/net10.0/Native/MeetingAssistantAudioCapture.app/Contents/MacOS/macos-meeting-audio-capture
|
||||
test -x MeetingAssistant/bin/Release/net10.0/Native/macos-desktop-controls
|
||||
test -x MeetingAssistant/bin/Release/net10.0/Native/macos-meeting-integrations
|
||||
test -x MeetingAssistant/bin/Release/net10.0/Native/macos-meeting-assistant-launcher
|
||||
|
||||
- name: Run tests on macOS
|
||||
- name: Run portable and macOS managed behavior tests on Ubuntu
|
||||
run: |
|
||||
rm -f artifacts/tests/portable.trx
|
||||
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
|
||||
-c Release \
|
||||
-f net10.0 \
|
||||
--no-build \
|
||||
-p:EnableWindowsTargeting=true \
|
||||
--logger "trx;LogFileName=portable.trx" \
|
||||
--results-directory artifacts/tests \
|
||||
--nologo
|
||||
test -s artifacts/tests/portable.trx
|
||||
|
||||
- name: Show test output folder on failure
|
||||
if: failure()
|
||||
run: |
|
||||
find artifacts/tests -type f -print || true
|
||||
find MeetingAssistant.Tests -type d -name TestResults -print || true
|
||||
find MeetingAssistant.Tests -type f -path "*/TestResults/*" -maxdepth 5 -print || true
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -32,14 +32,13 @@ public sealed class PyannoteDiarizationWarmupHostedService : IHostedService
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var cancellation = startupCancellation;
|
||||
var cancellation = Interlocked.Exchange(ref startupCancellation, null);
|
||||
var task = startupTask;
|
||||
if (cancellation is null || task is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
startupCancellation = null;
|
||||
startupTask = null;
|
||||
await cancellation.CancelAsync();
|
||||
try
|
||||
|
||||
@@ -169,7 +169,11 @@ Detailed workflow syntax and extension guidance live in `docs/meeting-workflow-e
|
||||
|
||||
Behavior changes are OpenSpec-driven and test-first: update the relevant requirement/scenario, add a failing public behavior test, implement the smallest passing change, run focused tests and then the justified broader suite, and validate the active change with `openspec validate <change-id> --strict`. Documentation-only maintenance does not need a new OpenSpec change.
|
||||
|
||||
The Gitea workflow runs for pull requests, pushes, and manual dispatch. Its Linux job builds the Windows target, installs Wine plus a matching Windows .NET SDK, and runs the test project through the Windows host under Wine. A separate `runs-on: macos` job builds the portable target and bundled Swift helpers, verifies the native helper executables, and runs the complete portable unit-test suite on macOS. The Gitea runner serving that job must run natively on a Mac and expose the `macos:host` label; Docker-OSX is not used because it requires nested KVM that is normally unavailable on CI runners. CI validates source; it does not publish or deploy the workstation application.
|
||||
The Gitea workflow runs for pull requests, pushes, and manual dispatch on the existing `ubuntu-latest` runners. One job explicitly builds the Windows desktop target, installs Wine plus a matching Windows .NET SDK, and runs the portable test project through the Windows host under Wine. Another job builds and tests `net10.0` on Ubuntu, including the managed macOS audio, calendar, screenshot, registration, and desktop-control behavior tests. Its `TZ=Europe/Berlin` setting also exercises the calendar daylight-saving regression. No additional runner labels or host devices are required.
|
||||
|
||||
Ubuntu does not compile the Swift helpers or execute Apple frameworks. Tests requiring the native macOS environment report an explicit skip through `MacOsFact`; they must also be run on a supported Mac with `dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0 -c Release -p:EnableWindowsTargeting=true`. That build compiles and signs the helpers, and the suite checks packaging, helper self-tests, and native image cropping. Real microphone/system-audio capture and privacy permissions still require the operational checks described above.
|
||||
|
||||
[Docker-OSX](https://github.com/sickcodes/Docker-OSX) runs a macOS VM rather than providing a Wine-style compatibility layer. Its launcher supports software emulation with `KVM=accel=tcg`, so KVM is not an absolute requirement. However, its ready-made `auto` image uses Catalina; a supported .NET 10 guest needs macOS 14 or later plus the Swift build tools. No reproducible guest image or verified CI bootstrap for that environment is owned by this repository. CI validates source; it does not publish or deploy the workstation application.
|
||||
|
||||
## Operations And Limitations
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# macOS PR validation on existing Ubuntu infrastructure — 2026-10-03
|
||||
|
||||
## Scope and identity
|
||||
|
||||
Source repository: `/Users/dh/Documents/DanielsVault/_ops/meeting-assistant`.
|
||||
Delivery branch: `codex/macos-support` in `Daniel/meeting-assistant`; intended PR target: `Manuel/meeting-assistant:main`, PR #39.
|
||||
Fixed change baseline: `bd35ebc4c81bedf80caabb82481169124775fb36` (already contains main `7b2bcd36310ae6434a4ee6eeb3a47b6d1d431df4`).
|
||||
Work was isolated under `/private/tmp/meeting-assistant-macos-ci-20261003`; the original checkout was clean and no workstation service was restarted.
|
||||
Review covers this CI/test/shutdown delta. Earlier macOS feature and main-sync evidence remains in the respective OpenSpec implementation receipts; this record is not a fresh review of all earlier branch changes.
|
||||
|
||||
The evidence record and generated logs/TRX files are excluded from the substantive content identity to avoid self-referential hashes.
|
||||
|
||||
| Reviewed file | SHA-256 |
|
||||
| --- | --- |
|
||||
| `.gitea/workflows/pr-push-build-and-test.yaml` | `a6251c435c55967ca2babd58c136ba739ed7c18674e93fff96b477f7b7e1366e` |
|
||||
| `MeetingAssistant.Tests/MacOsDesktopControlManifestTests.cs` | `1df35cf6071f3cdae7d06af8566d59bd2af6c8c5c8c6ca9e04c24197a391975e` |
|
||||
| `MeetingAssistant.Tests/MacOsFactAttribute.cs` | `c756088c1efca9dfe7509a8597845d94e2076445a9f7a2ecec08cd41d333ba7e` |
|
||||
| `MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs` | `11c3e36bb31a8aa3b7542ff44367bb0e71ad68360e494fa219b77c0ced3e2456` |
|
||||
| `MeetingAssistant.Tests/MacOsMeetingIntegrationTests.cs` | `51337c27d6126e8e37f4e3b268307830678b8365de51b0c9ce49557ac40160f4` |
|
||||
| `MeetingAssistant.Tests/PyannoteDiarizationWarmupHostedServiceTests.cs` | `0d3494860ce044a2e73e7edba11017b910ca5c0207e2a79da8ff08bde2ff0dfe` |
|
||||
| `MeetingAssistant/Transcription/PyannoteDiarizationWarmupHostedService.cs` | `bcabf6007c9c942845336c1dd958852a8d6dcc383225b4bb1ea97903ef2848e8` |
|
||||
| `README.md` | `79e834ae2953ba690defd4ab10a24e08ae2ce2186ab689afc0ab607e579498e9` |
|
||||
| `openspec/specs/meeting-transcription/spec.md` | `c77edffc0b6ea374e250df758a9be76cbc3ee1146422fc374901c02f216676f6` |
|
||||
|
||||
## Requirements and observed evidence
|
||||
|
||||
- Existing Ubuntu infrastructure: both workflow jobs select only `ubuntu-latest`. The native `macos:host` runner requirement is removed. No KVM device, privileged container, new secret, guest image, or runner registration is required by these jobs. YAML parsing, each job's shell syntax (`bash -n`), and `git diff --check` passed.
|
||||
- Executable macOS managed coverage: the complete portable job's restore/build/test shell steps ran in an Ubuntu 24.04 amd64 `.NET 10` container with `TZ=Europe/Berlin`: **572 passed, 5 explicitly skipped, 0 failed, 577 total**. The skipped tests require a macOS environment and are not counted as passed. Three formerly guarded source/registration/approval tests now actually execute on Ubuntu. A separate `linux-x64` RID build passed the source identity test, proving lookup through the extra output-directory level. Raw Linux log: `/private/tmp/meeting-assistant-portable-final.log`; TRX: `/private/tmp/meeting-assistant-macos-ci-20261003/artifacts/tests/portable.trx`.
|
||||
- Native macOS coverage: the same test/runtime source contents built and signed the Swift helpers on this Mac and passed **577/577**, no skips. Raw receipt: `/private/tmp/meeting-assistant-native-ci-20261003/artifacts/tests/macos-warmup-final-green.trx`. This is local qualification; Ubuntu does not compile or execute the Swift helpers. Earlier real capture → Azure transcription → summary evidence is recorded in `openspec/changes/add-macos-desktop-controls/main-sync-completion.md`; no new live recording was started here.
|
||||
- Accurate Windows verification: `win-x64` RID alone does not select the application's Windows desktop TFM. CI now explicitly builds `net10.0-windows10.0.19041.0` through the existing Wine/Windows .NET SDK, then runs the portable tests under that Windows host. A fresh Windows DLL and fresh TRX are mandatory; a zero exit code without executed work is insufficient. Full Wine validation on the current candidate remains pending on the actual Ubuntu-x64 runner. The local Apple-Silicon Docker attempt hit `rosetta error: invalid gdt selector index 5`; it is not a passing Windows result. Raw attempt: `/private/tmp/meeting-assistant-wine-ci.log`.
|
||||
- Safe warmup shutdown: two real macOS full-suite runs exposed `ObjectDisposedException` from `PyannoteDiarizationWarmupHostedService.StopAsync` at `CancelAsync`, in different endpoint fixtures. A public concurrent-stop reproducer with temporarily increased scheduling overlap produced the exact linked-source exception. Under identical instrumentation, changing only cancellation-source ownership to `Interlocked.Exchange` passed; after instrumentation removal all four lifecycle tests and both final suites passed. Raw red/green receipts: `warmup-overlap-linked-red.trx`, `warmup-overlap-instrumented-green.trx`, and `warmup-overlap-final-green.trx` under the native checkout's `artifacts/tests`. The permanent test coordinates real stop callers with bounded waits; a race test is scheduling-sensitive on the original code, so the instrumented counterexample and original full-suite failure remain part of the causal evidence. The existing single-stop test still proves pending model work observes cancellation.
|
||||
- Test isolation: the macOS audio endpoint fixture supplies its own speech pipeline and no longer starts the unrelated external pyannote model warmup. Its actual mixing, recording-stop, and native-process-termination assertions remain. This fixture isolation is not presented as the production race fix.
|
||||
- OpenSpec: CI/build/test plumbing needs no new change under `AGENTS.md`. The runtime shutdown bug is specified as an addendum to the original accepted pyannote warmup requirement in `openspec/specs/meeting-transcription/spec.md`; strict validation passed (`openspec validate meeting-transcription --type spec --strict`). No active change was archived.
|
||||
|
||||
## Standards coverage and sequential review
|
||||
|
||||
Sources: repository `AGENTS.md`, applicable specs, and `code-review` criteria.
|
||||
DRY owns duplicated platform/test/CI knowledge; SOLID owns lifecycle resource ownership, dependency boundaries and test isolation; KISS owns bounded concurrency test clarity, command naming, accurate documentation, and remaining manual standards. Tests, source builds, OpenSpec validation, YAML/shell parsing and whitespace checks provide automated coverage.
|
||||
|
||||
The passes ran sequentially in fresh, independent reviewer contexts against the nine frozen hashes above. Each inspected the delta plus adjacent implementation and repository standards; none edited files or claimed remote CI success.
|
||||
|
||||
- DRY (`/root/review_dry`): clean; platform decisions are centralized in `MacOsFact`, and the two CI host contracts do not justify further indirection.
|
||||
- SOLID (`/root/review_solid`): clean; atomic cancellation ownership, the public stop regression, and the exact fixture dependency removal preserve coherent responsibilities.
|
||||
- KISS (`/root/review_kiss`): clean; bounded stop coordination, CI command names, platform boundaries and README instructions accurately describe the checks.
|
||||
|
||||
No review repair changed the frozen substantive identity. All required structural passes are complete; runtime evidence and remaining Windows/native limitations are recorded separately above.
|
||||
|
||||
## Delivery and remaining limits
|
||||
|
||||
State at publication: structural review complete; current-head remote verification pending. This is not a merge-readiness or main-merge receipt.
|
||||
The user authorized preparing this branch for PR CI and merging into main after tests pass. Authenticated Gitea identity `Daniel` has write/admin on the fork and read-only access to `Manuel/meeting-assistant`; it cannot merge there. The latest published PR head at intake was `bf6e1e7560c6bdfcebf69ad0e3240b4de647adb6`, so the earlier local main-sync commits also need publishing.
|
||||
Authorized delivery is a normal push of this reviewed delta plus the earlier local main-sync commits to the existing fork branch, followed by a PR description update. The published commit, actual workflow status, and any remaining owner action are reported on PR #39. Main must not be reported as merged before the required remote checks pass and the repository owner performs the permitted merge.
|
||||
A native macOS guest/Swift CI run on Ubuntu is not implemented; Docker-OSX supports software emulation, but its ready-made Catalina guest does not provide a supported .NET 10 + modern Apple-framework build environment. No unverified guest bootstrap was made a required PR check.
|
||||
@@ -595,3 +595,8 @@ When pyannote secondary validation is enabled, Meeting Assistant SHALL start a n
|
||||
- **WHEN** Meeting Assistant starts
|
||||
- **THEN** it begins preparing the configured pyannote runtime image and model cache without waiting for the first validation request
|
||||
- **AND** application startup is not blocked by the warm-up task
|
||||
|
||||
#### Scenario: Overlapping shutdown requests stop startup warm-up safely
|
||||
- **GIVEN** the pyannote startup warm-up hosted service has started
|
||||
- **WHEN** application shutdown calls its stop lifecycle concurrently
|
||||
- **THEN** it cancels pending warm-up and completes cleanup without a disposed-resource exception
|
||||
|
||||
Reference in New Issue
Block a user