Compare commits

..
Author SHA1 Message Date
dh 3aaab45b46 Preserve initial kernel faults and restore upstream TCG profile 2026-10-04 12:51:24 +02:00
dh efc3fdc681 ci: stop Recovery boot loops and capture first reset evidence 2026-10-04 12:25:20 +02:00
dh c01ae13185 ci: qualify supported macOS emulation before Recovery download 2026-10-04 10:30:23 +02:00
dh 720a43158c reuse verified native platform version without duplicate query 2026-10-04 09:41:25 +02:00
dh 227884723a retain bounded disk stack and resource pressure evidence 2026-10-04 09:14:39 +02:00
dh 25989cf0eb diagnose native Recovery disk IPC with bounded observation 2026-10-04 08:41:06 +02:00
dh 94a70b2005 ci: patch known Recovery variants and preserve UDIF checksums
PR and Push Build/Test / build-and-test (push) Failing after 18m28s
PR and Push Build/Test / portable-build-and-test (push) Successful in 6m35s
2026-10-03 21:53:38 +02:00
dh 45d7bde71f ci: probe macOS Ventura on existing KVM hardware
PR and Push Build/Test / build-and-test (push) Canceled after 0s
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
2026-10-03 20:58:12 +02:00
dh 4606de0696 ci: preserve Apple Recovery daemon during read-only readiness probe
PR and Push Build/Test / build-and-test (push) Canceled after 0s
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
2026-10-03 20:17:56 +02:00
dh 40281b57a5 Isolate measured UID watchdog failure in the native TCG diagnostic
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
PR and Push Build/Test / build-and-test (push) Canceled after 1m33s
2026-10-03 18:58:10 +02:00
dh c92e62bdf5 Reduce native readiness probe overhead and preserve screenshot evidence
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
PR and Push Build/Test / build-and-test (push) Canceled after 1m15s
2026-10-03 18:05:15 +02:00
dh b40234d3b5 ci: capture native recovery startup context before platform probe
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
PR and Push Build/Test / build-and-test (push) Canceled after 3m24s
2026-10-03 16:38:34 +02:00
dh 0a30a1ca5a ci: retain current build fixes in the readonly native diagnostic 2026-10-03 16:22:03 +02:00
dh 6b1896660f ci: derive the macOS probe from the pinned QEMU filesystem image 2026-10-03 15:01:00 +02:00
dh 9a91a81992 ci: probe native macOS Recovery readiness on the existing Ubuntu runner 2026-10-03 14:18:50 +02:00
76 changed files with 777 additions and 6339 deletions

No files matched your search

@@ -0,0 +1,36 @@
name: macOS 14 TCG Recovery prerequisite diagnostic
on:
workflow_dispatch:
jobs:
macos-native-diagnostic:
runs-on: ubuntu-latest
timeout-minutes: 25
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1"
steps:
- name: Checkout diagnostic source
uses: actions/checkout@v7
- name: Setup .NET for the diagnostic helper
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Verify actual AVX2 emulation then probe macOS 14 Recovery
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
- name: Always clean up only this diagnostic's owned resources
if: always()
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
- name: Preserve native diagnostic evidence
if: always()
uses: actions/upload-artifact@v3
with:
name: native-macos-recovery-diagnostic
path: artifacts/native-macos/
if-no-files-found: error
retention-days: 7
+40 -137
View File
@@ -3,18 +3,20 @@ name: PR and Push Build/Test
on: on:
pull_request: pull_request:
push: push:
# This temporary branch changes only the native prerequisite diagnostic.
# Its manual workflow provides that evidence; the PR branch still runs all jobs.
branches-ignore:
- codex/macos-ci-kvm-compatibility
- codex/macos-ci-tcg-supported
workflow_dispatch: workflow_dispatch:
jobs: jobs:
windows-build-and-test: build-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1" DOTNET_NOLOGO: "1"
WINEDEBUG: "-all" WINEDEBUG: "-all"
defaults:
run:
shell: bash
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -25,14 +27,6 @@ jobs:
with: with:
dotnet-version: "10.0.x" dotnet-version: "10.0.x"
- name: Record Windows validation identity
run: |
set -euo pipefail
mkdir -p artifacts/tests/windows
git rev-parse HEAD > artifacts/tests/windows/commit.txt
dotnet --info > artifacts/tests/windows/linux-sdk.txt
echo "WINEPREFIX=${RUNNER_TEMP}/meeting-assistant-wine" >> "${GITHUB_ENV}"
- name: Install Wine - name: Install Wine
run: | run: |
set -euo pipefail set -euo pipefail
@@ -53,11 +47,25 @@ jobs:
WINE_BIN="/opt/wine-stable/bin/wine" WINE_BIN="/opt/wine-stable/bin/wine"
test -x "${WINE_BIN}" test -x "${WINE_BIN}"
echo "WINE_BIN=${WINE_BIN}" >> "${GITHUB_ENV}" echo "WINE_BIN=${WINE_BIN}" >> "${GITHUB_ENV}"
"${WINE_BIN}" --version | tee artifacts/tests/windows/wine-version.txt "${WINE_BIN}" --version
- name: Restore (portable tests for Windows/Wine)
run: |
dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-p:EnableWindowsTargeting=true \
-r win-x64
- name: Build (portable tests for Windows/Wine)
run: |
dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release \
--no-restore \
-p:EnableWindowsTargeting=true \
-p:RuntimeIdentifier=win-x64 \
--nologo
- name: Install Windows .NET SDK for Wine - name: Install Windows .NET SDK for Wine
run: | run: |
set -euo pipefail
SDK_VERSION="$(dotnet --version)" SDK_VERSION="$(dotnet --version)"
WIN_DOTNET_DIR="${RUNNER_TEMP}/dotnet-win" WIN_DOTNET_DIR="${RUNNER_TEMP}/dotnet-win"
WIN_DOTNET_ZIP="${RUNNER_TEMP}/dotnet-sdk-win-x64.zip" WIN_DOTNET_ZIP="${RUNNER_TEMP}/dotnet-sdk-win-x64.zip"
@@ -68,11 +76,10 @@ jobs:
mkdir -p "${WIN_DOTNET_DIR}" mkdir -p "${WIN_DOTNET_DIR}"
unzip -oq "${WIN_DOTNET_ZIP}" -d "${WIN_DOTNET_DIR}" unzip -oq "${WIN_DOTNET_ZIP}" -d "${WIN_DOTNET_DIR}"
echo "WIN_DOTNET_DIR=${WIN_DOTNET_DIR}" >> "${GITHUB_ENV}" echo "WIN_DOTNET_DIR=${WIN_DOTNET_DIR}" >> "${GITHUB_ENV}"
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" --info 2>&1 | tee artifacts/tests/windows/windows-sdk.txt "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" --info
- name: Provision NuGet signature trust inside Wine - name: Provision NuGet signature trust inside Wine
run: | run: |
set -euo pipefail
SDK_VERSION="$(dotnet --version)" SDK_VERSION="$(dotnet --version)"
SDK_TRUST_ROOT="${DOTNET_ROOT}/sdk/${SDK_VERSION}/trustedroots" SDK_TRUST_ROOT="${DOTNET_ROOT}/sdk/${SDK_VERSION}/trustedroots"
dotnet publish scripts/wine-sdk-trust.cs \ dotnet publish scripts/wine-sdk-trust.cs \
@@ -80,92 +87,47 @@ jobs:
-p:UseAppHost=false \ -p:UseAppHost=false \
-p:PublishAot=false \ -p:PublishAot=false \
-o "${RUNNER_TEMP}/wine-sdk-trust" \ -o "${RUNNER_TEMP}/wine-sdk-trust" \
--nologo 2>&1 | tee artifacts/tests/windows/trust-helper-build.log --nologo
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" \ "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" \
"Z:${RUNNER_TEMP}/wine-sdk-trust/WineSdkTrust.dll" --import \ "Z:${RUNNER_TEMP}/wine-sdk-trust/WineSdkTrust.dll" --import \
"Z:${SDK_TRUST_ROOT}/codesignctl.pem" \ "Z:${SDK_TRUST_ROOT}/codesignctl.pem" \
"Z:${SDK_TRUST_ROOT}/timestampctl.pem" \ "Z:${SDK_TRUST_ROOT}/timestampctl.pem"
2>&1 | tee artifacts/tests/windows/nuget-trust.log
- name: Diagnose Windows SDK targeting-pack resolution - name: Diagnose Windows SDK targeting-pack resolution
run: | run: |
set -euo pipefail
{
find "${WIN_DOTNET_DIR}/packs" -maxdepth 4 -name PackageOverrides.txt -print find "${WIN_DOTNET_DIR}/packs" -maxdepth 4 -name PackageOverrides.txt -print
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" msbuild MeetingAssistant/MeetingAssistant.csproj \ "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" msbuild MeetingAssistant/MeetingAssistant.csproj \
-p:EnableWindowsTargeting=true \ -p:EnableWindowsTargeting=true \
-p:TargetFramework=net10.0-windows10.0.19041.0 \ -p:TargetFramework=net10.0 \
-getProperty:NetCoreRoot,NetCoreTargetingPackRoot,PrunePackageDataRoot,PrunePackageTargetingPackRoots,MSBuildSDKsPath,DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR -getProperty:NetCoreRoot,NetCoreTargetingPackRoot,PrunePackageDataRoot,PrunePackageTargetingPackRoots,MSBuildSDKsPath,DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" msbuild MeetingAssistant/MeetingAssistant.csproj \ "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" msbuild MeetingAssistant/MeetingAssistant.csproj \
-p:EnableWindowsTargeting=true \ -p:EnableWindowsTargeting=true \
-p:TargetFramework=net10.0-windows10.0.19041.0 \ -p:TargetFramework=net10.0 \
-t:AddPrunePackageReferences \ -t:AddPrunePackageReferences \
-v:normal -v:normal
} 2>&1 | tee artifacts/tests/windows/targeting-pack-diagnostics.log
- name: Restore the Windows test and application targets via Wine
run: |
set -euo pipefail
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-p:TargetFramework=net10.0-windows10.0.19041.0 \
-p:EnableWindowsTargeting=true -r win-x64 \
2>&1 | tee artifacts/tests/windows/restore.log
- name: Build Windows desktop target via Wine - name: Build Windows desktop target via Wine
run: | run: |
set -euo pipefail
rm -f MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll 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 \ "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" build MeetingAssistant/MeetingAssistant.csproj \
-c Release \ -c Release \
-f net10.0-windows10.0.19041.0 \ -f net10.0-windows10.0.19041.0 \
-r win-x64 \ -r win-x64 \
-p:EnableWindowsTargeting=true \ -p:EnableWindowsTargeting=true \
--nologo 2>&1 | tee artifacts/tests/windows/application-build.log --nologo
test -s MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll test -s MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll
- name: Build the actual Windows test target via Wine - name: Run tests via Wine (Windows dotnet host)
run: | run: |
set -euo pipefail rm -f artifacts/tests/wine.trx
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release -f net10.0-windows10.0.19041.0 -r win-x64 \
--no-restore -p:EnableWindowsTargeting=true --nologo \
2>&1 | tee artifacts/tests/windows/test-build.log
- name: Discover Windows cases via Wine
run: |
set -euo pipefail
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release -f net10.0-windows10.0.19041.0 -r win-x64 \
--no-build --list-tests --nologo \
2>&1 | tee artifacts/tests/windows/discovery.txt
- name: Execute Windows cases via Wine
run: |
set -euo pipefail
rm -f artifacts/tests/windows/windows.trx
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release \ -c Release \
-f net10.0-windows10.0.19041.0 \
-r win-x64 \ -r win-x64 \
--no-build \ --no-build \
--logger "trx;LogFileName=windows.trx" \ --logger "trx;LogFileName=wine.trx" \
--results-directory artifacts/tests/windows \ --results-directory artifacts/tests \
--nologo 2>&1 | tee artifacts/tests/windows/execution.log --nologo
test -s artifacts/tests/windows/windows.trx test -s artifacts/tests/wine.trx
- name: Verify Windows discovery and executed assertions
run: |
set -euo pipefail
dotnet run tools/VerifyPlatformTests.cs -- windows \
artifacts/tests/windows/discovery.txt artifacts/tests/windows/windows.trx \
2>&1 | tee artifacts/tests/windows/verification.txt
- name: Retain Windows evidence including incomplete builds
if: always()
uses: https://github.com/christopherhx/gitea-upload-artifact@v4
with:
name: windows-target-evidence
path: artifacts/tests/windows/
- name: Show test output folder on failure - name: Show test output folder on failure
if: failure() if: failure()
@@ -180,9 +142,6 @@ jobs:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1" DOTNET_NOLOGO: "1"
TZ: "Europe/Berlin" TZ: "Europe/Berlin"
defaults:
run:
shell: bash
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -195,59 +154,30 @@ jobs:
- name: Restore (portable target) - name: Restore (portable target)
run: | run: |
set -euo pipefail
mkdir -p artifacts/tests/portable
git rev-parse HEAD > artifacts/tests/portable/commit.txt
dotnet --info > artifacts/tests/portable/sdk.txt
dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-p:TargetFramework=net10.0 \ -p:EnableWindowsTargeting=true
-p:EnableWindowsTargeting=true \
2>&1 | tee artifacts/tests/portable/restore.log
- name: Build (portable target including macOS managed adapters) - name: Build (portable target including macOS managed adapters)
run: | run: |
set -euo pipefail
dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release \ -c Release \
-f net10.0 \ -f net10.0 \
--no-restore \ --no-restore \
-p:EnableWindowsTargeting=true \ -p:EnableWindowsTargeting=true \
--nologo 2>&1 | tee artifacts/tests/portable/test-build.log --nologo
- name: Discover portable cases
run: |
set -euo pipefail
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release -f net10.0 --no-build --list-tests --nologo \
2>&1 | tee artifacts/tests/portable/discovery.txt
- name: Run portable and macOS managed behavior tests on Ubuntu - name: Run portable and macOS managed behavior tests on Ubuntu
run: | run: |
set -euo pipefail rm -f artifacts/tests/portable.trx
rm -f artifacts/tests/portable/portable.trx
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release \ -c Release \
-f net10.0 \ -f net10.0 \
--no-build \ --no-build \
-p:EnableWindowsTargeting=true \ -p:EnableWindowsTargeting=true \
--logger "trx;LogFileName=portable.trx" \ --logger "trx;LogFileName=portable.trx" \
--results-directory artifacts/tests/portable \ --results-directory artifacts/tests \
--nologo 2>&1 | tee artifacts/tests/portable/execution.log --nologo
test -s artifacts/tests/portable/portable.trx test -s artifacts/tests/portable.trx
- name: Verify portable discovery and honest skips
run: |
set -euo pipefail
dotnet run tools/VerifyPlatformTests.cs -- portable \
artifacts/tests/portable/discovery.txt artifacts/tests/portable/portable.trx \
2>&1 | tee artifacts/tests/portable/verification.txt
- name: Retain portable evidence
if: always()
uses: https://github.com/christopherhx/gitea-upload-artifact@v4
with:
name: portable-target-evidence
path: artifacts/tests/portable/
- name: Show test output folder on failure - name: Show test output folder on failure
if: failure() if: failure()
@@ -255,30 +185,3 @@ jobs:
find artifacts/tests -type f -print || true find artifacts/tests -type f -print || true
find MeetingAssistant.Tests -type d -name TestResults -print || true find MeetingAssistant.Tests -type d -name TestResults -print || true
find MeetingAssistant.Tests -type f -path "*/TestResults/*" -maxdepth 5 -print || true find MeetingAssistant.Tests -type f -path "*/TestResults/*" -maxdepth 5 -print || true
macos-native-full:
needs: [windows-build-and-test, portable-build-and-test]
runs-on: ubuntu-latest
timeout-minutes: 180
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1"
steps:
- name: Checkout exact native CI candidate
uses: actions/checkout@v7
- name: Setup .NET orchestration SDK
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Run owned macOS 13 KVM guest and all native tests
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --recovery-format raw --output artifacts/native-macos-full
- name: Always remove only this run's owned resources
if: always()
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos-full
- name: Retain native build, signatures, TRX and guest receipts
if: always()
uses: actions/upload-artifact@v3
with:
name: native-macos-full
path: artifacts/native-macos-full/
if-no-files-found: error
retention-days: 7
@@ -34,7 +34,7 @@ public sealed class AsrDiagnosticEndpointTests
public async Task NamedProfileEndpointPassesProfileToConfiguredProvider() public async Task NamedProfileEndpointPassesProfileToConfiguredProvider()
{ {
var pipelineFactory = new CapturingProfileSpeechRecognitionPipelineFactory(); var pipelineFactory = new CapturingProfileSpeechRecognitionPipelineFactory();
await using var factory = new IsolatedApplicationFactory() await using var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder => .WithWebHostBuilder(builder =>
{ {
builder.ConfigureAppConfiguration((_, configuration) => builder.ConfigureAppConfiguration((_, configuration) =>
@@ -104,7 +104,7 @@ public sealed class AsrDiagnosticEndpointTests
public async Task SpeakerIdentityMergeDiagnosticEndpointUsesRecentDaysParameter() public async Task SpeakerIdentityMergeDiagnosticEndpointUsesRecentDaysParameter()
{ {
var mergeService = new CapturingSpeakerIdentityMergeService(); var mergeService = new CapturingSpeakerIdentityMergeService();
await using var factory = new IsolatedApplicationFactory() await using var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder => .WithWebHostBuilder(builder =>
{ {
builder.ConfigureAppConfiguration((_, configuration) => builder.ConfigureAppConfiguration((_, configuration) =>
@@ -140,7 +140,7 @@ public sealed class AsrDiagnosticEndpointTests
Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize) Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize)
where TProvider : class, IStreamingTranscriptionProvider where TProvider : class, IStreamingTranscriptionProvider
{ {
return new IsolatedApplicationFactory() return new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder => .WithWebHostBuilder(builder =>
{ {
builder.ConfigureAppConfiguration((_, configuration) => builder.ConfigureAppConfiguration((_, configuration) =>
@@ -267,3 +267,4 @@ public sealed class AsrDiagnosticEndpointTests
} }
} }
} }
@@ -107,9 +107,14 @@ public sealed class AzureSpeechStreamingTranscriptionProviderTests
speechConfig.GetProperty(PropertyId.SpeechServiceResponse_PostProcessingOption))); speechConfig.GetProperty(PropertyId.SpeechServiceResponse_PostProcessingOption)));
} }
[WindowsFact("per-user environment variables")] [Fact]
public void ResolveKeyFallsBackToUserEnvironment() public void ResolveKeyFallsBackToUserEnvironment()
{ {
if (!OperatingSystem.IsWindows())
{
return;
}
var keyEnv = $"MEETING_ASSISTANT_AZURE_KEY_{Guid.NewGuid():N}"; var keyEnv = $"MEETING_ASSISTANT_AZURE_KEY_{Guid.NewGuid():N}";
try try
{ {
@@ -1,166 +0,0 @@
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
@@ -1,198 +0,0 @@
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; namespace MeetingAssistant.Tests;
public sealed class HealthEndpointTests : IClassFixture<IsolatedApplicationFactory> public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{ {
private readonly WebApplicationFactory<Program> factory; private readonly WebApplicationFactory<Program> factory;
public HealthEndpointTests(IsolatedApplicationFactory factory) public HealthEndpointTests(WebApplicationFactory<Program> factory)
{ {
this.factory = factory.WithWebHostBuilder(builder => this.factory = factory.WithWebHostBuilder(builder =>
{ {
@@ -55,7 +55,6 @@ public sealed class HealthEndpointTests : IClassFixture<IsolatedApplicationFacto
Assert.Equal(HostOperatingSystem.DetectCurrent(), first); Assert.Equal(HostOperatingSystem.DetectCurrent(), first);
} }
#if !WINDOWS
[Fact] [Fact]
public async Task NeutralBuildReportsSpeakerPlaybackUnavailable() public async Task NeutralBuildReportsSpeakerPlaybackUnavailable()
{ {
@@ -66,7 +65,6 @@ public sealed class HealthEndpointTests : IClassFixture<IsolatedApplicationFacto
Assert.Contains("Windows build", result); Assert.Contains("Windows build", result);
Assert.DoesNotContain("Queued", result); Assert.DoesNotContain("Queued", result);
} }
#endif
[Fact] [Fact]
public async Task ApplicationStartupDeletesStaleTemporaryRecordings() public async Task ApplicationStartupDeletesStaleTemporaryRecordings()
@@ -77,7 +75,6 @@ public sealed class HealthEndpointTests : IClassFixture<IsolatedApplicationFacto
{ {
services.RemoveAll<IRecordedAudioStore>(); services.RemoveAll<IRecordedAudioStore>();
services.AddSingleton<IRecordedAudioStore, StartupCleanupRecordedAudioStore>(); services.AddSingleton<IRecordedAudioStore, StartupCleanupRecordedAudioStore>();
services.AddHostedService<TemporaryRecordingCleanupHostedService>();
}); });
}); });
using var client = cleanupFactory.CreateClient(); using var client = cleanupFactory.CreateClient();
@@ -1,25 +0,0 @@
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,6 +1,8 @@
using MeetingAssistant.LaunchProfiles; using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.MacOs; using MeetingAssistant.MacOs;
using MeetingAssistant.Workflow; using MeetingAssistant.Workflow;
using MeetingAssistant.Notifications;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -8,17 +10,33 @@ namespace MeetingAssistant.Tests;
public sealed class MacOsDesktopControlManifestTests public sealed class MacOsDesktopControlManifestTests
{ {
#if !WINDOWS [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));
}
[Fact] [Fact]
public void MacOsApplicationRegistersARealInteractiveAgentWindow() public void MacOsApplicationRegistersARealInteractiveAgentWindow()
{ {
using var factory = new IsolatedApplicationFactory(); using var factory = new WebApplicationFactory<Program>();
var service = factory.Services.GetRequiredService<IWorkflowRulesEditorWindowService>(); var service = factory.Services.GetRequiredService<IWorkflowRulesEditorWindowService>();
Assert.IsType<MacOsWorkflowRulesEditorWindowService>(service); Assert.IsType<MacOsWorkflowRulesEditorWindowService>(service);
} }
#endif
[Fact] [Fact]
public void ManifestPreservesConfiguredWindowsEquivalentShortcutsAndControlEndpoints() public void ManifestPreservesConfiguredWindowsEquivalentShortcutsAndControlEndpoints()
@@ -4,13 +4,9 @@ public sealed class MacOsFactAttribute : FactAttribute
{ {
public MacOsFactAttribute() 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()) if (!OperatingSystem.IsMacOS())
{ {
Skip = "Requires native macOS helpers and Apple system tools; run on macOS."; Skip = "Requires native macOS helpers and Apple system tools; run on macOS.";
} }
#endif
} }
} }
@@ -1,14 +1,15 @@
using MeetingAssistant.Recording; using MeetingAssistant.Recording;
using MeetingAssistant.Speakers;
using MeetingAssistant.MeetingNotes; using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Summary; using MeetingAssistant.Summary;
using MeetingAssistant.Transcription; using MeetingAssistant.Transcription;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using System.Net.Http.Json; using System.Net.Http.Json;
@@ -163,7 +164,7 @@ public sealed class MacOsMeetingAudioSourceTests
microphonePcm: Pcm16(2_000), microphonePcm: Pcm16(2_000),
systemPcm: Pcm16(10_000)); systemPcm: Pcm16(10_000));
var speechPipelines = new CapturingSpeechRecognitionPipelineFactory(); var speechPipelines = new CapturingSpeechRecognitionPipelineFactory();
var factory = new IsolatedApplicationFactory().WithWebHostBuilder(builder => var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{ {
builder.ConfigureAppConfiguration((_, configuration) => builder.ConfigureAppConfiguration((_, configuration) =>
{ {
@@ -183,7 +184,10 @@ public sealed class MacOsMeetingAudioSourceTests
}); });
builder.ConfigureTestServices(services => builder.ConfigureTestServices(services =>
{ {
services.AddHostedService<SpeakerIdentityDatabaseInitializer>(); // 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<IMeetingAudioSource>();
services.RemoveAll<IMacOsAudioCaptureProcessFactory>(); services.RemoveAll<IMacOsAudioCaptureProcessFactory>();
PortableMeetingAudioRegistration.Add(services, isMacOs: true); PortableMeetingAudioRegistration.Add(services, isMacOs: true);
@@ -1,14 +1,13 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using MeetingAssistant.Calendar; using MeetingAssistant.Calendar;
using MeetingAssistant.MacOs; using MeetingAssistant.MacOs;
using MeetingAssistant.Notifications;
using MeetingAssistant.MeetingNotes; using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Screenshots; using MeetingAssistant.Screenshots;
using MeetingAssistant.Transcription; using MeetingAssistant.Transcription;
using MeetingAssistant.Workflow; using MeetingAssistant.Workflow;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Hosting;
namespace MeetingAssistant.Tests; namespace MeetingAssistant.Tests;
@@ -19,10 +18,6 @@ public sealed class MacOsMeetingIntegrationTests
{ {
var macServices = new ServiceCollection().AddLogging(); var macServices = new ServiceCollection().AddLogging();
var unsupportedServices = 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(macServices, isMacOs: true);
MacOsMeetingIntegrationRegistration.Add(unsupportedServices, isMacOs: false); MacOsMeetingIntegrationRegistration.Add(unsupportedServices, isMacOs: false);
@@ -101,20 +96,39 @@ public sealed class MacOsMeetingIntegrationTests
} }
[Fact] [Fact]
public async Task MacOsScreenshotAndCropUseNativeClientResults() public async Task MacOsPromptAndScreenshotUseNativeClientResults()
{ {
var screenshot = new ActiveWindowScreenshot([137, 80, 78, 71], "Meeting deck"); var screenshot = new ActiveWindowScreenshot([137, 80, 78, 71], "Meeting deck");
var client = new StubMacOsDesktopIntegrationClient( var client = new StubMacOsDesktopIntegrationClient(
screenshot: screenshot, screenshot: screenshot,
promptResponse: MeetingStartPromptResponse.Record); promptResponse: MeetingStartPromptResponse.Record);
IMeetingStartPromptService prompt = new MacOsMeetingStartPromptService(
client,
NullLogger<MacOsMeetingStartPromptService>.Instance);
IActiveWindowScreenshotCapture capture = client; IActiveWindowScreenshotCapture capture = client;
IScreenshotImageCropper cropper = 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 captured = await capture.CaptureAsync(CancellationToken.None);
var croppedPath = await cropper.SaveCroppedScreenshotAsync( var croppedPath = await cropper.SaveCroppedScreenshotAsync(
"meeting.png", "meeting.png",
new ScreenshotCropCoordinates(1, 2, 3, 4), new ScreenshotCropCoordinates(1, 2, 3, 4),
CancellationToken.None); CancellationToken.None);
Assert.Equal(MeetingStartPromptResponse.Record, callbackResponse);
Assert.Equal(screenshot, captured); Assert.Equal(screenshot, captured);
Assert.Equal("meeting.png.cropped.png", croppedPath); Assert.Equal("meeting.png.cropped.png", croppedPath);
} }
@@ -122,7 +136,7 @@ public sealed class MacOsMeetingIntegrationTests
[MacOsFact] [MacOsFact]
public async Task MacOsCapabilityEndpointReportsEnabledRealProviders() public async Task MacOsCapabilityEndpointReportsEnabledRealProviders()
{ {
await using var factory = new IsolatedApplicationFactory(); await using var factory = new WebApplicationFactory<Program>();
using var client = factory.CreateClient(); using var client = factory.CreateClient();
var report = await client.GetFromJsonAsync<PlatformCapabilityReport>( var report = await client.GetFromJsonAsync<PlatformCapabilityReport>(
@@ -1,129 +0,0 @@
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);
}
@@ -1,222 +0,0 @@
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,16 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net10.0;net10.0-windows10.0.19041.0</TargetFrameworks> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<DefineConstants>$(DefineConstants);PLATFORM_TEST_VERIFIER_LIBRARY</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<PlatformTarget>x64</PlatformTarget>
<UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -25,15 +19,8 @@
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </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> <ItemGroup>
<ProjectReference Include="..\MeetingAssistant\MeetingAssistant.csproj" SetTargetFramework="TargetFramework=$(TargetFramework)" /> <ProjectReference Include="..\MeetingAssistant\MeetingAssistant.csproj" />
<Compile Include="..\tools\VerifyPlatformTests.cs" Link="PlatformTestVerification.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace MeetingAssistant.Tests; namespace MeetingAssistant.Tests;
@@ -16,7 +17,7 @@ public sealed class MeetingWorkflowDiagnosticEndpointTests
var firstRulesPath = Path.Combine(root, "first-rules.yaml"); var firstRulesPath = Path.Combine(root, "first-rules.yaml");
var secondRulesPath = Path.Combine(root, "second-rules.yaml"); var secondRulesPath = Path.Combine(root, "second-rules.yaml");
await File.WriteAllTextAsync(configPath, CreateConfigJson(firstRulesPath)); await File.WriteAllTextAsync(configPath, CreateConfigJson(firstRulesPath));
await using var factory = new IsolatedApplicationFactory() await using var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder => .WithWebHostBuilder(builder =>
{ {
builder.ConfigureAppConfiguration((_, configuration) => builder.ConfigureAppConfiguration((_, configuration) =>
@@ -1,377 +0,0 @@
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,6 +1,7 @@
using MeetingAssistant.Speakers; using MeetingAssistant.Speakers;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -24,7 +25,7 @@ public sealed class SpeakerRecognitionFeatureSelectionTests
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
try try
{ {
await using var factory = new IsolatedApplicationFactory() await using var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder => .WithWebHostBuilder(builder =>
{ {
builder.ConfigureAppConfiguration((_, configuration) => builder.ConfigureAppConfiguration((_, configuration) =>
+6 -1
View File
@@ -177,10 +177,15 @@ public sealed class TaskbarIconTests
MeetingTaskbarExitPolicy.RequiresConfirmation(state)); MeetingTaskbarExitPolicy.RequiresConfirmation(state));
} }
[WindowsFact("System.Drawing bitmap rendering")] [Fact]
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public void TaskbarIconGlyphsAreVisuallyCentered() public void TaskbarIconGlyphsAreVisuallyCentered()
{ {
if (!OperatingSystem.IsWindows())
{
return;
}
foreach (var state in new[] foreach (var state in new[]
{ {
RecordingProcessState.Idle, RecordingProcessState.Idle,
@@ -1,72 +0,0 @@
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}");
}
}
@@ -1,12 +0,0 @@
namespace MeetingAssistant.Tests;
public sealed class WindowsFactAttribute : FactAttribute
{
public WindowsFactAttribute(string requiredCapability)
{
if (!OperatingSystem.IsWindows())
{
Skip = $"Requires Windows {requiredCapability}; run on Windows.";
}
}
}
@@ -1,109 +0,0 @@
#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
@@ -1,43 +0,0 @@
using MeetingAssistant.Notifications;
namespace MeetingAssistant.MacOs;
public static class DesktopCommunicationEndpoints
{
public static void MapDesktopCommunication(this WebApplication app)
{
app.MapGet("/desktop/requests", (DesktopUserRequestStore requests) => Results.Ok(requests.List()));
app.MapPost("/desktop/requests/{id}/respond", async (
string id, DesktopUserRequestResponse response, DesktopUserRequestStore requests) =>
{
if (string.IsNullOrEmpty(response.Action)) return Results.BadRequest();
return await requests.RespondAsync(id, response.Action) switch
{
DesktopUserRequestRespondResult.Accepted => Results.Ok(new { status = "accepted" }),
DesktopUserRequestRespondResult.InvalidAction => Results.BadRequest(new { status = "invalid-action" }),
DesktopUserRequestRespondResult.NoLongerPending => Results.StatusCode(StatusCodes.Status410Gone),
_ => Results.Problem("The user action could not be completed.")
};
});
app.MapGet("/desktop/agent-windows", (MacOsWorkflowRulesEditorSessions sessions) =>
Results.Ok(sessions.GetAgentWindows()));
app.MapGet("/diagnostics/settings-and-logs", () =>
Results.Content(MacOsWorkflowRulesEditorPage.Html, "text/html"));
app.MapGet("/diagnostics/settings-and-logs/chat", IResult (
string? session, MacOsWorkflowRulesEditorSessions sessions) =>
{
return sessions.TryGetSession(session, out var chat)
? Results.Ok(chat.GetSnapshot())
: Results.NotFound();
});
app.MapPost("/diagnostics/settings-and-logs/chat", async Task<IResult> (
string? session, MacOsWorkflowRulesEditorChatRequest request,
MacOsWorkflowRulesEditorSessions sessions, CancellationToken cancellationToken) =>
{
if (!sessions.TryGetSession(session, out var chat)) return Results.NotFound();
return Results.Ok(await chat.SendAsync(request.Message, cancellationToken));
});
}
}
public sealed record DesktopUserRequestResponse(string? Action);
@@ -1,23 +0,0 @@
using MeetingAssistant.Notifications;
namespace MeetingAssistant.MacOs;
public sealed class MacOsAgentApprovalPromptService(DesktopUserRequestStore requests) : IAgentApprovalPromptService
{
public async Task<bool> ConfirmAsync(AgentApprovalPrompt prompt, CancellationToken cancellationToken)
{
var request = requests.Add(new DesktopUserRequestSpec(
"agent-approval", prompt.Title, prompt.Message, prompt.Timeout,
[new("yes", "Yes"), new("no", "No")]), cancellationToken);
try
{
var response = await request.Decision.WaitAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
return response == "yes";
}
finally
{
requests.Dismiss(request.Id);
}
}
}
@@ -33,13 +33,7 @@ public sealed class MacOsDesktopControlService : IHostedService, IDisposable
return Task.CompletedTask; return Task.CompletedTask;
} }
var helperPath = Path.Combine( var helperPath = Path.Combine(AppContext.BaseDirectory, "Native", "macos-desktop-controls");
AppContext.BaseDirectory,
"Native",
"MeetingAssistantDesktopControls.app",
"Contents",
"MacOS",
"macos-desktop-controls");
if (!File.Exists(helperPath)) if (!File.Exists(helperPath))
{ {
logger.LogWarning("macOS desktop controls helper was not found at {HelperPath}", helperPath); logger.LogWarning("macOS desktop controls helper was not found at {HelperPath}", helperPath);
@@ -1,30 +0,0 @@
using MeetingAssistant.Notifications;
using MeetingAssistant.Recording;
namespace MeetingAssistant.MacOs;
public sealed class MacOsMeetingInactivityPromptService(DesktopUserRequestStore requests) : IMeetingInactivityPromptService
{
private const string Kind = "meeting-inactivity";
public Task ShowStopPromptAsync(MeetingInactivityPromptRequest request,
Func<MeetingInactivityPromptResponse, CancellationToken, Task> handleResponseAsync,
CancellationToken cancellationToken)
{
requests.Add(new DesktopUserRequestSpec(
Kind, "Stop meeting?",
$"No transcript text has arrived for {request.InactivityDuration.TotalMinutes:0.#} minutes.",
TimeSpan.FromMinutes(1),
[new("stop", "Stop"), new("continue", "Continue"), new("pause", "Pause transcription")]),
cancellationToken,
(action, token) => handleResponseAsync(MeetingInactivityPromptActions.ParseResponse(action), token));
return Task.CompletedTask;
}
public Task DismissAllAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
requests.DismissKind(Kind);
return Task.CompletedTask;
}
}
@@ -1,6 +1,5 @@
using System.Text.Json; using System.Text.Json;
using MeetingAssistant.Calendar; using MeetingAssistant.Calendar;
using MeetingAssistant.Notifications;
using MeetingAssistant.MeetingNotes; using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Screenshots; using MeetingAssistant.Screenshots;
using MeetingAssistant.Transcription; using MeetingAssistant.Transcription;
@@ -363,30 +362,33 @@ internal sealed class MacOsCalendarMeetingProvider : ICalendarMeetingProvider
} }
} }
internal sealed class MacOsMeetingStartPromptService( internal sealed class MacOsMeetingStartPromptService : IMeetingStartPromptService
DesktopUserRequestStore requests, TimeProvider timeProvider) : IMeetingStartPromptService
{ {
public Task ShowPromptAsync(MeetingStartPromptRequest request, private readonly IMacOsMeetingPromptClient client;
private readonly ILogger<MacOsMeetingStartPromptService> logger;
public MacOsMeetingStartPromptService(
IMacOsMeetingPromptClient client,
ILogger<MacOsMeetingStartPromptService> logger)
{
this.client = client;
this.logger = logger;
}
public async Task ShowPromptAsync(
MeetingStartPromptRequest request,
Func<MeetingStartPromptResponse, CancellationToken, Task> handleResponseAsync, Func<MeetingStartPromptResponse, CancellationToken, Task> handleResponseAsync,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested(); try
var timeout = request.Meeting.End - timeProvider.GetUtcNow();
if (timeout <= TimeSpan.Zero) return Task.CompletedTask;
if (timeout > TimeSpan.FromMinutes(5)) timeout = TimeSpan.FromMinutes(5);
var actions = new List<DesktopRequestAction> { new("record", "Record"), new("skip", "Skip") };
if (request.CanAttachToCurrentMeeting) actions.Add(new("attach-metadata", "Add metadata to current meeting"));
requests.Add(new DesktopUserRequestSpec(
"meeting-start", "Record meeting?",
$"{request.Meeting.Subject} starts at {request.Meeting.Start.LocalDateTime:t}.",
timeout, actions), cancellationToken,
(action, token) => handleResponseAsync(action switch
{ {
"record" => MeetingStartPromptResponse.Record, var response = await client.ShowMeetingStartPromptAsync(request, cancellationToken);
"attach-metadata" => MeetingStartPromptResponse.AttachMetadataToCurrentMeeting, await handleResponseAsync(response, cancellationToken);
_ => MeetingStartPromptResponse.Skip }
}, token)); catch (Exception exception) when (exception is not OperationCanceledException)
return Task.CompletedTask; {
logger.LogError(exception, "Failed to display native macOS meeting-start prompt");
}
} }
} }
@@ -1,74 +0,0 @@
using System.Collections.Concurrent;
using MeetingAssistant.Workflow;
using Microsoft.Extensions.Options;
namespace MeetingAssistant.MacOs;
public sealed class MacOsWorkflowRulesEditorSessions
{
private readonly MacOsWorkflowRulesEditorSession defaultSession;
private readonly ConcurrentDictionary<string, MacOsWorkflowRulesEditorSession> sessions = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, MacOsAgentWindowRequest> windows = new(StringComparer.Ordinal);
private readonly IWorkflowRulesEditorChatPipeline pipeline;
private readonly string agentUrl;
private readonly CancellationToken applicationStopping;
private readonly ILogger<MacOsWorkflowRulesEditorSessions> logger;
public MacOsWorkflowRulesEditorSessions(
IWorkflowRulesEditorChatPipeline pipeline,
IOptions<MeetingAssistantOptions> options,
IHostApplicationLifetime applicationLifetime,
ILogger<MacOsWorkflowRulesEditorSessions> logger)
{
this.pipeline = pipeline;
this.logger = logger;
applicationStopping = applicationLifetime.ApplicationStopping;
agentUrl = $"{options.Value.Api.PublicBaseUrl.TrimEnd('/')}/diagnostics/settings-and-logs";
defaultSession = CreateSession();
}
public bool TryGetSession(string? id, out MacOsWorkflowRulesEditorSession session)
{
if (string.IsNullOrEmpty(id))
{
session = defaultSession;
return true;
}
return sessions.TryGetValue(id, out session!);
}
public IReadOnlyList<MacOsAgentWindowRequest> GetAgentWindows() =>
applicationStopping.IsCancellationRequested ? [] : windows.Values.ToArray();
public void OpenWithPrompt(string initialPrompt)
{
ArgumentException.ThrowIfNullOrWhiteSpace(initialPrompt);
applicationStopping.ThrowIfCancellationRequested();
var id = Guid.NewGuid().ToString("N");
var session = CreateSession();
sessions[id] = session;
var task = session.SendAsync(initialPrompt, applicationStopping);
windows[id] = new MacOsAgentWindowRequest(id, $"{agentUrl}?session={id}");
_ = ObservePromptAsync(task, id);
}
private MacOsWorkflowRulesEditorSession CreateSession() =>
new(new WorkflowRulesEditorChatViewModel(pipeline), applicationStopping);
private async Task ObservePromptAsync(Task task, string id)
{
try
{
await task;
}
catch (OperationCanceledException) when (applicationStopping.IsCancellationRequested)
{
}
catch (Exception exception)
{
logger.LogError(exception, "Could not start approved workflow conversation {SessionId}", id);
}
}
}
public sealed record MacOsAgentWindowRequest(string Id, string Url);
@@ -7,14 +7,10 @@ namespace MeetingAssistant.MacOs;
public sealed class MacOsWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService public sealed class MacOsWorkflowRulesEditorWindowService : IWorkflowRulesEditorWindowService
{ {
private readonly IOptions<MeetingAssistantOptions> options; private readonly IOptions<MeetingAssistantOptions> options;
private readonly Func<MacOsWorkflowRulesEditorSessions> getSessions;
public MacOsWorkflowRulesEditorWindowService( public MacOsWorkflowRulesEditorWindowService(IOptions<MeetingAssistantOptions> options)
IOptions<MeetingAssistantOptions> options,
Func<MacOsWorkflowRulesEditorSessions> getSessions)
{ {
this.options = options; this.options = options;
this.getSessions = getSessions;
} }
public void Show() public void Show()
@@ -35,82 +31,59 @@ public sealed class MacOsWorkflowRulesEditorWindowService : IWorkflowRulesEditor
public void ShowWithPrompt(string initialPrompt) public void ShowWithPrompt(string initialPrompt)
{ {
getSessions().OpenWithPrompt(initialPrompt); throw new PlatformNotSupportedException(
"Automatic workflow-change conversations require the Windows approval UI. Use the interactive macOS agent instead.");
} }
} }
public sealed class MacOsWorkflowRulesEditorSession public sealed class MacOsWorkflowRulesEditorSession
{ {
private readonly WorkflowRulesEditorChatViewModel viewModel; private readonly WorkflowRulesEditorChatViewModel viewModel;
private readonly CancellationToken applicationStopping;
private readonly SemaphoreSlim sendLock = new(1, 1); private readonly SemaphoreSlim sendLock = new(1, 1);
private MacOsWorkflowRulesEditorChatResponse snapshot = new([], false);
public MacOsWorkflowRulesEditorSession( public MacOsWorkflowRulesEditorSession(WorkflowRulesEditorChatViewModel viewModel)
WorkflowRulesEditorChatViewModel viewModel,
CancellationToken applicationStopping = default)
{ {
this.viewModel = viewModel; this.viewModel = viewModel;
this.applicationStopping = applicationStopping;
} }
public Task<MacOsWorkflowRulesEditorChatResponse> SendAsync( public async Task<MacOsWorkflowRulesEditorChatResponse> SendAsync(
string? message, string? message,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(message)) if (string.IsNullOrWhiteSpace(message))
{ {
return Task.FromResult(GetSnapshot()); return Snapshot();
} }
// The response connection controls only its wait; accepted agent work belongs to the application. await sendLock.WaitAsync(cancellationToken);
return SendCoreAsync(message.Trim()).WaitAsync(cancellationToken);
}
public MacOsWorkflowRulesEditorChatResponse GetSnapshot() => Volatile.Read(ref snapshot);
private async Task<MacOsWorkflowRulesEditorChatResponse> SendCoreAsync(string message)
{
await sendLock.WaitAsync(applicationStopping);
try try
{ {
applicationStopping.ThrowIfCancellationRequested(); viewModel.Draft = message;
var messages = GetSnapshot().Messages await viewModel.SendAsync(cancellationToken);
.Append(new MacOsWorkflowRulesEditorMessage("user", message)) return Snapshot();
.ToArray();
Volatile.Write(ref snapshot, new MacOsWorkflowRulesEditorChatResponse(
Array.AsReadOnly(messages), true));
try
{
// Dispatch the agent away from the caller so opening a window or processing a decision stays responsive.
await Task.Run(() => viewModel.SendPromptAsync(message, applicationStopping), applicationStopping);
}
finally
{
var completedMessages = viewModel.Messages
.Where(item => item.Kind == WorkflowRulesEditorConversationItemKind.Message)
.Select(item => new MacOsWorkflowRulesEditorMessage(
item.Message!.Role.ToString().ToLowerInvariant(),
item.Message.Content))
.ToArray();
Volatile.Write(ref snapshot, new MacOsWorkflowRulesEditorChatResponse(
Array.AsReadOnly(completedMessages), false));
}
return GetSnapshot();
} }
finally finally
{ {
sendLock.Release(); sendLock.Release();
} }
} }
private MacOsWorkflowRulesEditorChatResponse Snapshot()
{
return new MacOsWorkflowRulesEditorChatResponse(
viewModel.Messages
.Where(item => item.Kind == WorkflowRulesEditorConversationItemKind.Message)
.Select(item => new MacOsWorkflowRulesEditorMessage(
item.Message!.Role.ToString().ToLowerInvariant(),
item.Message.Content))
.ToArray());
}
} }
public sealed record MacOsWorkflowRulesEditorChatRequest(string? Message); public sealed record MacOsWorkflowRulesEditorChatRequest(string? Message);
public sealed record MacOsWorkflowRulesEditorChatResponse( public sealed record MacOsWorkflowRulesEditorChatResponse(
IReadOnlyList<MacOsWorkflowRulesEditorMessage> Messages, IReadOnlyList<MacOsWorkflowRulesEditorMessage> Messages);
bool IsThinking = false);
public sealed record MacOsWorkflowRulesEditorMessage(string Role, string Content); public sealed record MacOsWorkflowRulesEditorMessage(string Role, string Content);
@@ -153,13 +126,6 @@ public static class MacOsWorkflowRulesEditorPage
const prompt = document.querySelector('#prompt'); const prompt = document.querySelector('#prompt');
const send = document.querySelector('#send'); const send = document.querySelector('#send');
const messages = document.querySelector('#messages'); const messages = document.querySelector('#messages');
const session = new URLSearchParams(location.search).get('session');
const chatUrl = '/diagnostics/settings-and-logs/chat' + (session ? '?session=' + encodeURIComponent(session) : '');
let submitting = false;
let thinking = false;
let refreshing = false;
let available = true;
let refreshTimer;
const render = items => { const render = items => {
messages.replaceChildren(...items.map(item => { messages.replaceChildren(...items.map(item => {
const card = document.createElement('div'); const card = document.createElement('div');
@@ -169,57 +135,22 @@ public static class MacOsWorkflowRulesEditorPage
})); }));
messages.scrollTop = messages.scrollHeight; messages.scrollTop = messages.scrollHeight;
}; };
const update = body => {
render(body.messages);
thinking = !!body.isThinking;
send.disabled = submitting || thinking;
};
const showError = text => {
const card = document.createElement('div');
card.className = 'message agent';
card.textContent = text;
messages.append(card);
};
const refresh = async () => {
if (refreshing || !available) return;
clearTimeout(refreshTimer);
refreshing = true;
try {
const response = await fetch(chatUrl, {cache: 'no-store'});
if (!response.ok) throw new Error('This conversation is no longer available.');
update(await response.json());
} catch (error) {
available = false;
send.disabled = true;
showError(error.message);
} finally {
refreshing = false;
if (available && (thinking || submitting)) refreshTimer = setTimeout(refresh, 300);
}
};
form.addEventListener('submit', async event => { form.addEventListener('submit', async event => {
event.preventDefault(); event.preventDefault();
const message = prompt.value.trim(); const message = prompt.value.trim();
if (!message || submitting || thinking || !available) return; if (!message) return;
prompt.value = ''; prompt.value = '';
submitting = true;
send.disabled = true; send.disabled = true;
try { try {
const responseTask = fetch(chatUrl, { const response = await fetch('/diagnostics/settings-and-logs/chat', {
method: 'POST', method: 'POST',
headers: {'content-type': 'application/json'}, headers: {'content-type': 'application/json'},
body: JSON.stringify({message}) body: JSON.stringify({message})
}); });
void refresh(); const body = await response.json();
const response = await responseTask; render(body.messages);
if (!response.ok) throw new Error('The message could not be sent.');
update(await response.json());
} catch (error) {
showError(error.message);
} finally { } finally {
submitting = false; send.disabled = false;
send.disabled = !available || thinking;
void refresh();
prompt.focus(); prompt.focus();
} }
}); });
@@ -229,7 +160,6 @@ public static class MacOsWorkflowRulesEditorPage
form.requestSubmit(); form.requestSubmit();
} }
}); });
void refresh();
</script> </script>
</body> </body>
</html> </html>
+3 -14
View File
@@ -26,11 +26,7 @@
<MacOsAudioCaptureArchitecture Condition="'$(MacOsAudioCaptureRid)' == 'osx-x64'">x86_64</MacOsAudioCaptureArchitecture> <MacOsAudioCaptureArchitecture Condition="'$(MacOsAudioCaptureRid)' == 'osx-x64'">x86_64</MacOsAudioCaptureArchitecture>
<MacOsAudioCaptureArchitecture Condition="'$(MacOsAudioCaptureArchitecture)' == ''">arm64</MacOsAudioCaptureArchitecture> <MacOsAudioCaptureArchitecture Condition="'$(MacOsAudioCaptureArchitecture)' == ''">arm64</MacOsAudioCaptureArchitecture>
<MacOsDesktopControlsSource>$(MSBuildProjectDirectory)/Native/MacOsDesktopControls/main.swift</MacOsDesktopControlsSource> <MacOsDesktopControlsSource>$(MSBuildProjectDirectory)/Native/MacOsDesktopControls/main.swift</MacOsDesktopControlsSource>
<MacOsDesktopControlsRequestsSource>$(MSBuildProjectDirectory)/Native/MacOsDesktopControls/DesktopUserRequests.swift</MacOsDesktopControlsRequestsSource> <MacOsDesktopControlsOutputPath>Native/macos-desktop-controls</MacOsDesktopControlsOutputPath>
<MacOsDesktopControlsSelfTestsSource>$(MSBuildProjectDirectory)/Native/MacOsDesktopControls/SelfTests.swift</MacOsDesktopControlsSelfTestsSource>
<MacOsDesktopControlsInfoPlist>$(MSBuildProjectDirectory)/Native/MacOsDesktopControls/Info.plist</MacOsDesktopControlsInfoPlist>
<MacOsDesktopControlsBundlePath>Native/MeetingAssistantDesktopControls.app</MacOsDesktopControlsBundlePath>
<MacOsDesktopControlsOutputPath>$(MacOsDesktopControlsBundlePath)/Contents/MacOS/macos-desktop-controls</MacOsDesktopControlsOutputPath>
<MacOsMeetingIntegrationsSource>$(MSBuildProjectDirectory)/Native/MacOsMeetingIntegrations/main.swift</MacOsMeetingIntegrationsSource> <MacOsMeetingIntegrationsSource>$(MSBuildProjectDirectory)/Native/MacOsMeetingIntegrations/main.swift</MacOsMeetingIntegrationsSource>
<MacOsMeetingIntegrationsInfoPlist>$(MSBuildProjectDirectory)/Native/MacOsMeetingIntegrations/Info.plist</MacOsMeetingIntegrationsInfoPlist> <MacOsMeetingIntegrationsInfoPlist>$(MSBuildProjectDirectory)/Native/MacOsMeetingIntegrations/Info.plist</MacOsMeetingIntegrationsInfoPlist>
<MacOsMeetingIntegrationsOutputPath>Native/macos-meeting-integrations</MacOsMeetingIntegrationsOutputPath> <MacOsMeetingIntegrationsOutputPath>Native/macos-meeting-integrations</MacOsMeetingIntegrationsOutputPath>
@@ -96,11 +92,7 @@
<Copy SourceFiles="$(MacOsAudioCaptureInfoPlist)" DestinationFiles="$(TargetDir)$(MacOsAudioCaptureBundlePath)/Contents/Info.plist" /> <Copy SourceFiles="$(MacOsAudioCaptureInfoPlist)" DestinationFiles="$(TargetDir)$(MacOsAudioCaptureBundlePath)/Contents/Info.plist" />
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AVFoundation -framework CoreMedia -framework ScreenCaptureKit -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker &quot;$(MacOsAudioCaptureInfoPlist)&quot; &quot;$(MacOsAudioCaptureSource)&quot; -o &quot;$(TargetDir)$(MacOsAudioCaptureOutputPath)&quot;" /> <Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AVFoundation -framework CoreMedia -framework ScreenCaptureKit -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker &quot;$(MacOsAudioCaptureInfoPlist)&quot; &quot;$(MacOsAudioCaptureSource)&quot; -o &quot;$(TargetDir)$(MacOsAudioCaptureOutputPath)&quot;" />
<Exec Command="/usr/bin/codesign --force --deep --sign - &quot;$(TargetDir)$(MacOsAudioCaptureBundlePath)&quot;" /> <Exec Command="/usr/bin/codesign --force --deep --sign - &quot;$(TargetDir)$(MacOsAudioCaptureBundlePath)&quot;" />
<MakeDir Directories="$(TargetDir)$(MacOsDesktopControlsBundlePath)/Contents/MacOS" /> <Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework Carbon -framework WebKit &quot;$(MacOsDesktopControlsSource)&quot; -o &quot;$(TargetDir)$(MacOsDesktopControlsOutputPath)&quot;" />
<Copy SourceFiles="$(MacOsDesktopControlsInfoPlist)" DestinationFiles="$(TargetDir)$(MacOsDesktopControlsBundlePath)/Contents/Info.plist" />
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework Carbon -framework WebKit -framework UserNotifications &quot;$(MacOsDesktopControlsSource)&quot; &quot;$(MacOsDesktopControlsRequestsSource)&quot; &quot;$(MacOsDesktopControlsSelfTestsSource)&quot; -o &quot;$(TargetDir)$(MacOsDesktopControlsOutputPath)&quot;" />
<Exec Command="/usr/bin/codesign --force --deep --sign - --requirements '=designated =&gt; identifier &quot;cloud.schweigert.meeting-assistant.desktop-controls&quot;' &quot;$(TargetDir)$(MacOsDesktopControlsBundlePath)&quot;" />
<Exec Command="&quot;$(TargetDir)$(MacOsDesktopControlsOutputPath)&quot; --self-test" Condition="'$(MacOsAudioCaptureRid)' == '$(NETCoreSdkRuntimeIdentifier)'" />
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework CoreGraphics -framework EventKit -framework ImageIO -framework UniformTypeIdentifiers -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker &quot;$(MacOsMeetingIntegrationsInfoPlist)&quot; &quot;$(MacOsMeetingIntegrationsSource)&quot; -o &quot;$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)&quot;" /> <Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework CoreGraphics -framework EventKit -framework ImageIO -framework UniformTypeIdentifiers -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker &quot;$(MacOsMeetingIntegrationsInfoPlist)&quot; &quot;$(MacOsMeetingIntegrationsSource)&quot; -o &quot;$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)&quot;" />
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 &quot;$(MacOsLauncherSource)&quot; -o &quot;$(TargetDir)$(MacOsLauncherOutputPath)&quot;" /> <Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 &quot;$(MacOsLauncherSource)&quot; -o &quot;$(TargetDir)$(MacOsLauncherOutputPath)&quot;" />
</Target> </Target>
@@ -133,10 +125,7 @@
<Copy SourceFiles="$(TargetDir)$(MacOsAudioCaptureOutputPath)" DestinationFiles="$(PublishDir)$(MacOsAudioCaptureOutputPath)" /> <Copy SourceFiles="$(TargetDir)$(MacOsAudioCaptureOutputPath)" DestinationFiles="$(PublishDir)$(MacOsAudioCaptureOutputPath)" />
<Copy SourceFiles="$(MacOsAudioCaptureInfoPlist)" DestinationFiles="$(PublishDir)$(MacOsAudioCaptureBundlePath)/Contents/Info.plist" /> <Copy SourceFiles="$(MacOsAudioCaptureInfoPlist)" DestinationFiles="$(PublishDir)$(MacOsAudioCaptureBundlePath)/Contents/Info.plist" />
<Exec Command="/usr/bin/codesign --force --deep --sign - &quot;$(PublishDir)$(MacOsAudioCaptureBundlePath)&quot;" /> <Exec Command="/usr/bin/codesign --force --deep --sign - &quot;$(PublishDir)$(MacOsAudioCaptureBundlePath)&quot;" />
<MakeDir Directories="$(PublishDir)$(MacOsDesktopControlsBundlePath)/Contents/MacOS" /> <Copy SourceFiles="$(TargetDir)$(MacOsDesktopControlsOutputPath)" DestinationFolder="$(PublishDir)Native" />
<Copy SourceFiles="$(TargetDir)$(MacOsDesktopControlsOutputPath)" DestinationFiles="$(PublishDir)$(MacOsDesktopControlsOutputPath)" />
<Copy SourceFiles="$(MacOsDesktopControlsInfoPlist)" DestinationFiles="$(PublishDir)$(MacOsDesktopControlsBundlePath)/Contents/Info.plist" />
<Exec Command="/usr/bin/codesign --force --deep --sign - --requirements '=designated =&gt; identifier &quot;cloud.schweigert.meeting-assistant.desktop-controls&quot;' &quot;$(PublishDir)$(MacOsDesktopControlsBundlePath)&quot;" />
<Copy SourceFiles="$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)" DestinationFolder="$(PublishDir)Native" /> <Copy SourceFiles="$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)" DestinationFolder="$(PublishDir)Native" />
</Target> </Target>
@@ -1,115 +0,0 @@
import Foundation
import UserNotifications
struct DesktopUserAction: Decodable, Equatable {
let id: String
let label: String
}
struct DesktopUserRequest: Decodable {
let id: String
let kind: String
let title: String
let message: String
let createdAt: Date
let expiresAt: Date
let actions: [DesktopUserAction]
}
struct DesktopAgentWindow: Decodable {
let id: String
let url: String
}
func decodeDesktopRequests(_ data: Data) throws -> [DesktopUserRequest] {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let value = try decoder.singleValueContainer().decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: value) { return date }
formatter.formatOptions = [.withInternetDateTime]
if let date = formatter.date(from: value) { return date }
throw DecodingError.dataCorruptedError(in: try decoder.singleValueContainer(), debugDescription: "Invalid request timestamp")
}
return try decoder.decode([DesktopUserRequest].self, from: data)
}
struct DesktopUserRequestState {
private(set) var pending: [DesktopUserRequest] = []
private var announced: Set<String> = []
private var responding: Set<String> = []
private var completed: Set<String> = []
mutating func beginResponse(baseUrl: String, requestId: String, action: String, now: Date) -> URLRequest? {
guard !responding.contains(requestId),
let pendingRequest = pending.first(where: { $0.id == requestId }),
pendingRequest.expiresAt > now,
pendingRequest.actions.contains(where: { $0.id == action }),
let id = requestId.addingPercentEncoding(withAllowedCharacters: .alphanumerics.union(CharacterSet(charactersIn: "-._~"))),
let url = URL(string: baseUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/desktop/requests/\(id)/respond"),
let body = try? JSONEncoder().encode(["action": action]) else { return nil }
var response = URLRequest(url: url)
response.httpMethod = "POST"
response.setValue("application/json", forHTTPHeaderField: "Content-Type")
response.httpBody = body
responding.insert(requestId)
return response
}
mutating func completeResponse(_ id: String, accepted: Bool) {
responding.remove(id)
if accepted { completed.insert(id); pending.removeAll { $0.id == id } }
}
func isResponding(_ id: String) -> Bool { responding.contains(id) }
mutating func reconcile(_ requests: [DesktopUserRequest], now: Date) -> [String] {
let previousIds = Set(pending.map(\.id))
pending = requests.filter { $0.expiresAt > now && !completed.contains($0.id) }.sorted {
$0.createdAt == $1.createdAt ? $0.id < $1.id : $0.createdAt < $1.createdAt
}
let currentIds = Set(pending.map(\.id))
return previousIds.subtracting(currentIds).sorted()
}
func notificationsToAnnounce() -> [DesktopUserRequest] {
pending.filter { !announced.contains($0.id) && !responding.contains($0.id) }
}
mutating func markAnnounced(_ id: String) {
if pending.contains(where: { $0.id == id }) { announced.insert(id) }
}
}
extension DesktopUserRequest {
var notificationIdentifier: String { "desktop-request." + id }
func notificationCategory() -> UNNotificationCategory {
UNNotificationCategory(
identifier: notificationIdentifier,
actions: actions.map { UNNotificationAction(identifier: $0.id, title: $0.label, options: []) },
intentIdentifiers: [],
options: [.customDismissAction])
}
func notificationRequest() -> UNNotificationRequest {
let content = UNMutableNotificationContent()
content.title = title
content.body = message
content.categoryIdentifier = notificationIdentifier
content.userInfo = ["requestId": id]
content.sound = .default
return UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: nil)
}
}
struct DesktopAgentWindowState {
private var opened: Set<String> = []
mutating func takeUnopened(_ windows: [DesktopAgentWindow]) -> [DesktopAgentWindow] {
windows.filter { opened.insert($0.id).inserted }
}
}
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>macos-desktop-controls</string>
<key>CFBundleIdentifier</key>
<string>cloud.schweigert.meeting-assistant.desktop-controls</string>
<key>CFBundleName</key>
<string>Meeting Assistant</string>
<key>CFBundleDisplayName</key>
<string>Meeting Assistant</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
@@ -1,67 +0,0 @@
import Foundation
private enum SelfTestFailure: Error { case failed(String) }
private func expect(_ condition: @autoclosure () -> Bool, _ message: String) throws {
if !condition() { throw SelfTestFailure.failed(message) }
}
func runDesktopControlSelfTests() {
do {
let status = try JSONDecoder().decode(RecordingStatus.self, from: Data(#"{"isRecording":false,"state":2,"launchProfile":null}"#.utf8))
try expect(String(describing: status.state).lowercased() == "summarizing", "numeric status 2 must show summarizing")
print("PASS numeric recording status")
let requests = try decodeDesktopRequests(Data(requestFixture.utf8))
try expect(requests.count == 1 && requests[0].id == "approval-1", "pending request must decode")
try expect(requests[0].actions == [DesktopUserAction(id: "yes", label: "Yes"), DesktopUserAction(id: "no", label: "No")], "offered actions must be preserved")
try expect(requests[0].expiresAt.timeIntervalSince1970 == 1_791_022_260, "offset and fractional timestamps must decode")
print("PASS pending request wire payload")
var state = DesktopUserRequestState()
let now = Date(timeIntervalSince1970: 1_791_022_220)
_ = state.reconcile(requests, now: now)
try expect(state.notificationsToAnnounce().map(\.id) == ["approval-1"], "new pending request must be announced")
state.markAnnounced("approval-1")
_ = state.reconcile(requests, now: now)
try expect(state.notificationsToAnnounce().isEmpty && state.pending.count == 1, "repeated polling must not redeliver or hide menu requests")
try expect(state.reconcile([], now: now) == ["approval-1"] && state.pending.isEmpty, "withdrawn request must remove native presentation")
print("PASS notification reconciliation and menu fallback")
_ = state.reconcile(requests, now: now)
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "not-offered", now: now) == nil, "invalid action must keep request pending")
guard let response = state.beginResponse(baseUrl: "http://127.0.0.1:5090/", requestId: "approval-1", action: "yes", now: now) else { throw SelfTestFailure.failed("offered action must produce a response") }
try expect(response.url?.absoluteString == "http://127.0.0.1:5090/desktop/requests/approval-1/respond" && response.httpMethod == "POST", "notification and menu response endpoint")
let body = try JSONDecoder().decode([String: String].self, from: response.httpBody ?? Data())
try expect(body == ["action": "yes"], "chosen action must use shared JSON contract")
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "yes", now: now) == nil, "same pending action must not submit twice")
state.completeResponse("approval-1", accepted: false)
try expect(!state.isResponding("approval-1") && state.pending.count == 1, "failed transport must leave menu fallback actionable")
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "yes", now: requests[0].expiresAt) == nil, "expired response must not submit")
try expect(state.reconcile(requests, now: requests[0].expiresAt) == ["approval-1"] && state.pending.isEmpty, "expired request must be removed locally")
try expect(state.beginResponse(baseUrl: "http://127.0.0.1:5090", requestId: "approval-1", action: "yes", now: now) == nil, "stale notification must not reconstruct request")
print("PASS allowed once-only response and stale action rejection")
_ = state.reconcile(requests, now: now)
state.completeResponse("approval-1", accepted: true)
_ = state.reconcile(requests, now: now)
try expect(state.pending.isEmpty && state.notificationsToAnnounce().isEmpty, "old HTTP snapshot must not resurrect accepted request")
print("PASS acknowledged response survives stale polling")
let category = requests[0].notificationCategory()
let notification = requests[0].notificationRequest()
try expect(category.actions.map(\.identifier) == ["yes", "no"], "native notification must offer same actions as menu")
try expect(category.actions.map(\.title) == ["Yes", "No"], "native action labels")
try expect(notification.content.title == "Associate project?" && notification.content.body == "Add project Delta to this meeting?", "native notification must show pending request text")
try expect(notification.content.categoryIdentifier == category.identifier && notification.content.userInfo["requestId"] as? String == "approval-1", "native actions must route original request id")
try expect(category.options.contains(.customDismissAction), "notification dismissal must leave menu decision pending")
print("PASS actionable native notification payload")
let windows = try JSONDecoder().decode([DesktopAgentWindow].self, from: Data(#"[{"id":"workflow-1","url":"http://127.0.0.1:5090/desktop/agent-sessions/workflow-1"},{"id":"workflow-2","url":"http://127.0.0.1:5090/desktop/agent-sessions/workflow-2"}]"#.utf8))
var windowState = DesktopAgentWindowState()
try expect(windowState.takeUnopened(windows).map(\.id) == ["workflow-1", "workflow-2"], "two approved workflows must request independent windows")
try expect(windowState.takeUnopened(windows).isEmpty, "polling must not reopen independent windows")
print("PASS independent agent window wire payload and opening")
} catch {
fputs("FAIL desktop controls self-test: \(error)\n", stderr)
exit(1)
}
}
private let requestFixture = #"""
[{"id":"approval-1","kind":"approval","title":"Associate project?","message":"Add project Delta to this meeting?","createdAt":"2026-10-03T12:10:00.1234567+02:00","expiresAt":"2026-10-03T12:11:00+02:00","actions":[{"id":"yes","label":"Yes"},{"id":"no","label":"No"}]}]
"""#
@@ -2,7 +2,6 @@ import AppKit
import Carbon import Carbon
import Foundation import Foundation
import WebKit import WebKit
import UserNotifications
struct Manifest: Decodable { struct Manifest: Decodable {
let baseUrl: String let baseUrl: String
@@ -22,36 +21,20 @@ struct Hotkey: Decodable {
let endpoint: String let endpoint: String
} }
enum RecordingState: Int, Decodable {
case idle = 0
case recording = 1
case summarizing = 2
}
struct RecordingStatus: Decodable { struct RecordingStatus: Decodable {
let isRecording: Bool let isRecording: Bool
let state: RecordingState let state: String
let launchProfile: String? let launchProfile: String?
} }
@main @main
@MainActor final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate {
final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UNUserNotificationCenterDelegate {
private let manifest: Manifest private let manifest: Manifest
private var status = RecordingStatus(isRecording: false, state: .idle, launchProfile: nil) private var status = RecordingStatus(isRecording: false, state: "Idle", launchProfile: nil)
private var statusItem: NSStatusItem! private var statusItem: NSStatusItem!
private var registeredHotkeys: [UInt32: Hotkey] = [:] private var registeredHotkeys: [UInt32: Hotkey] = [:]
private var timer: Timer? private var timer: Timer?
private var agentWindow: NSWindow? private var agentWindow: NSWindow?
private var independentAgentWindows: [String: NSWindow] = [:]
private var agentWindowState = DesktopAgentWindowState()
private var requests = DesktopUserRequestState()
private var fetchingRequests = false
private var fetchingWindows = false
private var authorizationInFlight = false
private var authorizationChecked = false
private var notificationsAuthorized = false
private lazy var notificationCenter = UNUserNotificationCenter.current()
override init() { override init() {
guard CommandLine.arguments.count == 2, guard CommandLine.arguments.count == 2,
@@ -65,19 +48,6 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
} }
static func main() { static func main() {
if CommandLine.arguments.count == 2 && CommandLine.arguments[1] == "--self-test" {
runDesktopControlSelfTests()
return
}
// Notification activation after shutdown has no trusted service manifest.
if CommandLine.arguments.count == 1 {
fputs("Ignoring desktop notification activation without an active service manifest.\n", stderr)
return
}
guard Bundle.main.bundleIdentifier == "cloud.schweigert.meeting-assistant.desktop-controls" else {
fputs("Desktop controls must run from the signed MeetingAssistantDesktopControls.app bundle.\n", stderr)
exit(2)
}
let application = NSApplication.shared let application = NSApplication.shared
let delegate = DesktopControls() let delegate = DesktopControls()
application.delegate = delegate application.delegate = delegate
@@ -85,12 +55,6 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
application.run() application.run()
} }
func applicationWillFinishLaunching(_ notification: Notification) {
notificationCenter.delegate = self
notificationCenter.removeAllPendingNotificationRequests()
notificationCenter.removeAllDeliveredNotifications()
}
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
statusItem.button?.image = NSImage( statusItem.button?.image = NSImage(
@@ -100,22 +64,19 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
menu.delegate = self menu.delegate = self
statusItem.menu = menu statusItem.menu = menu
registerHotkeys() registerHotkeys()
refresh() refreshStatus()
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
DispatchQueue.main.async { self?.refresh() } self?.refreshStatus()
} }
} }
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
timer?.invalidate() timer?.invalidate()
notificationCenter.removeAllPendingNotificationRequests()
notificationCenter.removeAllDeliveredNotifications()
} }
func menuWillOpen(_ menu: NSMenu) { func menuWillOpen(_ menu: NSMenu) {
removeNotifications(requests.reconcile(requests.pending, now: Date()))
rebuildMenu(menu) rebuildMenu(menu)
refresh() refreshStatus()
} }
private func rebuildMenu(_ menu: NSMenu) { private func rebuildMenu(_ menu: NSMenu) {
@@ -141,45 +102,17 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
} }
} }
menu.addItem(.separator()) menu.addItem(.separator())
let pending = NSMenuItem(title: "Pending requests (\(requests.pending.count))", action: nil, keyEquivalent: "")
let pendingMenu = NSMenu()
if requests.pending.isEmpty {
let empty = NSMenuItem(title: "No pending requests", action: nil, keyEquivalent: "")
empty.isEnabled = false
pendingMenu.addItem(empty)
}
for request in requests.pending {
let entry = NSMenuItem(title: request.title, action: nil, keyEquivalent: "")
entry.toolTip = request.message
let decisions = NSMenu()
let message = NSMenuItem(title: request.message, action: nil, keyEquivalent: "")
message.isEnabled = false
decisions.addItem(message)
decisions.addItem(.separator())
for action in request.actions {
let decision = item(action.label, #selector(answerRequest(_:)))
decision.representedObject = DesktopRequestDecision(request.id, action.id)
decision.isEnabled = !requests.isResponding(request.id)
decisions.addItem(decision)
}
entry.submenu = decisions
pendingMenu.addItem(entry)
}
pending.submenu = pendingMenu
menu.addItem(pending)
menu.addItem(.separator())
menu.addItem(item("Exit", #selector(exitApplication))) menu.addItem(item("Exit", #selector(exitApplication)))
} }
private var tooltip: String { private var tooltip: String {
let suffix = requests.pending.isEmpty ? "" : " — \(requests.pending.count) pending request(s)"
if status.isRecording { if status.isRecording {
return (status.launchProfile.map { "Meeting Assistant — recording (\($0))" } return status.launchProfile.map { "Meeting Assistant — recording (\($0))" }
?? "Meeting Assistant — recording") + suffix ?? "Meeting Assistant — recording"
} }
return (status.state == .summarizing return status.state.lowercased() == "summarizing"
? "Meeting Assistant — summarizing" ? "Meeting Assistant — summarizing"
: "Meeting Assistant — idle") + suffix : "Meeting Assistant — idle"
} }
private func item(_ title: String, _ action: Selector) -> NSMenuItem { private func item(_ title: String, _ action: Selector) -> NSMenuItem {
@@ -189,14 +122,7 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
} }
@objc private func openAgent() { @objc private func openAgent() {
if agentWindow == nil, let url = URL(string: manifest.openAgentUrl) { if agentWindow == nil {
agentWindow = makeAgentWindow(url)
}
NSApplication.shared.activate(ignoringOtherApps: true)
agentWindow?.makeKeyAndOrderFront(nil)
}
private func makeAgentWindow(_ url: URL) -> NSWindow {
let webView = WKWebView(frame: .zero) let webView = WKWebView(frame: .zero)
let window = NSWindow( let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 840, height: 760), contentRect: NSRect(x: 0, y: 0, width: 840, height: 760),
@@ -207,44 +133,11 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
window.contentView = webView window.contentView = webView
window.center() window.center()
window.isReleasedWhenClosed = false window.isReleasedWhenClosed = false
webView.load(URLRequest(url: url)) webView.load(URLRequest(url: URL(string: manifest.openAgentUrl)!))
return window agentWindow = window
} }
NSApplication.shared.activate(ignoringOtherApps: true)
@objc private func answerRequest(_ sender: NSMenuItem) { agentWindow?.makeKeyAndOrderFront(nil)
guard let decision = sender.representedObject as? DesktopRequestDecision else { return }
submitResponse(requestId: decision.requestId, action: decision.action)
}
private func submitResponse(requestId: String, action: String) {
guard let response = requests.beginResponse(baseUrl: manifest.baseUrl, requestId: requestId, action: action, now: Date()) else { return }
URLSession.shared.dataTask(with: response) { [weak self] _, response, error in
let accepted = error == nil && (response as? HTTPURLResponse).map { (200..<300).contains($0.statusCode) } == true
DispatchQueue.main.async {
guard let self else { return }
self.requests.completeResponse(requestId, accepted: accepted)
if accepted { self.removeNotifications([requestId]) }
if let error { desktopLog("User response failed: \(error.localizedDescription)") }
self.refresh()
}
}.resume()
}
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .list, .sound])
}
nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
// Closing the announcement is independent of deciding its menu request.
guard response.actionIdentifier != UNNotificationDismissActionIdentifier,
response.actionIdentifier != UNNotificationDefaultActionIdentifier,
let id = response.notification.request.content.userInfo["requestId"] as? String else {
completionHandler()
return
}
let action = response.actionIdentifier
DispatchQueue.main.async { [weak self] in self?.submitResponse(requestId: id, action: action) }
completionHandler()
} }
@objc private func startProfile(_ sender: NSMenuItem) { @objc private func startProfile(_ sender: NSMenuItem) {
@@ -267,114 +160,6 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
} }
} }
private func refresh() {
removeNotifications(requests.reconcile(requests.pending, now: Date()))
updateStatusItem()
refreshStatus()
refreshRequests()
refreshAgentWindows()
}
private func updateStatusItem() {
statusItem.button?.image = NSImage(
systemSymbolName: status.isRecording ? "record.circle.fill" : "waveform.circle",
accessibilityDescription: tooltip)
statusItem.button?.toolTip = tooltip
}
private func refreshRequests() {
guard !fetchingRequests, let url = URL(string: manifest.baseUrl + "/desktop/requests") else { return }
fetchingRequests = true
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
var snapshot: [DesktopUserRequest]?
if let data, error == nil, (response as? HTTPURLResponse)?.statusCode == 200 {
do { snapshot = try decodeDesktopRequests(data) }
catch { desktopLog("Cannot decode pending desktop requests: \(error)") }
}
DispatchQueue.main.async {
guard let self else { return }
self.fetchingRequests = false
guard let snapshot else { return }
self.removeNotifications(self.requests.reconcile(snapshot, now: Date()))
self.updateStatusItem()
self.announcePendingRequests()
}
}.resume()
}
private func removeNotifications(_ ids: [String]) {
guard !ids.isEmpty else { return }
let identifiers = ids.map { "desktop-request." + $0 }
notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers)
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
}
private func announcePendingRequests() {
guard !requests.notificationsToAnnounce().isEmpty else { return }
if !authorizationChecked {
guard !authorizationInFlight else { return }
authorizationInFlight = true
notificationCenter.getNotificationSettings { [weak self] settings in
DispatchQueue.main.async {
guard let self else { return }
if settings.authorizationStatus == .notDetermined {
self.notificationCenter.requestAuthorization(options: [.alert, .sound]) { [weak self] granted, error in
DispatchQueue.main.async {
if let error { desktopLog("Notification authorization failed: \(error.localizedDescription)") }
self?.finishNotificationAuthorization(granted)
}
}
} else {
self.finishNotificationAuthorization(settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional)
}
}
}
return
}
guard notificationsAuthorized else { return }
notificationCenter.setNotificationCategories(Set(requests.pending.map { $0.notificationCategory() }))
for request in requests.notificationsToAnnounce() {
requests.markAnnounced(request.id)
notificationCenter.add(request.notificationRequest()) { error in
if let error { desktopLog("Cannot schedule desktop notification \(request.id): \(error.localizedDescription)") }
else { desktopLog("Scheduled desktop notification \(request.id)") }
}
}
}
private func finishNotificationAuthorization(_ granted: Bool) {
authorizationChecked = true
authorizationInFlight = false
notificationsAuthorized = granted
desktopLog(granted ? "Desktop notifications authorized." : "Desktop notifications unavailable; pending requests remain in the menu.")
removeNotifications(requests.reconcile(requests.pending, now: Date()))
announcePendingRequests()
}
private func refreshAgentWindows() {
guard !fetchingWindows, let url = URL(string: manifest.baseUrl + "/desktop/agent-windows") else { return }
fetchingWindows = true
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
let windows = data.flatMap { data in
error == nil && (response as? HTTPURLResponse)?.statusCode == 200
? try? JSONDecoder().decode([DesktopAgentWindow].self, from: data) : nil
}
DispatchQueue.main.async {
guard let self else { return }
self.fetchingWindows = false
guard let windows else { return }
for requested in self.agentWindowState.takeUnopened(windows) {
guard let url = URL(string: requested.url) else { continue }
let window = self.makeAgentWindow(url)
self.independentAgentWindows[requested.id] = window
NSApplication.shared.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
desktopLog("Opened independent workflow agent window \(requested.id)")
}
}
}.resume()
}
private func refreshStatus() { private func refreshStatus() {
guard let url = URL(string: manifest.statusUrl) else { return } guard let url = URL(string: manifest.statusUrl) else { return }
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
@@ -382,7 +167,9 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
let next = try? JSONDecoder().decode(RecordingStatus.self, from: data) else { return } let next = try? JSONDecoder().decode(RecordingStatus.self, from: data) else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
self?.status = next self?.status = next
self?.updateStatusItem() self?.statusItem.button?.image = NSImage(
systemSymbolName: next.isRecording ? "record.circle.fill" : "waveform.circle",
accessibilityDescription: self?.tooltip)
} }
}.resume() }.resume()
} }
@@ -397,7 +184,7 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
request.httpMethod = "POST" request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { [weak self] _, _, _ in URLSession.shared.dataTask(with: request) { [weak self] _, _, _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self?.refresh() self?.refreshStatus()
} }
}.resume() }.resume()
} }
@@ -452,20 +239,6 @@ final class DesktopControls: NSObject, NSApplicationDelegate, NSMenuDelegate, UN
} }
} }
private final class DesktopRequestDecision: NSObject {
let requestId: String
let action: String
init(_ requestId: String, _ action: String) {
self.requestId = requestId
self.action = action
}
}
private func desktopLog(_ message: String) {
fputs("\(message)\n", stderr)
}
private func tokens(_ shortcut: String) -> [String] { private func tokens(_ shortcut: String) -> [String] {
shortcut.uppercased().split(separator: "+").map(String.init) shortcut.uppercased().split(separator: "+").map(String.init)
} }
@@ -1,176 +0,0 @@
namespace MeetingAssistant.Notifications;
public sealed record DesktopRequestAction(string Id, string Label);
public sealed record DesktopUserRequest(string Id, string Kind, string Title, string Message,
DateTimeOffset CreatedAt, DateTimeOffset ExpiresAt, IReadOnlyList<DesktopRequestAction> Actions);
public sealed record DesktopUserRequestSpec(string Kind, string Title, string Message,
TimeSpan Timeout, IReadOnlyList<DesktopRequestAction> Actions);
public sealed record PendingUserRequestRegistration(string Id, Task<string?> Decision);
public enum DesktopUserRequestRespondResult { Accepted, InvalidAction, NoLongerPending, Failed }
public sealed class DesktopUserRequestStore : IDisposable
{
private readonly object gate = new();
private readonly Dictionary<string, PendingRequest> pending = new(StringComparer.Ordinal);
private readonly TimeProvider timeProvider;
private readonly IHostApplicationLifetime applicationLifetime;
private readonly ILogger<DesktopUserRequestStore> logger;
private readonly CancellationTokenRegistration shutdownRegistration;
private bool disposed;
public DesktopUserRequestStore(TimeProvider timeProvider, IHostApplicationLifetime applicationLifetime,
ILogger<DesktopUserRequestStore> logger)
{
this.timeProvider = timeProvider;
this.applicationLifetime = applicationLifetime;
this.logger = logger;
shutdownRegistration = applicationLifetime.ApplicationStopping.Register(Dispose);
}
public PendingUserRequestRegistration Add(DesktopUserRequestSpec spec, CancellationToken lifetime,
Func<string, CancellationToken, Task>? onResponse = null)
{
lifetime.ThrowIfCancellationRequested();
var now = timeProvider.GetUtcNow();
var request = new DesktopUserRequest(Guid.NewGuid().ToString("N"), spec.Kind,
spec.Title, spec.Message, now, now.Add(spec.Timeout), spec.Actions.ToArray());
var entry = new PendingRequest(request, lifetime, onResponse);
lock (gate)
{
ObjectDisposedException.ThrowIf(disposed, this);
pending.Add(request.Id, entry);
}
var registration = lifetime.Register(() => Dismiss(request.Id));
var retained = false;
lock (gate)
{
retained = pending.ContainsKey(request.Id);
if (retained) entry.CancellationRegistration = registration;
}
if (!retained) registration.Dispose();
var deadlineTimer = timeProvider.CreateTimer(_ => Dismiss(request.Id), null,
spec.Timeout, Timeout.InfiniteTimeSpan);
lock (gate)
{
retained = pending.ContainsKey(request.Id);
if (retained) entry.DeadlineTimer = deadlineTimer;
}
if (!retained) deadlineTimer.Dispose();
return new PendingUserRequestRegistration(request.Id, entry.Decision.Task);
}
public IReadOnlyList<DesktopUserRequest> List()
{
PendingRequest[] expired;
DesktopUserRequest[] snapshot;
lock (gate)
{
var now = timeProvider.GetUtcNow();
expired = pending.Values.Where(entry => entry.Request.ExpiresAt <= now || entry.Lifetime.IsCancellationRequested).ToArray();
foreach (var entry in expired) pending.Remove(entry.Request.Id);
snapshot = pending.Values.Select(entry => entry.Request).ToArray();
}
foreach (var entry in expired)
{
entry.Decision.TrySetResult(null);
entry.Dispose();
}
return snapshot;
}
public async Task<DesktopUserRequestRespondResult> RespondAsync(string id, string action)
{
PendingRequest? entry;
bool expired;
lock (gate)
{
if (!pending.TryGetValue(id, out entry)) return DesktopUserRequestRespondResult.NoLongerPending;
expired = entry.Request.ExpiresAt <= timeProvider.GetUtcNow();
if (!expired && !entry.Request.Actions.Any(candidate => candidate.Id == action))
return DesktopUserRequestRespondResult.InvalidAction;
pending.Remove(id);
}
entry.Dispose();
if (expired || entry.Lifetime.IsCancellationRequested)
{
entry.Decision.TrySetResult(null);
return DesktopUserRequestRespondResult.NoLongerPending;
}
try
{
if (entry.OnResponse is not null) await entry.OnResponse(action, entry.Lifetime);
entry.Decision.TrySetResult(action);
return DesktopUserRequestRespondResult.Accepted;
}
catch (OperationCanceledException) when (entry.Lifetime.IsCancellationRequested)
{
entry.Decision.TrySetResult(null);
return DesktopUserRequestRespondResult.NoLongerPending;
}
catch (Exception exception)
{
entry.Decision.TrySetResult(null);
logger.LogError(exception, "Desktop request {RequestId} action failed", id);
return DesktopUserRequestRespondResult.Failed;
}
}
public void Dismiss(string id)
{
PendingRequest? entry;
lock (gate)
{
if (!pending.Remove(id, out entry)) return;
}
entry.Decision.TrySetResult(null);
entry.Dispose();
}
public void DismissKind(string kind)
{
PendingRequest[] removed;
lock (gate)
{
removed = pending.Values.Where(entry => entry.Request.Kind == kind).ToArray();
foreach (var entry in removed) pending.Remove(entry.Request.Id);
}
foreach (var entry in removed)
{
entry.Decision.TrySetResult(null);
entry.Dispose();
}
}
public void Dispose()
{
PendingRequest[] removed;
lock (gate)
{
if (disposed) return;
disposed = true;
removed = pending.Values.ToArray();
pending.Clear();
}
foreach (var entry in removed)
{
entry.Decision.TrySetResult(null);
entry.Dispose();
}
shutdownRegistration.Dispose();
}
private sealed class PendingRequest(DesktopUserRequest request, CancellationToken lifetime,
Func<string, CancellationToken, Task>? onResponse) : IDisposable
{
public DesktopUserRequest Request { get; } = request;
public CancellationToken Lifetime { get; } = lifetime;
public CancellationTokenRegistration CancellationRegistration { get; set; }
public ITimer? DeadlineTimer { get; set; }
public void Dispose()
{
CancellationRegistration.Dispose();
DeadlineTimer?.Dispose();
}
public Func<string, CancellationToken, Task>? OnResponse { get; } = onResponse;
public TaskCompletionSource<string?> Decision { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
}
+12 -25
View File
@@ -44,20 +44,11 @@ builder.Services.AddSingleton<IMeetingAudioSource>(services => new CompositeMeet
services.GetRequiredService<ILogger<CompositeMeetingAudioSource>>())); services.GetRequiredService<ILogger<CompositeMeetingAudioSource>>()));
builder.Services.AddSingleton<IMeetingMetadataProvider, OutlookClassicMeetingMetadataProvider>(); builder.Services.AddSingleton<IMeetingMetadataProvider, OutlookClassicMeetingMetadataProvider>();
#else #else
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<DesktopUserRequestStore>();
PortableMeetingAudioRegistration.Add(builder.Services); PortableMeetingAudioRegistration.Add(builder.Services);
MacOsMeetingIntegrationRegistration.Add(builder.Services, OperatingSystem.IsMacOS()); MacOsMeetingIntegrationRegistration.Add(builder.Services, OperatingSystem.IsMacOS());
builder.Services.AddHostedService<MacOsDesktopControlService>(); builder.Services.AddHostedService<MacOsDesktopControlService>();
builder.Services.AddSingleton<IMicrophoneSwitchPromptService, NoopMicrophoneSwitchPromptService>(); builder.Services.AddSingleton<IMicrophoneSwitchPromptService, NoopMicrophoneSwitchPromptService>();
if (OperatingSystem.IsMacOS())
{
builder.Services.AddSingleton<IAgentApprovalPromptService, MacOsAgentApprovalPromptService>();
}
else
{
builder.Services.AddSingleton<IAgentApprovalPromptService, NoopAgentApprovalPromptService>(); builder.Services.AddSingleton<IAgentApprovalPromptService, NoopAgentApprovalPromptService>();
}
#endif #endif
builder.Services.AddSingleton<ITranscriptStore, VaultTranscriptStore>(); builder.Services.AddSingleton<ITranscriptStore, VaultTranscriptStore>();
builder.Services.AddSingleton<IMeetingNoteStore, MarkdownMeetingNoteStore>(); builder.Services.AddSingleton<IMeetingNoteStore, MarkdownMeetingNoteStore>();
@@ -75,14 +66,7 @@ builder.Services.AddSingleton<IMeetingInactivityPromptService>(services =>
builder.Services.AddHostedService(services => builder.Services.AddHostedService(services =>
services.GetRequiredService<WindowsMeetingInactivityPromptService>()); services.GetRequiredService<WindowsMeetingInactivityPromptService>());
#else #else
if (OperatingSystem.IsMacOS())
{
builder.Services.AddSingleton<IMeetingInactivityPromptService, MacOsMeetingInactivityPromptService>();
}
else
{
builder.Services.AddSingleton<IMeetingInactivityPromptService, NoopMeetingInactivityPromptService>(); builder.Services.AddSingleton<IMeetingInactivityPromptService, NoopMeetingInactivityPromptService>();
}
#endif #endif
builder.Services.AddSingleton<IRecordingDictationWordProvider, RecordingDictationWordProvider>(); builder.Services.AddSingleton<IRecordingDictationWordProvider, RecordingDictationWordProvider>();
builder.Services.AddSingleton<ICalendarPromptClock, SystemCalendarPromptClock>(); builder.Services.AddSingleton<ICalendarPromptClock, SystemCalendarPromptClock>();
@@ -163,11 +147,8 @@ builder.Services.AddSingleton(services => WorkflowRulesEditorMarkdownLinkResolve
services.GetRequiredService<IOptions<MeetingAssistantOptions>>().Value)); services.GetRequiredService<IOptions<MeetingAssistantOptions>>().Value));
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, WpfWorkflowRulesEditorWindowService>(); builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, WpfWorkflowRulesEditorWindowService>();
#else #else
builder.Services.AddSingleton<MacOsWorkflowRulesEditorSessions>(); builder.Services.AddSingleton<MacOsWorkflowRulesEditorSession>();
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService>(services => builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, MacOsWorkflowRulesEditorWindowService>();
new MacOsWorkflowRulesEditorWindowService(
services.GetRequiredService<IOptions<MeetingAssistantOptions>>(),
() => services.GetRequiredService<MacOsWorkflowRulesEditorSessions>()));
#endif #endif
builder.Services.AddSingleton<AgentApprovalService>(); builder.Services.AddSingleton<AgentApprovalService>();
builder.Services.AddSingleton<WorkflowChangeRequestService>(); builder.Services.AddSingleton<WorkflowChangeRequestService>();
@@ -224,10 +205,6 @@ app.MapGet("/diagnostics/platform-capabilities", (
screenshotProvider, screenshotProvider,
workflowEditorProvider))); workflowEditorProvider)));
#if !WINDOWS
app.MapDesktopCommunication();
#endif
app.MapGet("/recording/status", (MeetingRecordingCoordinator coordinator) => Results.Ok(coordinator.CurrentStatus)); app.MapGet("/recording/status", (MeetingRecordingCoordinator coordinator) => Results.Ok(coordinator.CurrentStatus));
app.MapGet("/profiles/{launchProfile}/recording/status", ( app.MapGet("/profiles/{launchProfile}/recording/status", (
string launchProfile, string launchProfile,
@@ -366,6 +343,16 @@ app.MapPost("/diagnostics/settings-and-logs/show", (
rulesEditorWindow.Show(); rulesEditorWindow.Show();
return Results.Accepted(); return Results.Accepted();
}); });
#if !WINDOWS
app.MapGet("/diagnostics/settings-and-logs", () =>
Results.Content(MacOsWorkflowRulesEditorPage.Html, "text/html"));
app.MapPost("/diagnostics/settings-and-logs/chat", async (
MacOsWorkflowRulesEditorChatRequest request,
MacOsWorkflowRulesEditorSession session,
CancellationToken cancellationToken) =>
Results.Ok(await session.SendAsync(request.Message, cancellationToken)));
#endif
static WorkflowConfigurationReloadResponse ReloadWorkflowConfiguration(IConfiguration configuration) static WorkflowConfigurationReloadResponse ReloadWorkflowConfiguration(IConfiguration configuration)
{ {
if (configuration is not IConfigurationRoot root) if (configuration is not IConfigurationRoot root)
+11 -13
View File
@@ -7,7 +7,7 @@ Meeting Assistant is Manuel's local .NET meeting capture and knowledge service.
This repository owns the application source, tests, OpenSpec requirements, checked-in configuration, and CI validation. It does not own workstation startup automation, a homelab deployment stack, Traefik routing, Docker Compose, or a deployed image tag. This repository owns the application source, tests, OpenSpec requirements, checked-in configuration, and CI validation. It does not own workstation startup automation, a homelab deployment stack, Traefik routing, Docker Compose, or a deployed image tag.
- Windows builds provide the tray icon, global hotkeys, NAudio capture, Outlook Classic COM enrichment, active-window screenshots, and notifications. - Windows builds provide the tray icon, global hotkeys, NAudio capture, Outlook Classic COM enrichment, active-window screenshots, and notifications.
- Portable builds on macOS provide a native menu-bar icon, global hotkeys and actionable notifications with pending-menu fallback, and capture the default microphone through AVFoundation plus computer output through ScreenCaptureKit. - Portable builds on macOS provide a native menu-bar icon and global hotkeys, and capture the default microphone through AVFoundation plus computer output through ScreenCaptureKit.
- Portable builds on other hosts keep the server/testable service surface but do not provide meeting audio capture. - Portable builds on other hosts keep the server/testable service surface but do not provide meeting audio capture.
- The normal runtime endpoint is local HTTP on port `5090`, with `/health` and `/recording/status` as the safe first checks. - The normal runtime endpoint is local HTTP on port `5090`, with `/health` and `/recording/status` as the safe first checks.
Accepted requirements live under `openspec/specs`. Relevant active changes under `openspec/changes` can describe implemented behavior that has not yet been folded into the accepted specs. Accepted requirements live under `openspec/specs`. Relevant active changes under `openspec/changes` can describe implemented behavior that has not yet been folded into the accepted specs.
@@ -17,9 +17,9 @@ Accepted requirements live under `openspec/specs`. Relevant active changes under
The project requires the .NET 10 SDK. Build and test it with: The project requires the .NET 10 SDK. Build and test it with:
```powershell ```powershell
dotnet restore MeetingAssistant.slnx -p:TargetFramework=net10.0 -p:EnableWindowsTargeting=true dotnet restore MeetingAssistant.slnx
dotnet build MeetingAssistant.slnx -f net10.0 -p:EnableWindowsTargeting=true dotnet build MeetingAssistant.slnx
dotnet test MeetingAssistant.slnx -f net10.0 -p:EnableWindowsTargeting=true dotnet test MeetingAssistant.slnx
``` ```
Do not start a second copy over the live workstation instance. Check the local control surface first: Do not start a second copy over the live workstation instance. Check the local control surface first:
@@ -36,7 +36,7 @@ dotnet build MeetingAssistant/MeetingAssistant.csproj -f net10.0
dotnet run --project MeetingAssistant/MeetingAssistant.csproj -f net10.0 dotnet run --project MeetingAssistant/MeetingAssistant.csproj -f net10.0
``` ```
The macOS build compiles bundled Swift helpers into `Native/macos-meeting-audio-capture`, `Native/MeetingAssistantDesktopControls.app/Contents/MacOS/macos-desktop-controls`, and `Native/macos-meeting-integrations`. On first use, allow **Microphone**, **Screen & System Audio Recording**, and **Calendar Full Access** under System Settings > Privacy & Security. The menu-bar icon and global hotkeys use the same configured bindings as Windows and call the local service surface. The signed desktop-helper bundle requests notification permission when its first user request arrives. Allow notifications for **Meeting Assistant** in System Settings > Notifications. Pending questions remain actionable in the menu bar when notifications are denied, hidden by Focus or dismissed. The macOS build compiles bundled Swift helpers into `Native/macos-meeting-audio-capture`, `Native/macos-desktop-controls`, and `Native/macos-meeting-integrations`. On first use, allow **Microphone**, **Screen & System Audio Recording**, and **Calendar Full Access** under System Settings > Privacy & Security. The menu-bar icon and global hotkeys use the same configured bindings as Windows and call the local service surface.
For a foreground Windows development run when port `5090` is free: For a foreground Windows development run when port `5090` is free:
@@ -74,8 +74,6 @@ The loopback HTTP surface has no application authentication, so port `5090` must
- `POST /diagnostics/settings-and-logs/show` - `POST /diagnostics/settings-and-logs/show`
- `POST /diagnostics/workflow/rules-editor/show` - `POST /diagnostics/workflow/rules-editor/show`
- `GET /diagnostics/platform-capabilities` - `GET /diagnostics/platform-capabilities`
- macOS: `GET /desktop/requests`, `POST /desktop/requests/{id}/respond` with `{ "action": "yes" }` (use an offered action ID)
- macOS: `GET /desktop/agent-windows` and session-aware `GET`/`POST /diagnostics/settings-and-logs/chat?session={id}`
- `POST /meetings/current/summary/run` - `POST /meetings/current/summary/run`
- `POST` or `GET /meetings/summary/retry` - `POST` or `GET /meetings/summary/retry`
- `POST` or `GET /meetings/screenshot-ocr/retry` - `POST` or `GET /meetings/screenshot-ocr/retry`
@@ -96,7 +94,7 @@ Outlook enrichment selects an unambiguous current or imminent appointment. A sch
Meeting Assistant writes meeting notes, transcripts, assistant context, summaries, and project knowledge into the configured Obsidian vault. Assistant context is persistent meeting-specific memory: the summarizer records problems and assumptions, and the interactive agent can read it and append later repairs and conclusions. Meeting Assistant writes meeting notes, transcripts, assistant context, summaries, and project knowledge into the configured Obsidian vault. Assistant context is persistent meeting-specific memory: the summarizer records problems and assumptions, and the interactive agent can read it and append later repairs and conclusions.
Project discovery uses the `name` and short `description` in each project's `PROJECT.md`. The summarizer can request missing associations through a Yes/No notification that waits up to ten minutes; only approval adds the project and enables its file access. On macOS the same decisions are also available from the menu-bar pending list. Workflow-change requests return immediately and, when approved, open a separate settings-agent conversation with the proposed change. Project discovery uses the `name` and short `description` in each project's `PROJECT.md`. The summarizer can request missing associations through a Yes/No notification that waits up to ten minutes; only approval adds the project and enables its file access. Workflow-change requests return immediately and, when approved, open a separate settings-agent conversation with the proposed change.
Agents are intentionally stateful. Depending on the invoked tools, they can change workflow rules and appsettings, create or update project and meeting files, change frontmatter, merge or delete speaker identities and samples, run diagnostics, and trigger transcription or summary work. Screenshot OCR can add recognized attendee names to the meeting note after workflow transformation; OCR of images already embedded in the meeting note does not add attendees or modify that note. Agents are intentionally stateful. Depending on the invoked tools, they can change workflow rules and appsettings, create or update project and meeting files, change frontmatter, merge or delete speaker identities and samples, run diagnostics, and trigger transcription or summary work. Screenshot OCR can add recognized attendee names to the meeting note after workflow transformation; OCR of images already embedded in the meeting note does not add attendees or modify that note.
@@ -161,7 +159,7 @@ Meeting-specific automation lives in a local YAML file, not in committed persona
The Windows tray and macOS menu-bar menus expose `Open agent`, which opens the `Meeting Summary Agent` window. It can edit workflow rules with validation, inspect logs and health/status, manage speaker identities and samples, run ASR diagnostics, and read/write scoped meeting/project artifacts through explicit tools. Windows renders the chat with WPF; macOS renders the same agent pipeline in a native AppKit window backed by WebKit. The Windows tray and macOS menu-bar menus expose `Open agent`, which opens the `Meeting Summary Agent` window. It can edit workflow rules with validation, inspect logs and health/status, manage speaker identities and samples, run ASR diagnostics, and read/write scoped meeting/project artifacts through explicit tools. Windows renders the chat with WPF; macOS renders the same agent pipeline in a native AppKit window backed by WebKit.
At startup, Meeting Assistant detects whether the host is Windows or macOS and includes that immutable runtime context in the interactive settings/logs assistant instructions. On Windows the assistant uses Windows commands and concepts such as PowerShell, Windows paths, services, and Task Manager. On macOS it uses zsh, POSIX paths, launchd/LaunchAgents, and Activity Monitor. This guidance is also appended when a custom interactive-agent prompt is configured. macOS audio capture, menu-bar controls, global hotkeys, EventKit calendar enrichment/prompts, active-window screenshots, screenshot OCR, speaker identification, FunASR, local diarization, and the AppKit/WebKit workflow editor are available in the portable build. Outlook Classic COM and Windows toast notifications remain Windows-specific implementations. macOS uses native UserNotifications and retains unanswered approval, inactivity and calendar requests in its menu bar. Approved workflow prompts start once in separate native agent windows, preserving the normal conversation. Requests expire or cancel with their original lifetime; silence grants no approval. At startup, Meeting Assistant detects whether the host is Windows or macOS and includes that immutable runtime context in the interactive settings/logs assistant instructions. On Windows the assistant uses Windows commands and concepts such as PowerShell, Windows paths, services, and Task Manager. On macOS it uses zsh, POSIX paths, launchd/LaunchAgents, and Activity Monitor. This guidance is also appended when a custom interactive-agent prompt is configured. macOS audio capture, menu-bar controls, global hotkeys, EventKit calendar enrichment/prompts, active-window screenshots, screenshot OCR, speaker identification, FunASR, local diarization, and the AppKit/WebKit workflow editor are available in the portable build. Outlook Classic COM and Windows toast notifications remain Windows-specific implementations.
The macOS publish output includes a signed `MeetingAssistant.app` bundle. Install that bundle in `/Applications` and launch the background service through its native executable so macOS microphone, calendar, and Screen/System Audio privacy grants are attributed to the stable Meeting Assistant application identity. The macOS publish output includes a signed `MeetingAssistant.app` bundle. Install that bundle in `/Applications` and launch the background service through its native executable so macOS microphone, calendar, and Screen/System Audio privacy grants are attributed to the stable Meeting Assistant application identity.
@@ -171,13 +169,13 @@ 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. 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 test project targets both `net10.0` and `net10.0-windows10.0.19041.0`, with the same application target selected explicitly. Choose `-f net10.0` for portable tests; use the Windows TFM on a compatible Windows runtime. A `win-x64` RID or Windows test host alone does not compile Windows tests. See `docs/platform-test-verification.md` for separate build, discovery, execution and artifact-verification commands. 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.
The Gitea workflow runs for pull requests, pushes, and manual dispatch on the existing `ubuntu-latest` runners. Its Windows job installs pinned Wine 11 plus a matching Windows .NET SDK, imports the SDK's public NuGet trust roots into a disposable Wine prefix, performs the full Windows application build without disabling resource generation, and builds/discovers/executes the real Windows test TFM. The portable job builds and tests `net10.0` on Ubuntu, including the managed macOS adapters; `TZ=Europe/Berlin` also exercises the calendar daylight-saving regression. Each job retains its own logs, discovery, TRX and verification report. The C# verifier requires the five named Outlook cases to pass in the Windows target and checks genuine platform skips. Wine compatibility is an execution prerequisite: a failed resource or runtime step remains a failed Windows check, with its evidence retained. Windows acceptance requires a successful run for the tested revision. See [Windows builds under Wine](docs/wine-windows-build.md) and [SDK certificate trust](docs/wine-sdk-trust.md) for the existing dependency correction, preparation and evidence boundaries.
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. 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.
The required `macos-native-full` job follows both jobs on the same existing Ubuntu infrastructure. It boots a disposable macOS 13/x86_64 guest with the existing Docker daemon and `/dev/kvm`, explicitly uses the RAW Recovery backend, installs the toolchain and builds/tests the exact checked-out revision. The current test inventory is verified against fresh discovery and TRX; all native macOS assertions must pass, while only the named Windows-only cases may skip. See [native macOS CI](docs/macos-native-diagnostic.md) for dependencies, limits, retained receipts and owned-resource cleanup. macOS 13 is outside Microsoft's current .NET 10 OS support; this configuration still needs a complete remote build/test qualification. CI validates source; it does not publish or deploy the workstation application. [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. A supported .NET 10 guest needs macOS 14 or later plus the Swift build tools. The documented `auto` build downloads a preinstalled guest disk through `IMAGE_URL`; its documented ready-made tags and disk downloads were unavailable when checked on 2026-10-03. No verified native guest bootstrap is owned by this repository. CI validates source; it does not publish or deploy the workstation application.
The separate manual [native Recovery diagnostic](docs/macos-native-diagnostic.md) probes macOS startup and disk readiness through an unprivileged TCG guest on the existing Ubuntu Docker runner. It neither installs macOS nor runs application tests; its result is a prerequisite for a future native test job, not verification of macOS CI support.
## Operations And Limitations ## Operations And Limitations
+34 -51
View File
@@ -1,71 +1,54 @@
# Native macOS CI on the existing Ubuntu runner # macOS 14 TCG prerequisite diagnostic
`tools/ci/MacOsNativeDiagnostic.cs` is a .NET 10 file-based application that owns one temporary macOS VM inside Docker. The PR/push workflow runs it after the existing Ubuntu and Wine jobs. It uses the existing Intel runner, Docker daemon and `/dev/kvm`, with no additional runner label, service, secret or host configuration. This manual candidate uses the existing Ubuntu/x64 Docker runner. Before downloading Apple Recovery it tests actual AVX/AVX2 instruction execution in the pinned QEMU binary, then probes a fresh macOS 14+ Recovery guest. It does not install macOS, erase a disk, provision .NET/CLT or run Meeting Assistant tests. Readiness is only a prerequisite for full native CI.
The pipeline and its application/test prerequisites are consolidated on `codex/macos-support`, the source branch of PR 39. The older CI branches retain experimental history. The current production configuration uses the Full KVM/NoAVX/RAW controller and its latest bounded resource collector; no separate diagnostic workflow or experimental branch filter is needed. ## Profile and evidence
The configuration has not passed remote installation, build or tests. Run [4219](https://gitea.schweigert.cloud/Daniel/meeting-assistant/actions/runs/4219) proved native macOS 13.6/x86_64 but failed eight bounded disk-readiness attempts. Run [4221](https://gitea.schweigert.cloud/Daniel/meeting-assistant/actions/runs/4221) failed both bounded `sw_vers` attempts. Run [4245](https://gitea.schweigert.cloud/Daniel/meeting-assistant/actions/runs/4245) subsequently passed Recovery readiness and entered installation; it was stopped at the user's request before a native build/test result. Local validation checks preparation and ownership contracts, not native CI success. These results do not establish that PR 39 is ready to merge. The existing Intel Celeron 1037U has neither AVX nor AVX2. KVM run 4187 at `720a431` reached macOS 13.6/x86_64/root and visible whole writable 64-GiB media. Diskutil timed out after 122 seconds; the sampler produced no report after 61 seconds. The screen remained at the Apple boot progress bar. CPU throttling, memory-limit/OOM events and container swap were zero; memory peaked at 2.67 GB. Host paging occurred. No unsupported-instruction crash or particular IPC wait is proved.
## Entrypoints [CryptexFixup 1.0.5](https://github.com/acidanthera/CryptexFixup/blob/1.0.5/kern_start.cpp) selects the installed/updated Rosetta Cryptex and patches APFS hash checking; it does not replace the running Recovery cache or emulate instructions. macOS 13 is outside the [.NET 10 supported-OS policy](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md). This candidate therefore uses macOS 14 and software CPU emulation without Cryptex. It changes the compatibility profile, not one isolated causal variable; actual success must be measured.
Run from a clean checkout of the commit to test: Earlier TCG run 4159 observed guest AVX2 with the upstream-selected Skylake model. Runs 4161/4163 measured slow native startup and reached the 40-minute host limit before readiness. They predated the UDIF CRC repair at `94a70b2`, reuse of successful sw_vers output and capturing the large Recovery hash only once. They do not qualify this candidate.
Run 4188 at `c01ae13` passed the actual AVX/AVX2 ROM test (positive exit 33, negative exit 0), then recorded 93 UEFI starts and 87 XNU handoffs before its 90-minute deadline. Container restart count, CPU throttling and OOM events were zero; no guest hook proof appeared. This establishes a guest boot loop, without identifying its cause. The next diagnostic preserves the same CPU/OS profile and has host/workflow limits of 20/25 minutes. Its purpose is to capture the first failure, not qualify full-run performance.
Kernel arguments add `-v debug=0x108 serial=5 msgbuf=1048576` while preserving the other pinned arguments, following [OpenCore 1.0.7](https://raw.githubusercontent.com/acidanthera/OpenCorePkg/1.0.7/Docs/Configuration.tex). Actual VM arguments add `-no-reboot -no-shutdown` and `-d int,cpu_reset,guest_errors,unimp`. The [QEMU reset policy](https://github.com/qemu/qemu/blob/v11.1.1/system/runstate.c) pauses the VM after a requested reset so monitor/framebuffer evidence survives. Exception output uses two bounded 4-MiB Docker log files. CPU/staging markers and sparse kernel-handoff lines are retained separately; repeated handoff or halted VM status fails immediately. These diagnostics do not prove a particular panic, CPU deficiency, or completed native test.
Run 4189 at `efc3fdc` stopped the first reset within about six minutes of container startup, with `VM status: paused (shutdown)`, one handoff and successful cleanup. Its retained trace started at exception 9517 and showed repeated supervisor instruction-fetch pagefaults at RIP/CR2 `0x24b0`; the preceding cause was missing. The current producer therefore retains the first 2 MiB after the kernel handoff before Docker rotation, while passing all output onward. This small AWK filter is part of the existing container boot integration and runs before a guest SDK exists; orchestration remains C#/.NET. Final capture retrieves the available Docker log files, the separate first-context file and monitor register/stack state.
The current candidate restores `Skylake-Client-v4` and the upstream TCG `-spec-ctrl` mask used by run 4159. `enforce=on`, actual instruction preflight, macOS 14, resource budgets and Readiness gates remain. This tests a previously booted source-bound profile; it does not assert that Haswell caused the earlier fault.
## Entry points and dependencies
Orchestration remains the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Existing Bash/Python boot integration is necessary before a guest SDK exists. NASM assembles the CPU probe in the disposable image build, without host/runner installation. No new runner, device, capability, secret or service is used.
```sh ```sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --recovery-format raw --output artifacts/native-macos-full dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos-full dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --output /path/to/fresh/validation
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
``` ```
The output directory binds the run identity and must be fresh. `git archive HEAD` supplies the exact source to the guest; uncommitted application changes are not included. Cleanup uses the same output directory and removes only the saved container, image and anonymous storage volume with matching ownership labels and IDs. Retained artifacts stay outside that volume. Shared Docker caches are not pruned. The native diagnostic workflow is manual only. Temporary diagnostic branches are excluded from ordinary push jobs to avoid repeating unchanged Wine/portable jobs. Remove this routing when integrating qualified CI into the actual PR.
For a read-only Recovery check, omit `--full`. That mode does not install an OS or execute application tests. Offline checks use a pristine checkout of the pinned Dockur source and the two pinned compatibility archives: ## Before Apple downloads
```sh The existing daemon must be Linux/x64 with two CPUs and 6 GiB memory; the runner must have 5 GiB available memory and the Docker filesystem 8 GiB free. These checks do not reconfigure resources. Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, both imported QEMU image digests and original source seams remain pinned.
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --full --recovery-format raw --source /path/to/dockur --cryptex-archive /path/to/CryptexFixup.zip --noavx-archive /path/to/NoAVX.zip --output /path/to/fresh-validation
dotnet run --file tools/ci/MacOsNativeGuest.cs -- --validate
```
## Guest and dependencies Actual `Skylake-Client-v4` CPU flags under TCG use `enforce=on` to reject unsupported requests. The CPU preflight uses that same composed flag list and QEMU binary before Recovery download/boot. `tools/ci/macos-tcg-cpu-preflight.asm` enables long mode/YMM state, executes AVX and AVX2 integer arithmetic, and checks an Int32 from the upper 128-bit lane. Only the correct result reaches [QEMU debug-exit](https://github.com/qemu/qemu/blob/v11.1.1/hw/misc/debugexit.c) code 33. No disks/network attach; failure/timeout fails preflight. This tests that instruction chain, not the complete ISA or macOS.
The controller pins Dockur source `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, QEMU image digests, OpenCore bytes, CryptexFixup 1.0.5, Lilu 1.7.1 and the OCLP 2.5.1 NoAVX AVXpel 12.6 archive. Hashes and staging/configuration checks live beside their use in the controller. The NoAVX and Cryptex fixes accommodate the existing host CPU without AVX/AVX2; native readiness must still pass. Downloads use existing outbound networking to upstream sources, Apple, Microsoft and NuGet. The locally assembled NASM 2.16.03 ROM is 65,536 bytes, SHA256 `c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549`. Assembly/static review does not prove remote execution.
The guest uses macOS 13, KVM with the real host CPU, two CPUs, 4 GiB RAM and a fresh 64-GiB disk. The container has a 6-GiB memory/swap limit, two CPUs and 512 MiB shared memory. Its sole mapped host device is `/dev/kvm`; no privileged mode, bind mount, host network or published port is used. Full runs check existing free memory and 32 GiB of Docker storage before proceeding. ## Native gates, bounds and cleanup
RAW mode converts the patched Recovery DMG with QEMU, verifies sector equality and unchanged source SHA256, and retains both images plus their receipt. QEMU attaches the same read-only virtio device with `format=raw`. Before RAM admission, existing GNU `dd` requests eviction of only the two verified files' caches; it does not clear host caches or raise resource limits. The default CLI format remains DMG; the automatic workflow explicitly selects RAW. The original Apple recoveryosd runs under its existing job/PID beside the read-only probe. Exact known macOS 13/14 plist layouts and same-length replacements retain their allowlist. The patcher validates UDIF boundaries, updates changed mish/koly CRCs and reads back the image. Four raw/zlib positive and twelve rejection fixtures use an independent C# CRC32 reader. Apple chunklist authentication applies to the input, not the deliberately modified image.
After staging, the controller records the immutable Recovery hash once and retains the successful receipt. It does not reread the large DMG on every progress poll. Missing-image or failed captures can retry; they cannot create a successful receipt. Native readiness requires x86_64, UID 0, macOS 14+, successful launchd service queries and exactly one writable whole 64-GiB disk. The complete successful sw_vers output must contain one valid ProductVersion field and EOF within 1,024 bytes. The actual native diskutil query remains mandatory.
The collector also retains CPU, memory and I/O counters from its owned container at most once per minute. Each snapshot has a ten-second deadline and 16-KiB output budget; collection failure cannot qualify or fail a native gate. The optional failure-time `kmutil` probe records loaded Recovery extensions when available. These observations do not alter VM settings or prove a performance cause. Required commands retain 45 seconds, UID 180 seconds and the single disk query 120 seconds. The owned observer uses `/bin/ps -M -p <diskutil-child>` with a separate 60-second limit and two-second TERM/KILL grace. It avoids stack symbolication; thread waiting states do not identify an IPC endpoint. Observation failure passes no gate. Owned children are stopped on completion/cancellation; output remains 512 KiB per command and 4 MiB proof.
The guest receives Microsoft SDK 10.0.401 with its pinned official SHA512 and installs Apple Command Line Tools through headless `softwareupdate`. A compiled framework smoke check verifies the selected CLT/SDK. macOS 13 is outside Microsoft's current .NET 10 OS support: a successful runtime test establishes this tested configuration, not vendor support. The product's Swift helpers already target macOS 13. The container retains 6 GiB memory/swap, two-CPU limit, 512 MiB shared memory and a 4-GiB/two-vCPU guest. One fresh anonymous /storage volume holds the sparse 64-GiB target. Inspection rejects devices, capabilities, binds, ports, host networking and privileged mode. KVM is disabled with no /dev/kvm mapping; guest networking stays slirp.
## Required proof and side effects Evidence retains run/source/profile identity, CPU preflight, original/patched Recovery identity, container/QEMU state, native proof/result and cleanup. Optional bounded before/during/after pressure snapshots record host/cgroup counters. The /storage/14/setup.dmg hash is captured once after staging; successful evidence survives later capture failure. Screenshots/pressure observations pass no gate.
Recovery must prove macOS 13+/x86_64, root identity, live native service domains and exactly one writable 64-GiB disk. Before the guest's only erase operation, both host and guest verify the run-owned disk and its emulated serial. A token/commit/disk permit and persistent markers prevent an unqualified or duplicate installation. The erase is confined to that fresh guest disk. Both cleanup paths verify exact token/label/ID before removing only the owned container, anonymous volume and image. No pruning, host changes, original checkout changes or Meeting Assistant restart occurs. Artifacts remain seven days. Full CI remains unverified until an installed supported guest builds/signs fresh helpers and passes all 577 tests, including the five native macOS tests, with zero skips.
The installed guest must map its APFS root back to the same owned disk, validate the source payload, restore/build `net10.0`, compile all four Swift helpers, verify fresh Mach-O/x86_64 output and the audio/desktop app signatures, and discover/run the full test suite. Acceptance requires matching fresh discovery and TRX for the current source, zero failures, all five named native macOS tests explicitly passed, and only the two named Windows-only tests skipped with their platform reasons. The desktop helper lives in `Native/MeetingAssistantDesktopControls.app/Contents/MacOS/macos-desktop-controls`. Real microphone capture, system audio and user privacy consent remain separate operational checks.
The workflow limit is 180 minutes, with a 172-minute controller deadline and phase budgets of Recovery 40, installation 80, CLT 30 and build/tests 25 minutes. These phase budgets do not extend the overall deadline. A bounded heartbeat reports the phase and captured guest progress. Failures retain logs and receipts; `finally` and `always()` perform the same owned-resource cleanup.
CI artifacts contain source/SDK and boot-asset hashes, container/resource identity, Recovery proof, installation/firstboot/CLT logs, disk/APFS identity, native helper/signature evidence, guest result and binary-preserved TRX. A green Recovery check alone is insufficient; only an exact-source Full result and matching native TRX prove macOS build/test execution. CI does not deploy or restart the workstation application.
## Branch inventory — 2026-10-06
Thirteen distinct macOS branch names were found: ten on the Daniel remote and three only locally. `codex/macos-support` is the delivery branch for application, Windows/portable pipeline, current native controller and their test prerequisites. Its automatic workflow checks the source revision of PR 39. The other names retain prior experiments and are no longer separate delivery targets.
| Branch | Location at inventory | Purpose before consolidation |
| --- | --- | --- |
| `codex/macos-support` | Remote/local | Current delivery branch |
| `codex/macos-native-full-kvm-noavx-raw` | Remote/local | Latest VM/resource-observation source, `b15670c` |
| `codex/macos-ci-pr39` | Local | Prepared PR integration, `1b9ec14` |
| `codex/macos-native-full-kvm-noavx` | Local | Earlier full KVM/NoAVX candidate |
| `codex/macos-ci-native` | Local | Earlier integrated native candidate |
| `codex/macos-ci-bootstrap-diagnostic` | Remote/local | Bootstrap diagnostic |
| `codex/macos-ci-diagnostic` | Remote/local | Recovery diagnostic |
| `codex/macos-ci-kvm-compatibility` | Remote/local | Earlier KVM compatibility experiment |
| `codex/macos-ci-kvm-diagnostic` | Remote/local | Separate KVM diagnostic |
| `codex/macos-ci-tcg-supported` | Remote/local | TCG diagnostic alternative |
| `codex/macos-kvm-noavx-recovery` | Remote/local | Earlier NoAVX Recovery experiment |
| `codex/macos-native-full-tcg` | Remote/local | Full TCG alternative |
| `codex/macos-native-raw-recovery` | Remote/local | Earlier RAW/cache experiment |
Local consolidation verification: 655 portable cases discovered on macOS, 653 passed, exactly two named Windows-only skips and zero failures; all five required native cases passed. Both test TFMs built normally, with four existing NAudio deprecation warnings in the Windows cross-build. The platform verifier and native evidence parser accepted the actual discovery/TRX. Both native controller offline contract suites, strict validation of the three affected OpenSpec changes, workflow YAML/dependencies and embedded shell syntax passed. This is local preparation evidence; native Ubuntu/macOS CI qualification remains open.
+4 -4
View File
@@ -144,7 +144,7 @@ During active capture, including paused transcription, active microphone endpoin
`Recording:MaxMetadataAttendeeImportCount` limits how many attendees calendar metadata enrichment imports into meeting-note frontmatter. The default is `30`; when an appointment has more attendees than that, Meeting Assistant still imports title, agenda, and scheduled end time, but leaves attendees empty because large invites are usually presentation-style meetings. Windows reads Outlook Classic through COM. macOS reads EventKit calendars, including Outlook accounts synchronized into macOS Calendar, and requires **Calendar Full Access**. `Recording:MaxMetadataAttendeeImportCount` limits how many attendees calendar metadata enrichment imports into meeting-note frontmatter. The default is `30`; when an appointment has more attendees than that, Meeting Assistant still imports title, agenda, and scheduled end time, but leaves attendees empty because large invites are usually presentation-style meetings. Windows reads Outlook Classic through COM. macOS reads EventKit calendars, including Outlook accounts synchronized into macOS Calendar, and requires **Calendar Full Access**.
`Recording:InactivitySafeguard` watches active recordings for long periods without transcript text. The timer starts at meeting start and resets whenever a live transcript segment with text is written. Writing new transcript text also dismisses every outstanding inactivity notification and invalidates its actions. By default the app asks whether to stop after 2, 5, and 10 minutes of inactivity through native Windows app notifications with Yes, No, and `Pause transcription` actions or macOS notifications/menu requests with Stop, Continue, and Pause actions, requests reminder-style toast behavior, keeps each stop reminder actionable for 1 minute, and automatically stops normally after 30 minutes. While transcription is intentionally paused, those prompts and the ordinary transcript-inactivity stop are fully suspended. A separate `MaximumPauseDuration`, defaulting to 4 hours, normally stops a meeting that remains continuously paused without showing an inactivity notification; it remains active when the ordinary inactivity safeguard is disabled, while a non-positive value disables the paused-session cutoff. Unpausing clears the continuous-pause timer and restarts transcript-inactivity timing from that moment. Ignoring a notification does not block later checks or auto-stop. Safeguard stops are not aborts: transcription drain, speaker processing, screenshots, and summary generation continue through the normal stop flow. When transcript inactivity stops a run, the meeting end time is inferred as the last transcript segment timestamp plus `InferredEndPadding`; if no transcript text arrived, it uses meeting start plus the same padding. `Recording:InactivitySafeguard` watches active recordings for long periods without transcript text. The timer starts at meeting start and resets whenever a live transcript segment with text is written. Writing new transcript text also dismisses every outstanding inactivity notification and invalidates its actions. By default the app asks whether to stop after 2, 5, and 10 minutes of inactivity through native Windows app notifications with Yes, No, and `Pause transcription` actions, requests reminder-style toast behavior, keeps each stop reminder actionable for 1 minute, and automatically stops normally after 30 minutes. While transcription is intentionally paused, those prompts and the ordinary transcript-inactivity stop are fully suspended. A separate `MaximumPauseDuration`, defaulting to 4 hours, normally stops a meeting that remains continuously paused without showing an inactivity notification; it remains active when the ordinary inactivity safeguard is disabled, while a non-positive value disables the paused-session cutoff. Unpausing clears the continuous-pause timer and restarts transcript-inactivity timing from that moment. Ignoring a notification does not block later checks or auto-stop. Safeguard stops are not aborts: transcription drain, speaker processing, screenshots, and summary generation continue through the normal stop flow. When transcript inactivity stops a run, the meeting end time is inferred as the last transcript segment timestamp plus `InferredEndPadding`; if no transcript text arrived, it uses meeting start plus the same padding.
| Setting | Purpose | | Setting | Purpose |
| --- | --- | | --- | --- |
@@ -347,7 +347,7 @@ See `docs/meeting-workflow-engine.md` for the detailed YAML format, supported va
`CalendarRecordingPrompts` controls optional calendar prompts for starting recordings. It is enabled by default on Windows and macOS. `CalendarRecordingPrompts` controls optional calendar prompts for starting recordings. It is enabled by default on Windows and macOS.
Meeting Assistant periodically syncs today's calendar appointments, filters all non-canceled Teams meeting markers for the day into an in-memory cache, and schedules prompt checks from that cache. Windows reads Outlook Classic through COM and displays a Windows app notification. macOS prefers EventKit and offers a native notification plus a menu-bar pending request with `Record`, `Skip` and, while allowed by the scheduler, `Attach metadata to current meeting`; the first read requests Calendar Full Access. When EventKit access is denied, it falls back to Calendar application automation so an already-authorized local Calendar setup remains usable. It does not query the calendar for every prompt. Accepting starts a recording. If another recording is active, Meeting Assistant stops that recording through the normal stop path first, then starts the new recording, so the usual empty/too-short cleanup and summary handoff rules still apply. Meeting Assistant periodically syncs today's calendar appointments, filters all non-canceled Teams meeting markers for the day into an in-memory cache, and schedules prompt checks from that cache. Windows reads Outlook Classic through COM and displays a Windows app notification. macOS prefers EventKit and displays a native AppKit `Record`/`Skip` alert; the first read requests Calendar Full Access. When EventKit access is denied, it falls back to Calendar application automation so an already-authorized local Calendar setup remains usable. It does not query the calendar for every prompt. Accepting starts a recording. If another recording is active, Meeting Assistant stops that recording through the normal stop path first, then starts the new recording, so the usual empty/too-short cleanup and summary handoff rules still apply.
| Setting | Purpose | | Setting | Purpose |
| --- | --- | | --- | --- |
@@ -412,9 +412,9 @@ The display name comes from `name`; `description` is limited to 256 characters.
`list_projects` returns the complete catalog as JSON entries with `id`, `displayname`, and `description`. This grants no file access. Reading, writing, searching, and retrieving historical summaries remain scoped to the meeting note's `projects` field. The summarizer uses its existing `write_projectfile` tool to maintain `PROJECT.md`; invalid resulting metadata is refused before writing. `list_projects` returns the complete catalog as JSON entries with `id`, `displayname`, and `description`. This grants no file access. Reading, writing, searching, and retrieving historical summaries remain scoped to the meeting note's `projects` field. The summarizer uses its existing `write_projectfile` tool to maintain `PROJECT.md`; invalid resulting metadata is refused before writing.
`request_project_association(project_id, reason)` asks the user through a native Yes/No notification. The reason must be one nonempty line of at most 100 characters. The tool blocks the summarizer until the decision or a ten-minute timeout. Approval adds the canonical ID to the latest meeting note, preserves other metadata and notes, immediately enables project access, and returns the project's metadata and optional `AGENTS.md`. No, timeout, cancellation, or an unavailable decision channel grant no access. macOS keeps the same Yes/No decision in its menu-bar pending list even if OS notification permission is denied. Expired notifications are removed and late actions have no effect. Already associated projects return context without another prompt. `request_project_association(project_id, reason)` asks the user through a native Yes/No notification. The reason must be one nonempty line of at most 100 characters. The tool blocks the summarizer until the decision or a ten-minute timeout. Approval adds the canonical ID to the latest meeting note, preserves other metadata and notes, immediately enables project access, and returns the project's metadata and optional `AGENTS.md`. No, timeout, cancellation, or unavailable notifications grant no access. Expired notifications are removed and late actions have no effect. Already associated projects return context without another prompt.
`request_workflow_change(intention, detailed_prompt)` returns requested immediately. The intention is a nonempty single line of at most 100 characters; the detailed prompt should be a self-contained scenario or specification. Its native notification remains valid for ten minutes and can be accepted after the summary completes. Approval opens a separate settings-agent conversation and submits the complete prompt automatically. Declining or expiration opens nothing. Requests are held in memory and canceled on application shutdown. The summarizer itself receives no workflow-editing tools. On macOS, the signed desktop helper requests notification permission on first delivery; System Settings > Notifications controls permission and presentation. Hidden or dismissed notifications leave the central request pending until its deadline. Notification buttons and menu actions use one decision endpoint, so duplicate or stale answers cannot repeat an action. Approved workflow prompts start once in independent native windows; reloading the page preserves the conversation without submitting the prompt again. `request_workflow_change(intention, detailed_prompt)` returns requested immediately. The intention is a nonempty single line of at most 100 characters; the detailed prompt should be a self-contained scenario or specification. Its native notification remains valid for ten minutes and can be accepted after the summary completes. Approval opens a separate settings-agent conversation and submits the complete prompt automatically. Declining or expiration opens nothing. Requests are held in memory and canceled on application shutdown. The summarizer itself receives no workflow-editing tools.
Once a stopped meeting reaches summarization, expiration of `Recording:StopProcessingTimeout` returns control while that run continues in the background. It does not cancel a summary waiting for project approval or queue that already-transcribed meeting for offline replay. The approval's own ten-minute deadline remains in effect. Once a stopped meeting reaches summarization, expiration of `Recording:StopProcessingTimeout` returns control while that run continues in the background. It does not cancel a summary waiting for project approval or queue that already-transcribed meeting for offline replay. The approval's own ten-minute deadline remains in effect.
+2 -2
View File
@@ -28,11 +28,11 @@ The tray menu includes `Open agent`, which opens the `Meeting Summary Agent` cha
Meeting Assistant detects the host operating system once during application startup and appends that context to the interactive agent prompt. Windows hosts receive PowerShell, Windows-path, Windows-service, and Task Manager guidance. macOS hosts receive zsh, POSIX-path, launchd/LaunchAgent, and Activity Monitor guidance. Other hosts receive neutral portable guidance. The platform context remains present when `WorkflowRulesEditor:InitialPrompt` is configured, and it does not imply that unavailable platform integrations have been implemented. Meeting Assistant detects the host operating system once during application startup and appends that context to the interactive agent prompt. Windows hosts receive PowerShell, Windows-path, Windows-service, and Task Manager guidance. macOS hosts receive zsh, POSIX-path, launchd/LaunchAgent, and Activity Monitor guidance. Other hosts receive neutral portable guidance. The platform context remains present when `WorkflowRulesEditor:InitialPrompt` is configured, and it does not imply that unavailable platform integrations have been implemented.
The automatic summarizer can propose changes through `request_workflow_change(intention, detailed_prompt)` but has no direct workflow write tool. `intention` is one nonempty line of at most 100 characters; `detailed_prompt` contains the complete desired change, preferably as a scenario/specification. A Windows notification or actionable macOS notification displays `Workflow change requested: ...` with Yes/No actions. The tool returns requested immediately so summarization continues. Yes opens a separate settings-agent window and automatically submits the complete detailed prompt; existing conversations and drafts are left intact. No or ten minutes without approval discards the request and removes the notification. Workflow requests can be approved after their summary finishes but are not persisted across application restarts. The settings agent applies approved requests through its normal validated tools. The automatic summarizer can propose changes through `request_workflow_change(intention, detailed_prompt)` but has no direct workflow write tool. `intention` is one nonempty line of at most 100 characters; `detailed_prompt` contains the complete desired change, preferably as a scenario/specification. A Windows notification displays `Workflow change requested: ...` with Yes/No actions. The tool returns requested immediately so summarization continues. Yes opens a separate settings-agent window and automatically submits the complete detailed prompt; existing conversations and drafts are left intact. No or ten minutes without approval discards the request and removes the notification. Workflow requests can be approved after their summary finishes but are not persisted across application restarts. The settings agent applies approved requests through its normal validated tools.
Project association requests follow a separate blocking path. `request_project_association` waits up to ten minutes before the summarizer continues; approval appends the project ID to meeting frontmatter and returns its instructions. This does not emit a new workflow trigger or change the YAML rule schema. See the configuration guide for project catalog metadata and access rules. Project association requests follow a separate blocking path. `request_project_association` waits up to ten minutes before the summarizer continues; approval appends the project ID to meeting frontmatter and returns its instructions. This does not emit a new workflow trigger or change the YAML rule schema. See the configuration guide for project catalog metadata and access rules.
On macOS, approvals remain in the menu-bar pending list when OS notifications are hidden, dismissed or denied. Notification and menu actions submit the same decision once. Yes opens a new native agent window and starts the full prompt once server-side; page reload and snapshot polling do not replay it. The normal agent conversation and its draft remain intact. No, expiration, cancellation and shutdown grant no project access and start no workflow conversation. Other portable hosts keep the unavailable-approval behavior. Automatic approval notifications are currently Windows-only. On macOS and other portable hosts, workflow-change and project-association requests are declined without opening an agent conversation or applying the requested change. The existing interactive macOS agent remains available from the menu bar.
```json ```json
{ {
-58
View File
@@ -1,58 +0,0 @@
# Platform test verification
The test and application projects expose `net10.0` (portable) and `net10.0-windows10.0.19041.0` (Windows). Always select a TFM. A RID selects runtime assets; it cannot enable Windows conditional compilation. `TestTargetCompositionTests.TestAndApplicationTargetsUseMatchingComposition` checks both assembly targets and the actual HTTP/service composition without starting application background services. Its TRX output records the target and runtime host.
## Portable path
From the repository root on a supported .NET 10 host:
```sh
mkdir -p artifacts/tests/portable
dotnet --info > artifacts/tests/portable/sdk.txt
dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -p:TargetFramework=net10.0 -p:EnableWindowsTargeting=true
dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0 -c Release --no-restore -p:EnableWindowsTargeting=true
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0 -c Release --no-build --list-tests > artifacts/tests/portable/discovery.txt
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0 -c Release --no-build --logger 'trx;LogFileName=portable.trx' --results-directory artifacts/tests/portable
dotnet run tools/VerifyPlatformTests.cs -- portable artifacts/tests/portable/discovery.txt artifacts/tests/portable/portable.trx
```
On macOS the application build compiles/signs the existing Swift helpers and runs their self-tests. `MacOsFact` tests exercise packaging and native helper/pixel behavior on the portable target only. Other hosts report reasoned skips. `ResolveKeyFallsBackToUserEnvironment` and `TaskbarIconGlyphsAreVisuallyCentered` report reasoned Windows skips on incompatible hosts; on Windows, even the portable target runs their assertions. The five Outlook cases are excluded from portable compilation and must not appear in portable discovery or results.
## Windows path
From the repository root in PowerShell on Windows with the .NET 10 SDK and Windows Desktop runtime:
```powershell
New-Item -ItemType Directory -Force artifacts/tests/windows | Out-Null
dotnet --info > artifacts/tests/windows/sdk.txt
dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -p:TargetFramework=net10.0-windows10.0.19041.0 -p:EnableWindowsTargeting=true -r win-x64
dotnet build MeetingAssistant/MeetingAssistant.csproj -f net10.0-windows10.0.19041.0 -c Release -r win-x64 -p:EnableWindowsTargeting=true
dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0-windows10.0.19041.0 -c Release -r win-x64 --no-restore -p:EnableWindowsTargeting=true
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0-windows10.0.19041.0 -c Release -r win-x64 --no-build --list-tests > artifacts/tests/windows/discovery.txt
dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0-windows10.0.19041.0 -c Release -r win-x64 --no-build --logger 'trx;LogFileName=windows.trx' --results-directory artifacts/tests/windows
dotnet run tools/VerifyPlatformTests.cs -- windows artifacts/tests/windows/discovery.txt artifacts/tests/windows/windows.trx
```
Check each command's exit code before proceeding. CI uses `pipefail` when retaining logs. A Windows-compatible Wine environment must run the Windows SDK for both builds and tests; the verifier can run through the Linux SDK against the resulting artifacts. The existing Wine job retains the full application/resource build log separately. It does not disable PRI/resource generation or downgrade build failures to successful compiler evidence. Wine's support for the currently required Windows resource tooling and native runtime must be demonstrated by that run.
Windows discovery and successful result records must name all five existing cases:
- `ExtractAgendaStopsBeforeTeamsJoinInformation`
- `NormalizeAttendeesDeduplicatesOrganizerAndRecipientEmail`
- `SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: "Canceled: Project sync")`
- `SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: "Cancelled: Project sync")`
- `SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: "Abgesagt: Project sync")`
The two Windows-only checks must pass their assertions on this runtime. Merely listing tests, returning before assertions, compiling with resource generation disabled, or running portable tests under Wine cannot satisfy Windows acceptance.
Ticket 02 additionally requires all three `WindowsScreenshotImageCropperTests` cases to be discovered and passed: `WindowsCropUsesOcrPixelCoordinates`, `WindowsCropPreservesExpectedPixelsAtNonzeroOrigin`, and `WindowsCropRejectsInvalidBounds`. The checker rejects omitted or skipped crop results and excludes the crop class from portable evidence, even on a Windows host. These tests use the registered production cropper and deterministic pixel data. See [Windows platform acceptance](windows-platform-acceptance.md) for the crop contract and separate external CI observation.
## Artifact checker and evidence boundaries
Entry point: `tools/VerifyPlatformTests.cs`, a .NET 10 file-based C# app using the standard library. Invocation: `dotnet run tools/VerifyPlatformTests.cs -- <portable|windows> <discovery.txt> <results.trx>`. It reads only the supplied files, prints named outcomes/skip reasons and totals derived from result records, and returns a nonzero exit code for invalid or incomplete evidence. It makes no network calls or application/runtime changes. Its fixture tests use synthetic records; those fixtures are not Windows execution evidence.
Gitea retains `portable-target-evidence` and `windows-target-evidence` separately, including available logs after a failed step. Artifact uploads use the Gitea-compatible action recommended by the [Gitea release documentation](https://blog.gitea.com/release-of-1.22.0/). Artifact publication and Wine execution still require validation on the configured runner.
Report current totals from each TRX, with host, SDK/runtime, TFM, discovery and content identity. Preserve the dated **2026-10-03 historical 602-pass portable baseline** in `openspec/changes/add-macos-user-communication/implementation-evidence.md`; do not reuse that number as a current total. Adapter summary counters for skipped tests can differ from `NotExecuted` records, so the checker counts actual result records.
A portable pass does not prove a full Windows resource build, native Windows operation, notification/menu acceptance, screenshot focus, or an activated ASR backend. Those results belong to their own acceptance evidence. Ticket 01's current evidence is in `openspec/changes/complete-macos-feature-parity/windows-test-assurance-evidence.md`. The old communication Task 4.2 stays open until a compatible runtime supplies the missing full Windows build; Task 4.4 and other tickets remain separate.
-23
View File
@@ -1,23 +0,0 @@
# Windows platform acceptance
The Windows crop checks use the existing Windows test target. Runtime acceptance is separate from the normal macOS cross-build already recorded by Ticket 01. Use `docs/platform-test-verification.md` for the complete build, discovery, execution and verifier commands; every command must succeed, and a Windows RID alone is insufficient. Operational ASR/backend acceptance remains separate from these pipeline checks.
## Target and native crop evidence
Run the actual `net10.0-windows10.0.19041.0` test target on Windows or a demonstrated compatible Wine runtime. Keep the SDK/runtime and Wine version, source revision/content manifest, complete build logs, discovery, TRX and verifier output together. The existing Gitea jobs collect those artifacts separately from the portable target.
The Windows verifier requires the five existing Outlook cases, both Windows runtime checks, the passed application-composition guard and these three additional cases:
- `WindowsScreenshotImageCropperTests.WindowsCropUsesOcrPixelCoordinates`
- `WindowsScreenshotImageCropperTests.WindowsCropPreservesExpectedPixelsAtNonzeroOrigin`
- `WindowsScreenshotImageCropperTests.WindowsCropRejectsInvalidBounds`
These tests resolve `IScreenshotImageCropper` from the Windows application factory, assert its real Windows implementation, and call `SaveCroppedScreenshotAsync`. The isolated factory suppresses background activities but preserves the production registrations. No capture permission, Outlook instance, OCR service or credential is needed. A 4×3 opaque PNG encodes a different color at every coordinate. Each valid crop checks every pixel and exact dimensions: 2×2 at (0,0) and 3×2 at (1,1), reaching the right and bottom boundaries. Invalid cases cover negative origins, zero/negative sizes, entirely outside origins and partially overflowing bounds. Each must throw the existing `InvalidDataException` and leave no crop file.
Each case records runtime, host, architecture and registered cropper in its TRX output. Portable compilation excludes these cases. Their compilation on macOS is preparation, not a native pixel pass. Synthetic verifier fixtures exercise evidence rejection only.
## CI observation and acceptance states
Observe an external run for the tested content, including both explicit-target jobs, named discovery, TRX, build logs, verifier outcome and two separate uploaded artifacts. Record run URL, revision and job outcomes. A run for an older revision, a workflow file, a cached status, or synthetic artifact checks cannot satisfy this row. An implementation request alone does not authorize publishing uncommitted prerequisites to trigger CI.
Use only `passed`, `failed`, `open — not executed with reason`, or `not configured/disabled — no pass` per host/profile/backend/model/operation row. Preserve the dated 602-pass and 639-pass portable baselines in their original evidence. Derive current totals from current TRX. Supplement the existing OpenSpec owners with the new evidence link; do not rewrite Ticket 01 or communication Task 4.2/4.4.
+5 -9
View File
@@ -9,22 +9,18 @@ dotnet artifacts/wine-sdk-trust/WineSdkTrust.dll --validate /path/to/sdk/trusted
Validation is read-only on every OS. Both files must exist and contain public PEM certificates. Explicit non-CA certificates are rejected. Historical self-issued SDK roots without `BasicConstraints` remain valid; certificate expiry is not filtered because trusted bundles also support historical signatures. The helper logs each bundle's certificate count and SHA-256, then the unique certificate count. Use bundles from the verified Microsoft SDK archive and retain those hashes with the SDK provenance. Validation is read-only on every OS. Both files must exist and contain public PEM certificates. Explicit non-CA certificates are rejected. Historical self-issued SDK roots without `BasicConstraints` remain valid; certificate expiry is not filtered because trusted bundles also support historical signatures. The helper logs each bundle's certificate count and SHA-256, then the unique certificate count. Use bundles from the verified Microsoft SDK archive and retain those hashes with the SDK provenance.
For the Windows SDK installed inside a disposable Wine prefix, invoke the published DLL with `--import` and the two authoritative Linux SDK bundle paths. From the repository root after installing the matching Windows SDK, for example: For the Windows SDK installed inside a disposable Wine prefix, invoke the published DLL with `--import` and the two authoritative Linux SDK bundle paths, for example:
```sh ```sh
export WINEPREFIX="${RUNNER_TEMP}/meeting-assistant-wine" "${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" artifacts/wine-sdk-trust/WineSdkTrust.dll --import \
SDK_VERSION="$(dotnet --version)" "Z:${DOTNET_ROOT}/sdk/10.0.401/trustedroots/codesignctl.pem" \
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" "Z:${PWD}/artifacts/wine-sdk-trust/WineSdkTrust.dll" --import \ "Z:${DOTNET_ROOT}/sdk/10.0.401/trustedroots/timestampctl.pem"
"Z:${DOTNET_ROOT}/sdk/${SDK_VERSION}/trustedroots/codesignctl.pem" \
"Z:${DOTNET_ROOT}/sdk/${SDK_VERSION}/trustedroots/timestampctl.pem"
``` ```
The import mode first requires `OperatingSystem.IsWindows()` and the Wine-specific `wine_get_version` export from `ntdll.dll`, located with `NativeLibrary.TryLoad` and `TryGetExport`. Native Windows is rejected as well as macOS/Linux. Only after both bundles validate does it open `CurrentUser Root` for writing and call `AddRange`. It closes the store, reopens it read-only and asserts that every imported thumbprint is present. There is no localized shell command, interactive prompt or GUI automation. Missing/unknown arguments and import attempts outside Wine return `2`; validation/import failures return `1`; successful validation or verified import returns `0`. The import mode first requires `OperatingSystem.IsWindows()` and the Wine-specific `wine_get_version` export from `ntdll.dll`, located with `NativeLibrary.TryLoad` and `TryGetExport`. Native Windows is rejected as well as macOS/Linux. Only after both bundles validate does it open `CurrentUser Root` for writing and call `AddRange`. It closes the store, reopens it read-only and asserts that every imported thumbprint is present. There is no localized shell command, interactive prompt or GUI automation. Missing/unknown arguments and import attempts outside Wine return `2`; validation/import failures return `1`; successful validation or verified import returns `0`.
The write side effect is confined to the selected Wine prefix's current-user root store. Repeated imports are safe. Run it only against the disposable CI prefix. Native validation never opens any certificate store, and native import attempts are rejected before parsing bundles or constructing a store. The normal PR workflow publishes this DLL and imports the bundles before restoring/building both projects for `net10.0-windows10.0.19041.0`. It retains helper-build and import/readback logs in `artifacts/tests/windows/`, alongside Windows discovery, TRX and verifier results. Trust-helper success does not replace a passing actual Windows test run; the Windows .NET SDK must build, discover and execute the Windows TFM, as described in [platform test verification](platform-test-verification.md). The write side effect is confined to the selected Wine prefix's current-user root store. Repeated imports are safe. Run it only against the disposable CI prefix. Native validation never opens any certificate store, and native import attempts are rejected before parsing bundles or constructing a store. The normal PR workflow publishes this DLL and imports the bundles before the Windows desktop restore/build. This repair still requires a real Wine restore/build result to establish that it fixes the observed trust failure.
[Microsoft documents](https://learn.microsoft.com/en-us/dotnet/core/tools/nuget-signed-package-verification) that these SDK bundles originate from its Trusted Root Program and provide code-signing and timestamping roots. The Windows NuGet restore remains responsible for checking actual package signatures; this helper populates the Wine store used for those checks. [Microsoft documents](https://learn.microsoft.com/en-us/dotnet/core/tools/nuget-signed-package-verification) that these SDK bundles originate from its Trusted Root Program and provide code-signing and timestamping roots. The Windows NuGet restore remains responsible for checking actual package signatures; this helper populates the Wine store used for those checks.
Local qualification on 2026-10-03 used SDK 10.0.203 to publish the DLL and the authoritative bundles from SDK 10.0.401 to validate it on macOS/.NET 10.0.7. Both bundles passed: codesign contained 307 certificates including seven historical roots without constraints; timestamp contained 327 including two such roots; the union contained 372 unique thumbprints. Their SHA-256 hashes were `AAB671F52E5229906B2100727370007EF4B6D2E360B23F2CF12A6D87773BE611` and `5CCB03367B52F047099F07E1653160E8578A83711CB18560C979FEBB65F8CB5D`, respectively. A native `--import` invocation with those same paths returned `2` before parsing bundles or opening a store; unknown arguments returned `2`, and an empty input returned `1`. No local store import was performed. This is helper qualification, not a passing Wine/NuGet result. Local qualification on 2026-10-03 used SDK 10.0.203 to publish the DLL and the authoritative bundles from SDK 10.0.401 to validate it on macOS/.NET 10.0.7. Both bundles passed: codesign contained 307 certificates including seven historical roots without constraints; timestamp contained 327 including two such roots; the union contained 372 unique thumbprints. Their SHA-256 hashes were `AAB671F52E5229906B2100727370007EF4B6D2E360B23F2CF12A6D87773BE611` and `5CCB03367B52F047099F07E1653160E8578A83711CB18560C979FEBB65F8CB5D`, respectively. A native `--import` invocation with those same paths returned `2` before parsing bundles or opening a store; unknown arguments returned `2`, and an empty input returned `1`. No local store import was performed. This is helper qualification, not a passing Wine/NuGet result.
Gitea run 4154 on 2026-10-03 subsequently imported and read back all 372 thumbprints under Wine 11 and completed Windows desktop restore without suppressing NuGet signature verification. Its later MakePRI build failure and the direct core-package correction are recorded in [Windows builds under Wine](wine-windows-build.md). That historical restore result does not prove the current Windows test target or application build.
+2 -26
View File
@@ -1,6 +1,6 @@
# Windows desktop build under Wine # Windows desktop build under Wine
The PR workflow uses the existing `ubuntu-latest` Gitea runner with pinned Wine 11 and a matching Windows .NET SDK. It builds both the application and test project for `net10.0-windows10.0.19041.0`, then discovers and executes that actual Windows test target through the Windows host. The desktop target retains WPF, `WinExe`, x64, and the Windows recording, hotkey, Outlook, screenshot, tray, and notification implementations. The separate portable job explicitly selects `net10.0`; a Windows host or `win-x64` RID alone does not compile Windows-only tests. The PR workflow uses the existing `ubuntu-latest` Gitea runner. It builds `net10.0-windows10.0.19041.0` with the Windows .NET SDK under Wine, then runs the portable `net10.0` test suite through that Windows host. The desktop target retains WPF, `WinExe`, x64, and the Windows recording, hotkey, Outlook, screenshot, tray, and notification implementations.
## Observed failures ## Observed failures
@@ -38,28 +38,4 @@ dotnet build MeetingAssistant/MeetingAssistant.csproj \
The command returned zero with `Build succeeded`, zero errors, and four existing NAudio deprecation warnings. The produced runtime configuration retains `Microsoft.WindowsDesktop.App` alongside the .NET and ASP.NET frameworks. The regenerated assets graph contains `H.NotifyIcon/2.4.1`, no `Microsoft.WindowsAppSDK*` packages, and no `Microsoft.Windows.SDK.BuildTools.MSIX`; generated package imports contain no Windows App SDK or MakePRI entry. The output `H.NotifyIcon.dll` is byte-identical to the previously selected cached core assembly, with SHA-256 `0027e443a6121af8fb616c0200d3c63bf4abb72e3c548e119fee1eae321a8368`. No application source or target suppression was required. This Linux cross-build does not establish that Windows `dotnet.exe` succeeds under Wine. The command returned zero with `Build succeeded`, zero errors, and four existing NAudio deprecation warnings. The produced runtime configuration retains `Microsoft.WindowsDesktop.App` alongside the .NET and ASP.NET frameworks. The regenerated assets graph contains `H.NotifyIcon/2.4.1`, no `Microsoft.WindowsAppSDK*` packages, and no `Microsoft.Windows.SDK.BuildTools.MSIX`; generated package imports contain no Windows App SDK or MakePRI entry. The output `H.NotifyIcon.dll` is byte-identical to the previously selected cached core assembly, with SHA-256 `0027e443a6121af8fb616c0200d3c63bf4abb72e3c548e119fee1eae321a8368`. No application source or target suppression was required. This Linux cross-build does not establish that Windows `dotnet.exe` succeeds under Wine.
Completion requires a new Gitea run at the tested revision: successful full Windows desktop and test builds under Wine, named Windows discovery, actual assertion execution with a fresh `artifacts/tests/windows/windows.trx`, and a passing evidence verifier. The workflow removes the previous application DLL/TRX at the expected paths and requires nonempty replacements. All five existing Outlook cases and the Windows-only environment/glyph checks must pass; native macOS tests report their explicit platform skip outside macOS. Windows-target test execution does not establish real Outlook COM, hardware or tray/UI operation. Completion requires a new Gitea run at the corrected commit: successful Windows desktop build under Wine with a freshly produced `MeetingAssistant.dll`, followed by actual Windows-host test execution with a fresh `wine.trx`, nonzero executed tests, zero failed tests, and a successful job. The workflow removes the previous DLL/TRX at the expected paths before these commands and requires nonempty replacements. The Wine test project targets `net10.0`, so these tests do not prove execution of Windows-TFM-only or WPF/Outlook UI behavior. Native macOS tests report their explicit platform skip outside macOS.
After the workflow installs the Windows SDK and provisions its Wine trust store, its validation commands use the Windows TFM throughout:
```sh
set -euo pipefail
mkdir -p artifacts/tests/windows
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-p:TargetFramework=net10.0-windows10.0.19041.0 -p:EnableWindowsTargeting=true -r win-x64
"${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
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release -f net10.0-windows10.0.19041.0 -r win-x64 --no-restore -p:EnableWindowsTargeting=true
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release -f net10.0-windows10.0.19041.0 -r win-x64 --no-build --list-tests \
> artifacts/tests/windows/discovery.txt
rm -f artifacts/tests/windows/windows.trx
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \
-c Release -f net10.0-windows10.0.19041.0 -r win-x64 --no-build \
--logger 'trx;LogFileName=windows.trx' --results-directory artifacts/tests/windows
dotnet run tools/VerifyPlatformTests.cs -- windows \
artifacts/tests/windows/discovery.txt artifacts/tests/windows/windows.trx
```
The checked-in workflow additionally records the commit, both SDK identities, Wine version, trust import/readback and targeting-pack diagnostics, plus separate build, discovery, execution and verification logs. It uploads available Windows evidence even after a failed step; the portable job retains its own evidence separately. See [platform test verification](platform-test-verification.md) for native Windows/portable commands and acceptance limits. No required PRI/resource target is disabled.
@@ -27,8 +27,8 @@ Selecting `Open agent` on macOS SHALL open a native window titled `Meeting Summa
- **THEN** a `Meeting Summary Agent` window opens - **THEN** a `Meeting Summary Agent` window opens
- **AND** the user can submit a chat message and receive the interactive agent response - **AND** the user can submit a chat message and receive the interactive agent response
#### Scenario: unanswered automatic approvals do not alter the macOS agent conversation #### Scenario: unavailable automatic approvals do not alter the macOS agent conversation
- **GIVEN** an automatic workflow-change or project-association request is pending on macOS - **GIVEN** automatic workflow-change approval notifications are unavailable on macOS
- **WHEN** no valid approval arrives before the request expires or its lifetime is canceled - **WHEN** the summary agent requests a workflow change or project association
- **THEN** the request is declined without opening or changing an interactive agent conversation - **THEN** the request is declined without opening or changing the interactive agent conversation
- **AND** merely hiding or dismissing its native notification does not approve the request - **AND** a direct attempt to open an automatic prompt reports the platform limitation instead of silently discarding the prompt
@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-10-03
@@ -1,36 +0,0 @@
## Context
The change started on codex/macos-support at bd35ebc. Verification includes the later fast-forward to 1164c26846686c1912fd1816cd06de80352e9504, which adds existing macOS CI and shutdown improvements. Existing OpenSpec requirements and local TDD policy own this work; no additional tracker or AGENTS scaffolding is needed. The user approved native notifications plus a menu-bar pending-request list. The interview exit audit found no remaining material product decision; packaging and transport are implementation choices.
## Goals / Non-Goals
Goals: restore bidirectional macOS user decisions for existing approvals, inactivity and calendar prompts, preserve pending decisions when OS notifications are hidden, and start independent approved workflow conversations.
Non-goals: microphone enumeration/recovery, new audio backends, unrelated chat rendering, remote notifications, changes to Windows behavior, production deployment or OpenSpec archival.
## Decisions
1. A small managed DesktopUserRequestStore owns process-local pending requests, allowed action IDs, deadlines and cancellation. Producers provide callbacks; the store has no recording/agent business dependencies. Approvals await decisions, while calendar/inactivity prompts enqueue and return immediately. Windows adapters remain unchanged.
2. GET /desktop/requests lists pending requests; POST /desktop/requests/{id}/respond submits an action. Native buttons and menu actions use the same endpoint. Response IDs are consumed once; invalid, canceled and expired responses cannot trigger work. Accepted callbacks use the originating lifetime rather than HTTP cancellation. Shutdown removes all requests. No answer never grants approval.
3. Package the existing AppKit desktop helper as Native/MeetingAssistantDesktopControls.app with stable identifier cloud.schweigert.meeting-assistant.desktop-controls. The launcher retains service supervision. The helper polls requests, schedules notifications once per ID, removes stale notifications and always offers pending-menu actions. Notification dismissal alone leaves the central request pending. Notification authorization is requested on first needed delivery; denied authorization still permits the menu fallback.
4. A managed macOS session store preserves the default interactive session and creates independent sessions for approved workflow prompts. GET /desktop/agent-windows announces requested native windows; the helper opens each session independently once. The detailed prompt starts once server-side. Session-aware chat snapshots and sends never restart the original prompt on page reload.
5. The helper decodes the existing numeric recording enum; the HTTP contract stays unchanged. Native self-tests exercise decoding and request/action handling without touching the live instance.
6. Keep current deadlines: approvals ten minutes, inactivity notifications one minute, calendar notifications five minutes capped at appointment end. Requests do not persist across application shutdown. Reopening a notification after shutdown cannot recreate an approval.
7. Resolve workflow sessions lazily when an approved prompt opens a window. Eagerly resolving sessions from the window service creates a dependency cycle through the agent pipeline, meeting coordinator and summary workflow service. The default window does not need to resolve this graph; application-owned session execution still observes shutdown.
## Risks / Trade-offs
- OS notifications can be hidden by Focus or screen sharing -> independent menu list retains actionable requests until their original deadline.
- Multiple callbacks and polling race -> atomic action validation/removal and idempotent stale-response rejection; cancellation registrations are disposed outside store locks.
- Awaiting a user decision could stall calendar or inactivity processing -> nonblocking prompt adapters with tests at their existing interfaces.
- Native app identity/direct launch may affect notification delivery -> build/signature/native checks and isolated operational verification; do not claim delivery without observing it.
- Background sessions and UI polling race -> immutable published snapshots and per-session send serialization.
- Existing active macOS specs describe unavailable automatic approvals -> update that scenario to refer to a genuinely unavailable communication channel, while the new capability defines menu fallback behavior.
## Migration Plan
Build and verify the portable target, complete behavioral tests and OpenSpec validation, then build the Windows target to check isolation. Any production replacement remains a separate delivery action and must occur only while the application is idle. Rollback uses the previous complete signed application bundle.
## Open Questions
No unresolved product decisions. Actual notification permission, delivery and response routing must be checked on macOS; unavailable permission is an operational verification limit, not a reason to remove menu access.
@@ -1,63 +0,0 @@
# Implementation evidence
Implementation completed on 2026-10-03; no production replacement, push, commit or archive was performed by this task. Full Windows resource-build verification and native interaction acceptance remain open below.
Branch: `codex/macos-support`. Starting baseline: `bd35ebc`. Verification HEAD: `1164c26846686c1912fd1816cd06de80352e9504`; the branch was fast-forwarded during this work to its existing macOS CI/shutdown change. The final portable suite ran after that commit. The 20 changed/new production and test files are hashed in `/private/tmp/mac-user-channel-evidence/code-identity.txt`; that manifest has SHA-256 `6cf3f10a72315c782cf17467be2dd7aa56fa279f191359780628ae818237d575`.
The approved implementation provides one managed pending-request lifecycle, macOS approval/inactivity/calendar adapters, native notifications and pending-menu actions, and independent approved workflow conversations. Managed logic uses C#/.NET. The existing Swift AppKit/UserNotifications host and embedded WebKit JavaScript retain their platform bindings. Windows notification registrations remain selected for the Windows target.
## Behavioral implementation and regression evidence
- Managed lifecycle: listing, one-time valid decisions, invalid-action preservation, deadlines, originating cancellation, withdrawal, shutdown and callback failure. Compiled/discovered red and green slices are retained, including `01-store-red.log`, `03-invalid-action-red.log`, `05-cancellation-red.log`, `07-expiry-red.log`, `09-withdraw-shutdown-red-and-sessions.log`, `11-failed-callback-red-and-sessions.log` and `19-expired-list-red.log`.
- Prompt adapters: explicit Yes/No approval with decline on timeout/cancellation; nonblocking inactivity Stop/Continue/Pause; nonblocking calendar Record/Skip/AttachMetadata using the original appointment and callback. Adapter reds include `13-approval-red.log`, `15-inactivity-red-and-workflow.log` and `17-calendar-red.log`.
- HTTP composition: list/respond and session-aware snapshot/send endpoints, invalid actions 400, consumed/stale requests 410, unknown sessions 404, and actual macOS service registrations. Executed reds are `root-http-list-red.log`, `root-http-response-red.log` and `root-composition-red.log`. Final focused managed/channel verification: **27 passed, zero failed/skipped**, `root-managed-final-green.log`.
- Workflow sessions: full prompt starts once, histories remain independent, snapshots/reloads do not replay work, response disconnection does not cancel accepted work, shutdown does cancel it. `sessions-01-red.log` records the original missing behavior. The first complete suite exposed an eager dependency-resolution cycle (14 failing startup tests), retained in `full-portable-suite-before-di-fix.log`; a deferred session factory fixed it. The startup/channel subset then passed **17/17**, `startup-and-channel-green.log`.
- Native host: compiled red/green checks cover numeric recording status, .NET timestamps, request reconciliation/menu fallback, allowed once-only actions, stale polling after acknowledgement, native actionable content and independent window IDs. All **seven native groups pass**, including during the integrated Release build and publish.
Temporary logs are under `/private/tmp/mac-user-channel-evidence`. They may be removed by normal temporary-directory cleanup.
## Complete builds and tests
The final portable suite passed **602/602 tests, zero failed/skipped**:
```zsh
dotnet test MeetingAssistant.slnx -f net10.0 -c Release -p:EnableWindowsTargeting=true --logger 'trx;LogFileName=macos-user-channel-full.trx'
```
Evidence: `full-portable-suite.log` and generated `MeetingAssistant.Tests/TestResults/macos-user-channel-full.trx`.
Strict OpenSpec validation passed for both `add-macos-user-communication` and the adjusted `add-macos-desktop-controls` change (`openspec-user-communication.log`, `openspec-desktop-controls.log`). `git diff --check` was clean. All 20 source-file hashes still matched the recorded manifest after documentation updates.
The standard Windows build reached C# compilation but failed at resource expansion because `MakePri.exe` cannot execute on macOS (`Exec format error`, `windows-build.log`). Windows C# compilation succeeded with only that resource-generation step disabled:
```zsh
dotnet build MeetingAssistant/MeetingAssistant.csproj -f net10.0-windows10.0.19041.0 -c Release -p:EnableWindowsTargeting=true -p:AppxGeneratePriEnabled=false
```
Evidence: `windows-cross-compile.log`. This checks compilation/isolation, not complete Windows resource/package generation or Windows runtime behavior. A Windows or configured Wine CI environment must complete task 4.2; no Wine runtime is installed on this Mac.
The macOS publish succeeded, compiling the native helpers and executing their self-tests:
```zsh
dotnet publish MeetingAssistant/MeetingAssistant.csproj -f net10.0 -c Release --self-contained false -p:EnableWindowsTargeting=true -o /private/tmp/mac-user-channel-evidence/publish
```
Evidence: `macos-publish.log`. The outer `MeetingAssistant.app` and nested `Native/MeetingAssistantDesktopControls.app` passed `codesign --verify --deep --strict`; the nested plist passed `plutil -lint`. The published desktop helper passed all seven self-test groups. Its stable identifier is `cloud.schweigert.meeting-assistant.desktop-controls`; no-argument cold activation exited without recreating a request.
## Isolated operational verification
The C# verification host (`/private/tmp/mac-user-channel-fixture/Program.cs`, `Fixture.csproj`, README) ran the production request store, endpoint module and workflow sessions at `127.0.0.1:5198`. Only the external agent pipeline was replaced with a harmless no-tool response. Its native manifest had no recording profiles or global hotkeys. It registered no live vault, calendar, audio, recording or LLM services.
- The actual signed desktop host reported notification authorization and scheduling. A workflow session opened a native AppKit/WebKit window; its accessibility tree and screenshot displayed the full verification prompt once and the expected harmless reply.
- A live request rejected an unoffered action with 400 and stayed pending; No returned 200; duplicate No and an expired request returned 410. In-memory callbacks recorded exactly one No per answered request.
- A temporary read-only Swift probe used the same stable app identifier and Apple's `UNUserNotificationCenter` API. It requested no permissions and scheduled/removed nothing. For request `09ba4dc708784aaf98b1ecd85f44aa8a`, `notification-delivery.json` reports `complete=true`, `delivered=true`, `pending=false`, registered category actions `yes/no`, authorized notification access and enabled alert/Notification Center/sound settings. This proves Notification Center delivery, not visible banner presentation or a physical button click.
- After that request received No (HTTP 200), `notification-after-response.json` reports `delivered=false` and `pending=false`. `operational-decisions.json` contains exactly the two independently answered No decisions, proving cleanup after the response and no duplicate callback.
- The isolated native host and verification web host were stopped after verification. The installed live Meeting Assistant was neither stopped nor replaced.
Probe entry: `/private/tmp/mac-user-channel-evidence/NotificationReadOnlyProbe.app/Contents/MacOS/macos-desktop-controls <request-id>`. Dependencies: the existing signed macOS app identity and UserNotifications framework. Effects: read-only API calls and one bounded JSON result, with no unrelated notification content printed.
## Remaining acceptance checks and limits
The native/browser UI bridge repeatedly timed out while selecting windowless menu/notification surfaces and interacting with the isolated browser page. No claim is made for actual notification/menu clicks, real notification dismissal retaining its menu item, permission-denied/Focus-hidden fallback, or the visible default draft alongside two simultaneous workflow windows. The implementation and native state tests cover those paths; manual/native interaction acceptance remains in task 4.4.
No live recording/hotkey regression exercise was performed in this isolated fixture. Existing numeric-status regression and the full portable suite passed. Full Windows resource/package generation remains the other verification limit. OpenSpec stays active pending acceptance and those checks; production delivery is separate.
@@ -1,24 +0,0 @@
## Why
macOS can record and process meetings, but several user-decision flows are disabled or block the calendar scheduler. The agreed solution is native actionable notifications plus a menu-bar list of pending requests, so meeting decisions remain accessible when notifications are hidden.
## What Changes
- Add pending user requests with allowed actions, deadlines, cancellation and one-time responses.
- Add macOS notification delivery and an actionable pending-request menu, sharing the same decisions.
- Enable project/workflow approvals and inactivity prompts on macOS; move calendar prompts onto the same non-blocking channel, including attachment to an active meeting.
- Open a separate interactive agent conversation after an approved workflow-change request without replacing the existing conversation or draft.
- Package the notification/menu host with a stable application identity and decode the existing numeric recording status correctly.
- Preserve Windows notification behavior. Microphone device monitoring, audio recovery, sample playback and unrelated agent rendering changes are outside this change.
## Capabilities
### New Capabilities
- `desktop-user-requests`: Actionable native notifications and pending-menu decisions, host adapters, lifecycle rules and approved macOS workflow conversations.
### Modified Capabilities
None. Existing approval and calendar behaviors gain a macOS implementation; existing recording-status requirements are repaired without changing the HTTP format.
## Impact
.NET request lifecycle and existing prompt registrations, read/respond loopback endpoints, Swift AppKit/UserNotifications desktop host, macOS packaging, agent session routing, behavioral tests and operational documentation. Requests and conversations remain process-local; shutdown cancels pending decisions. No remote notification service, new credentials or production deployment is required by this implementation change.
@@ -1,96 +0,0 @@
## ADDED Requirements
### Requirement: Desktop user requests have one managed lifecycle
Meeting Assistant SHALL expose pending user requests with an unpredictable ID, kind, title, message, creation time, expiration time and explicitly allowed action IDs and labels. The managed request lifecycle SHALL be independent of native notification presentation. Responses SHALL consume a request at most once. Invalid actions SHALL leave a valid request pending. Expiration, originating cancellation, withdrawal and application shutdown SHALL remove the request and prevent later responses from performing work. Absence of a response SHALL NOT grant approval. Accepted callbacks SHALL use the originating request lifetime, not the lifetime of the responding HTTP connection.
#### Scenario: User answers an offered action once
- **WHEN** a user submits an allowed action for a pending request
- **THEN** that decision is delivered once and the request disappears
- **AND** repeating the response performs no work
#### Scenario: Invalid action preserves the request
- **WHEN** a response names an action that the request does not offer
- **THEN** the response is rejected and the request remains pending
#### Scenario: Expired or canceled request cannot perform work
- **WHEN** a request expires, is canceled, is withdrawn or its application shuts down
- **THEN** the pending list excludes it
- **AND** a later response cannot invoke its action
### Requirement: macOS offers native notifications and pending-menu actions
Meeting Assistant SHALL announce pending user requests through native actionable macOS notifications and SHALL also show their count and offered actions in the menu bar. Both presentations SHALL submit the same managed decision. Denied or hidden notifications SHALL NOT prevent menu decisions. Dismissing a notification SHALL NOT approve the request or remove the independent menu decision. Answered or expired requests SHALL be removed from native delivered and scheduled notifications. Delivery SHALL be asynchronous and SHALL NOT block recording, calendar checks or other requests.
The native notification/menu host SHALL be packaged as a signed application bundle with a stable identifier and SHALL preserve the existing configured controls and numeric recording-status HTTP contract. Cold activation without a valid service/manifest SHALL NOT reconstruct an old request or approve it.
#### Scenario: Native action and menu action have the same effect
- **WHEN** the user chooses an offered action from either its notification or pending-menu entry
- **THEN** the application submits the same request ID and action
- **AND** the other presentation cannot execute the action a second time
#### Scenario: Notification permission is denied
- **WHEN** notification authorization is denied or a notification is hidden
- **THEN** the request remains selectable from the menu until its original deadline
#### Scenario: Existing recording status is decoded
- **WHEN** the service returns recording state as the existing numeric enum
- **THEN** the macOS helper shows the corresponding recording state and existing stop/abort controls
### Requirement: macOS approvals wait for explicit user decisions
On macOS, existing project-association and workflow-change approval interfaces SHALL use the desktop request channel. Yes SHALL approve; No, cancellation or ten-minute expiration SHALL decline. The summarizer SHALL receive new project access only after the existing approved-association operation succeeds. Workflow requests SHALL remain nonblocking for the summary and SHALL open their detailed prompt only after valid approval.
#### Scenario: macOS project association is approved
- **WHEN** a pending project association receives Yes before its deadline
- **THEN** the existing association operation can proceed and scope expands only through that operation
#### Scenario: macOS approval is declined or expires
- **WHEN** the user chooses No or an approval expires
- **THEN** no new project access or workflow conversation is granted
### Requirement: macOS inactivity and calendar prompts are nonblocking
The macOS inactivity prompt SHALL offer Stop, Continue and Pause through the pending request channel, expire after one minute and use the originating recording cancellation. DismissAllAsync SHALL remove inactivity requests without removing other kinds. The prompt method SHALL return without waiting for a user decision so existing inactivity checks and automatic stop remain operational.
The macOS calendar prompt SHALL offer Record and Skip and SHALL additionally offer Attach metadata to current meeting when the existing request allows it. It SHALL preserve the exact appointment metadata in the existing callback and expire after five minutes or at appointment end, whichever comes first. Its prompt method SHALL return without waiting, so other appointments and calendar synchronization continue.
#### Scenario: Inactivity prompt does not block automatic safeguards
- **WHEN** the application enqueues an inactivity warning
- **THEN** the prompt method returns while the request remains unanswered
- **AND** a valid offered response invokes the existing meeting callback
#### Scenario: An inactive recording withdraws its request
- **WHEN** the originating recording cancels its inactivity prompt lifetime
- **THEN** that request disappears and a late action cannot affect a later recording
#### Scenario: Calendar prompt allows attaching exact appointment metadata
- **WHEN** a calendar request permits attachment and the user selects Attach metadata
- **THEN** the existing callback receives AttachMetadataToCurrentMeeting for that appointment
- **AND** recording is not stopped and restarted by that action
#### Scenario: Unanswered calendar prompt does not block other work
- **WHEN** a calendar prompt remains unanswered
- **THEN** its show method has already returned
- **AND** subsequent calendar checks can run
### Requirement: Approved workflow prompts use independent macOS conversations
After a valid workflow approval, macOS SHALL create a new independent interactive agent conversation and request its own native window. The full detailed prompt SHALL start once server-side. The existing conversation, history and draft SHALL remain intact. Window loading, snapshot polling and page reload SHALL NOT resubmit that prompt. Unknown session IDs SHALL be rejected instead of using another conversation. Background work SHALL observe application shutdown and SHALL NOT be canceled merely because a page load or response connection closes.
#### Scenario: Approved workflow starts once without replacing the current chat
- **WHEN** a valid workflow request is approved while the normal agent conversation exists
- **THEN** a separate session receives the full prompt once
- **AND** the normal session and draft remain intact
- **AND** requesting snapshots or reloading its page does not start the prompt again
#### Scenario: Two workflow requests remain independent
- **WHEN** two workflow requests receive valid approval
- **THEN** each receives its own conversation and window request
#### Scenario: Unknown session is rejected
- **WHEN** a client sends a chat message for an unknown session ID
- **THEN** the service returns not found without changing any conversation
### Requirement: Windows user decisions preserve their existing implementation
Windows builds SHALL retain existing notification services and user-decision semantics. macOS notification helper packaging SHALL NOT be required to build or run the Windows target.
#### Scenario: Windows composition stays isolated
- **WHEN** the Windows target is built and services are registered
- **THEN** existing Windows prompt implementations remain selected
- **AND** no macOS helper is invoked
@@ -1,28 +0,0 @@
## 1. Managed requests and existing prompt adapters
- [x] 1.1 Add one behavioral red/green slice for listing and accepting a pending request.
- [x] 1.2 Add red/green coverage for invalid/duplicate actions, deadlines, cancellation, withdrawal and shutdown.
- [x] 1.3 Add macOS approval, inactivity and calendar adapters through their existing public interfaces; keep calendar/inactivity nonblocking.
- [x] 1.4 Register macOS adapters and expose request list/response endpoints with HTTP behavior tests.
## 2. Independent approved workflow conversations
- [x] 2.1 Add red/green session tests for one full prompt per independent workflow conversation, keeping default history/draft.
- [x] 2.2 Expose session-aware snapshot/send and native window requests; reject unknown sessions and preserve application cancellation.
- [x] 2.3 Replace obsolete unavailable-approval tests/scenarios with the new communication behavior.
## 3. Native notification and pending-menu host
- [x] 3.1 Package/sign the AppKit helper as a stable nested application bundle and preserve publish layout.
- [x] 3.2 Add native request decoding/action self-tests and numeric status regression coverage before implementation.
- [x] 3.3 Deliver actionable notifications, retain menu fallback, route decisions once and remove stale notifications.
- [x] 3.4 Open independently requested agent windows without changing the normal agent window; handle safe cold activation.
## 4. Documentation and verification
- [x] 4.1 Update README/configuration/workflow documentation and record implementation evidence.
- [ ] 4.2 Pass focused tests, the complete portable suite, Windows target build, native self-tests/signature checks and strict OpenSpec validation.
- [x] 4.3 Verify the request channel through isolated application endpoints and native UI where available; record any notification authorization/delivery limitation explicitly.
- [ ] 4.4 Complete native UI acceptance: choose notification/menu actions, dismiss a notification while retaining its menu request, verify permission-denied fallback, and preserve the default draft alongside two workflow windows.
Verification notes: 602/602 portable tests, focused tests, seven native self-test groups, macOS publish/signature checks and strict specification checks passed. Task 4.2 remains open only for the full Windows resource build: the Windows C# target compiles with `AppxGeneratePriEnabled=false`, while its standard resource build needs Windows `MakePri.exe`. Task 4.3 observed an independent native conversation and proved OS notification delivery, registered actions, one-time HTTP decisions and removal after a response. Task 4.4 requires a working UI-control surface; the available native/browser bridge timed out during these interactions. See implementation-evidence.md for exact evidence and limitations.
@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-10-03
@@ -1,61 +0,0 @@
# Ticket 01 closeout record
Date: 2026-10-03. The user accepted the documented implementation and expressly requested ticket closure, commit, push and cleanup. Ticket 01 is closed locally on that instruction. Original Windows runtime acceptance remains **verification-incomplete**; OpenSpec Task 1.1 and original communication Tasks 4.2/4.4 remain unchecked. The coordinating change is active and is not archived.
## Scope and identity
- Repository: `meeting-assistant`; authorized delivery target: `fork/codex/macos-support`.
- Fixed delivery/review base: `1b19b08f2e62398661dc24692ce4b776e90e715f`, incorporating five remote commits after the initial implementation base `1164c26846686c1912fd1816cd06de80352e9504`.
- Final substantive review covers 52 files: the existing macOS communication prerequisite, Ticket 01 tests/verifier/CI, their specifications/documentation and the five-line communication correction in the existing desktop owner specification. Other future parity additions in that mixed owner file remain excluded. The exact write set and file checksums are in `artifacts/tests/ticket01/closeout/reviewed-content-v2.sha256`; SHA-256 `147f83cfe076e27aa5ff29eedc8e9084e2b2e9ee39f9c7ab8a5d834100a0928d`.
- Fixed final review diff SHA-256: `6a9e70bb5dcf0e20f19dd029bdd251f14e528d03037a0a35140254b03066a28b`; retained as `artifacts/tests/ticket01/closeout/review-final-v2.diff`. The prior 51-file manifest/diff remain retained and attributable in the reviewer receipts.
- The [current 295-file code manifest](windows-test-assurance-content.sha256) has SHA-256 `19d7f4afae918d1fc3df24bb400e27ade2546d00d94d071467191dc3525bed00`. It serves a broader source-correspondence purpose than the 51-file changed-content review manifest.
- Generated evidence, review receipts and this completion record are outside the substantive identity, avoiding self-referential hashes. Original communication evidence is retained as dated history.
- Other preexisting parity-owner specification/task edits, personal runtime configuration, ignored files and live meeting state are excluded and preserved. No application restart or deployment is authorized or performed.
The candidate is substantive code/configuration, so independent structural review is required. An isolated checkout permits concrete preparation without mixing the excluded working changes into a commit. Publication of the preexisting communication prerequisite awaits the separately requested scope decision; the ticket alone depends on its uncommitted types and registrations.
## Requirements evidence
Sources: Ticket 01; `platform-parity-verification` requirements PARITY-TEST-01, PARITY-TEST-02 and PARITY-EVIDENCE-01; the existing `desktop-user-requests` specification. The detailed [implementation evidence](windows-test-assurance-evidence.md) separates current, historical and unavailable results. Current verification commands and inputs are retained in `artifacts/tests/ticket01/closeout/receipt.md`.
| Requirement | Expected and observed | State |
| --- | --- | --- |
| Portable target/composition | Explicit matching TFMs; actual HTTP and registered-service guard passes on macOS and Linux. Current full discovery/TRX has 641 matching cases, 639 passed, 2 reasoned Windows skips, 0 failed. | Verified |
| Windows target/composition | Matching Windows TFMs, x64/WPF and conditional registrations compile normally. Runtime guard cannot execute on the available macOS host. | Compile verified; execution open |
| Five Outlook assertions | Checker requires agenda, normalized attendees and all three cancellation cases discovered and passed. Empty/omitted/failed/skipped fixtures are rejected. Actual Windows discovery produces no cases because its x64 host is unavailable. | Runtime unverified |
| Honest skips | Both early returns are replaced. Actual TRX contains capability-specific skip reasons; compatible-host assertions remain in the test bodies. | Incompatible-host verified; Windows assertions open |
| Normal Windows build | Both application and test targets build with 0 errors and no resource suppression. Remote core `H.NotifyIcon` correction removes unused WinUI/WindowsAppSDK/MSIX/PRI dependencies. These are actual normal cross-builds on macOS, not Windows/Wine execution. | Cross-build verified; runner execution open |
| Separate evidence | Portable/Windows CI jobs select explicit targets, retain named discovery/TRX/build logs and use one fail-closed C# verifier. Current workflow operation/upload has not been observed. | Implementation verified; CI operation open |
| Historical/native boundaries | Old 602-pass baseline remains unchanged. Current native helper self-tests pass all seven groups; Linux focus remains 15 passed/7 reasoned skips. Native notification/menu UI acceptance remains with its owner. | History preserved; stated limits retained |
The verifier's 38 fixture cases test evidence rejection rather than Windows execution. Actual current portable artifacts also pass the CLI checker. All 294 verification inputs matched before and after the final runs. Initial Linux/native coverage is carried forward through unchanged relevant source; historical MakePri failure is explicitly attributed to the superseded package graph. The final owner-scenario correction was verified against existing passed timeout/cancellation, workflow and project tests plus the native dismissal handler, without repeating the suite; its receipt addendum and `contract-delta-proof.txt` preserve that bounded verification. Actual native UI interaction remains open.
## Standards coverage and reviewers
Rule sources: repository `AGENTS.md`, OpenSpec configuration/relevant specifications, the implement/change-accepted/code-review workflow and the user's C#/.NET preference.
| Coverage | Owner |
| --- | --- |
| Repeated knowledge, shared request lifecycle, native transport/window reuse, common test fixture/verifier ownership | DRY |
| Public contracts, lifetimes/cancellation, adapter/DI/native boundaries, background-service isolation and fail-closed evidence behavior | SOLID |
| Minimal structure, naming/readability, documentation/entry points, active-change ownership and remaining manual repository standards | KISS |
| Compilation, real named test outcomes, native helper checks and strict specifications | Reused/current verification receipt |
| YAML parsing, all 18 embedded Bash blocks and whitespace | Recorded automated syntax/diff checks |
Independent reviewers ran sequentially with fresh contexts. They inspected the fixed diff and nearby implementations; no generic additional review was added. Receipts preserve each reviewer's provenance and explicit unavailable Windows/native/CI coverage.
- [DRY receipt](review-receipts/dry.md): clean, no actionable finding; initial code/spec coverage, final CI/documentation delta and the owner-scenario delta.
- [SOLID receipt](review-receipts/solid.md): clean, no actionable finding; owner-scenario delta checked with original coverage retained.
- [KISS receipt](review-receipts/kiss.md): clean after resolving KISS-01, the inconsistent old automatic-approval scenario. The repair touched five specification lines only; earlier requirements/DRY/SOLID coverage was refreshed through their same owners before final KISS approval.
## Final state and delivery
### Pipeline consolidation — 2026-10-06
The user requested all pipeline changes on `codex/macos-support` and explicitly included the required test files. The existing communication prerequisites needed by those tests are included in this delivery. The Windows/portable test-target work and crop checks are combined with the latest KVM/NoAVX/RAW native CI controller. Separate future parity-owner planning and backend speech fixtures are preserved outside this delivery.
Current local verification has 655 discovered portable cases, 653 passed, two reasoned Windows-only skips and no failures, including all five mandatory native macOS facts. The Windows application/test target builds normally with zero errors and four existing NAudio deprecation warnings. Dynamic native discovery/TRX validation replaces the obsolete 577-case gate and verifies the new signed desktop app path. Both native offline contract suites and strict OpenSpec validation passed. Historical reviews and manifests below retain their original scope and identity; these new results do not establish remote Wine execution, native Ubuntu/macOS full CI, or operational notification/backend acceptance.
Available requirements evidence is current and all three sequential structural reviews are complete for the 52-file identity; no unresolved binding finding remains. The original full Windows criteria remain verification-incomplete. Ticket closure follows the user's explicit acceptance rather than manufacturing those missing results. No OpenSpec task is checked solely from ticket closure or portable counts.
Commit/push is authorized for Ticket 01 on `fork/codex/macos-support`. The separately prepared communication prerequisite requires the pending scope answer before publication. No commit or push has yet occurred. The isolated delivery worktree remains needed until that decision. Approximately 4.3 GB of obsolete source/build snapshots were removed after retaining all original/current proof artifacts and the prerequisite preview; the rejected temporary credential helper was also removed. The prepared source checkout is approximately 10 MB. The primary checkout, unrelated dirty work and required evidence are retained.
@@ -1,41 +0,0 @@
## Context
Windows und macOS teilen Aufnahme- und Agent-Fachlogik in .NET. Der Mac-Port verwendet noch nicht sämtliche bestehenden Recovery-, Auswahl- und Desktop-Aktionswege. Die Chat-Präsentation transportiert außerdem nur einen Teil der bereits definierten Darstellungszustände. Das neutrale Testtarget entdeckt bedingt kompilierte Windows-Fälle auch auf einem Windows-Host nicht.
Der vorhandene Kommunikations-Change stellt User requests und unabhängige Workflow-Sessions bereit. Seine offene Windows-Resourcebuild- und native UI-Abnahme bleiben erhalten; diese Funktion wird nicht erneut implementiert.
## Goals / Non-Goals
**Goals:** dieselben fachlichen Ergebnisse auf beiden Plattformen; Verhaltenstests an bestehenden öffentlichen Grenzen; tatsächliche native Abnahme; Testabsicherung und Recovery, Pause sowie sicherer Exit zuerst; nachvollziehbare echte Abhängigkeiten.
**Non-Goals:** Code-, Test- oder CI-Implementierung in diesem Planungsschritt; neue ASR-Backends; zusätzliche Kalenderquellenauswahl oder Credential-Speicherung; Live-Neustart, Installation, Auslieferung oder Archivierung.
## Decisions
1. **Anforderungen beim bisherigen Eigentümer ergänzen.** Recovery, Pause, Mikrofonangebote, Desktop, Kalender und Resemblyzer erhalten macOS-Szenarien in ihren ursprünglichen aktiven Changes. Der koordinierende Change führt ausschließlich die zusätzliche Prüf-Capability. Damit bleiben bestehende Fachverträge die Verhaltensquelle.
2. **Hohe vorhandene Testgrenzen nutzen.** Aufnahme wird über Desktop-Aktion und öffentlichen RecordingCoordinator, Kalender über öffentliche Metadatensuche/Scheduler, Agenten über Session/Chat bis Präsentation und Speaker-Funktionen über öffentliche Tool-/Queue-/Encoder-Grenzen geprüft. Reine Registrierung oder ein Stub beweisen keine native Funktion.
3. **Recovery liefert die schmale Geräteanbindung.** Vollständiger Fallback benötigt Geräteauflösung und ID-basierte Capture. Eine fertige Auswahloberfläche ist keine Voraussetzung. Recovery arbeitet nach Laufzeitwahl, Konfiguration, Standard und verfügbarem Ersatz. Default-only Retry ist ein Teilschritt, keine volle Parität.
4. **Fachlogik und native Adapter behalten.** Auswahl-, Pause-, Exit-, Profil-, Kalender- und User-request-Regeln bleiben im C#-Kern. Swift erweitert Apple-Geräte-, Menü-, Notification-, Shortcut- und Fensterbindungen; die bestehende WebView erhält die benötigte Darstellung. Verteilte OS-Abfragen in der Fachlogik sind nicht erforderlich.
5. **Agent-Darstellung in fünf Ergebnisse teilen.** Markdown, Links, Bilder, Aktivität und Scroll sind einzeln beobachtbar. Parser, Resolver, Activity-ViewModel und Scroll-Policy bestehen bereits. Bilder enthalten ihren eigenen Auflösungs-/Transport-/Öffnungsweg; Activity und Scroll benötigen keine neue Markdown-Darstellung.
6. **Neue Unterhaltung explizit und verlustfrei öffnen.** Die Sessionverwaltung erstellt eine zusätzliche leere Session und ein Fenster ohne Modellaufruf. Bestehende Gespräche und Drafts bleiben erhalten. Implizites Löschen beim Schließen verletzt den bereits geschützten Erhaltungsvertrag.
7. **Plattformnachweise getrennt berichten.** Echter Windows-TFM, Discovery und Assertions sind erforderlich; RID genügt nicht. Unverfügbare Laufzeittests melden Skip statt scheinbaren Erfolg. Windows-Resourcebuild, native Pixeltests und operative Mac-Prüfungen erhalten eigene Ergebnisse.
## Risks / Trade-offs
- [Engine stoppt bei lebendem Helper] → native Konfigurations-/Stop-Erkennung und Recovery nachweisen; reine Prozessfehlerprüfung genügt nicht.
- [Verzögerte Aktion beeinflusst anderen Run] → ursprüngliche Run-/Request-Lebensdauer binden und Wirkungslosigkeit veralteter Entscheidungen prüfen.
- [Gleichzeitige Änderungen an nativer Präsentation] → Dateibearbeitung bei späterer Umsetzung koordinieren; gemeinsame Dateien sind keine fachlichen Blocker.
- [Betriebsumgebung fehlt] → fehlenden Runner, Gerätezustand, Backend oder Berechtigung ausdrücklich als offene Abnahme ausweisen.
- [Delta verliert vorhandene Szenarien] → vollständige Requirementblöcke erhalten und macOS-Szenarien additiv integrieren; vor Archivierung Konflikte aktiver Deltas abgleichen.
- [Standard-Paketpin auf macOS nicht installierbar] → offizielle Indexdaten prüfen und frische isolierte CPU-Provisionierung nachweisen; explizite Nutzerpins nicht still ändern.
## Migration Plan
Zunächst werden nur Planungsdokumente veröffentlicht. Spätere Umsetzung erfolgt in kleinen vollständigen Slices: roter Verhaltenstest, minimale Umsetzung, relevante grüne Tests, strikte OpenSpec-Validierung und operative Abnahme. Bereits gültige Nachweise werden weiterverwendet; Wiederholung benötigt eine neue Änderung oder konkrete Regression.
Die bestehende Kommunikation und ihre Nachweise bleiben erhalten. Eine fehlgeschlagene oder unmögliche Prüfung bleibt offen. Keine aktive Aufnahme oder Verarbeitung wird für eine Abnahme ohne ausdrückliche Autorisierung unterbrochen.
## Open Questions
- Verfügbare Windows-/Mac-Testumgebung, native UI-Steuerung, Audiofixture und aktivierte Backendkonfiguration sind vor den entsprechenden operativen Prüfungen zu bestimmen.
- Unterstützte Python-/macOS-/Architektur-Kombinationen und installierbare Standardpins werden im Resemblyzer-Slice mit offiziellen Paketdaten und frischem Lauf verbindlich nachgewiesen.
@@ -1,26 +0,0 @@
## Why
Der macOS-Port soll die vorhandenen Windows-Funktionen mit denselben beobachtbaren Verhaltensgarantien bieten. Die portable Testsuite weist Windows-spezifische Fälle und native Bedienwege bisher nicht vollständig nach; zwölf Restpunkte und offene operative Abnahmen benötigen klare Anforderungen.
## What Changes
- Bestehende Aufnahmeanforderungen erhalten macOS-Szenarien für Mikrofonwiederherstellung, Geräteauswahl, Angebote neuer Mikrofone, Pause/Fortsetzen und sicheres Beenden.
- Die Desktop-Anforderungen werden um aktive Profilwechsel, vollständige konfigurierbare Shortcuts, Sprecherproben-Wiedergabe und eine frische unabhängige Agent-Unterhaltung ergänzt.
- Agent-Markdown, Links, Bilder, sichtbare Aktivität und Scrollverhalten werden als einzeln überprüfbare Ergebnisse spezifiziert.
- Kalender-Absagepräfixe und isolierte plattformkompatible Resemblyzer-Provisionierung erhalten gemeinsame Verhaltensverträge.
- Eine Plattformmatrix unterscheidet portable Tests, echte Windows-Testtargets und native Nachweise. Die bisherigen offenen Kommunikations-Abnahmen bleiben ihrem ursprünglichen Change zugeordnet.
- Dieser Schritt veröffentlicht Anforderungen und zukünftige Aufgaben. Er startet keine Implementierung oder Anwendungsauslieferung.
## Capabilities
### New Capabilities
- `platform-parity-verification`: echte Testtargets, ehrliche Nichtausführung, native Bildverträge und operative Abnahme der Plattformparität.
### Modified Capabilities
Keine zusätzliche fachliche Delta-Spec in diesem koordinierenden Change. Ergänzungen von `meeting-recording`, `meeting-session` und `meeting-transcription` liegen bei den bestehenden Eigentümern `recover-microphone-disconnect`, `prompt-for-new-microphone`, `add-transcription-pause-controls`, `add-macos-desktop-controls`, `add-macos-meeting-integrations` und `add-resemblyzer-speaker-recognition`.
## Impact
Betroffen sind die Anforderungen an Aufnahme-/Mikrofonadapter, Desktop-Aktionswege, Kalenderfilter, Agent-Präsentation, Playback, Shortcutregistrierung, Resemblyzer und die Test-/Buildmatrix. Die Umsetzung bleibt im gemeinsamen .NET-Fachkern mit vorhandenen nativen Adaptergrenzen. Quellcode, Tests und CI werden in diesem Planungsschritt nicht geändert.
@@ -1,52 +0,0 @@
# DRY review receipt — Ticket 01
Reviewer: independent `/root/review_dry`; read-only review, 2026-10-03.
## Scope identity
- Repository: `/private/tmp/meeting-ticket01-closeout` (Git root confirmed).
- Base: `1b19b08f2e62398661dc24692ce4b776e90e715f`.
- Fixed initial diff: `/private/tmp/meeting-ticket01-review.diff`, SHA-256 `af701f7d01f0a7191d27bbd5c6ab614650a9915f2e496da3b295371c86590ffa`.
- Scoped paths: `/private/tmp/meeting-ticket01-review-files.txt`, SHA-256 `f59d9c9c6d1905b25d9b2ccfd1845eef03501dccbc0f275d5351bd816bce2eea`.
- Final candidate: `/private/tmp/meeting-ticket01-review-final.diff`, SHA-256 `40c69e017e5047b0d59348d22ec879122a9f6d02f4f232843847e8fdefe98dac`.
- Current content manifest: `/private/tmp/meeting-ticket01-reviewed-content.sha256`, SHA-256 `b411b6ddffc6ca4e7c2a76c37ff48a49253073e0382abd0ccb213bc531be2a82`; all 51 scoped file hashes matched the working files at review refresh.
- Scope includes the existing macOS communication prerequisite, Ticket 01 code/test/spec work and the final CI/README/Wine-documentation integration. Future parity-owner changes remain excluded.
## Inputs and assigned coverage
Read code-review skill and review criteria, repository README/AGENTS/config, communication request and parity verification requirements, active change designs/tasks/proposals, and Ticket 01 evidence. Inspected the fixed diff plus adjacent implementations and tests.
DRY coverage: shared request lifecycle; adapter decisions and lifetimes; native notification/menu transport and window construction; platform-test target/skip knowledge; HTTP fixture isolation; verifier code and synthetic fixtures; requirement ownership and evidence references. Repository DRY surrounding-context rule was applied. The final old-to-new delta was inspected separately; unchanged initial coverage carried forward. Behavioral verification was reused, not rerun.
## Result
**Final DRY outcome: clean for the pinned 51-file candidate.** No actionable binding or heuristic DRY finding, and no material behavioral defect found.
- The request store owns common decision/deadline/cancellation handling; adapters retain distinct approval, inactivity and calendar semantics. Native notification and menu decisions share response construction; ordinary and approved native windows share construction.
- `IsolatedApplicationFactory` consolidates background isolation while fixtures opt into the activity they test. The verifier is linked into test compilation, so its implementation has one owner.
- Exact expected case names, target markers and totals in verifier fixtures are an independent test oracle. Sharing them directly with the checker would weaken regression detection. Explicit project target declarations are guarded by the public composition test.
- Small repeated request-completion statements, notification identifier spelling and test setup do not presently justify a new abstraction: no inconsistent behavior or separate maintenance policy was found.
- Supplementary parity evidence references the original communication acceptance tasks and preserves their historical result/limits; the two changes retain distinct ownership.
- CI's repeated commands serve distinct Wine and portable setup/runtime boundaries. Both jobs invoke the same C# artifact verifier. Native Windows, Wine and portable documentation has distinct invocation purposes and links the common verification requirements; no inconsistent maintenance decision was found.
## Verification and closeout limits
Coordinator supplied the initial critical-requirements receipt: 295 original content hashes verified; actual portable suite 641 results / 639 passed / 2 reasoned skips; Linux subset 22 / 15 passed / 7 reasoned skips; focused HTTP 21 passed; verifier synthetic fixtures 38 passed. Portable sources and the portable conditional package graph are unchanged by the Windows-only H.NotifyIcon core-package correction, so that portable evidence carries forward. The subsequent actual full portable rerun also passed with 641 results / 639 passed / 2 reasoned Windows skips / 0 failed and all seven native helper self-test groups; its evidence is recorded in `artifacts/tests/ticket01/closeout/receipt.md`.
The coordinator's refreshed current-graph check reports normal, unsuppressed Windows application and test-target cross-builds on macOS with zero errors; the direct H.NotifyIcon graph has no Windows App SDK/MSIX/PRI dependency. This supersedes the old package graph's limited-compile/MakePRI failure as current build evidence. Actual Windows/Wine runtime discovery and assertions remain unavailable, and current remote CI plus native UI acceptance remain unverified. Tasks 1.1 and original communication 4.2/4.4 remain open as recorded; their closure is not inferred from cross-compilation.
This is a final structural DRY receipt for the supplied limited-implementation scope, not full original acceptance or technical completion. It grants no delivery or prerequisite-commit authorization.
## Bounded communication-owner requirement refresh
The earlier outcome above applies to the original 51-file candidate. A later five-line replacement in `openspec/changes/add-macos-desktop-controls/specs/meeting-session/spec.md:30` is reviewed here only for DRY requirement knowledge and ownership; unchanged coverage carries forward. No runtime implementation, test or configuration changed, and no future-parity owner plan was added to the review.
- Current diff: `/private/tmp/meeting-ticket01-review-final-v2.diff`, SHA-256 `6a9e70bb5dcf0e20f19dd029bdd251f14e528d03037a0a35140254b03066a28b`.
- Current 52-file manifest: `/private/tmp/meeting-ticket01-reviewed-content-v2.sha256`, SHA-256 `147f83cfe076e27aa5ff29eedc8e9084e2b2e9ee39f9c7ab8a5d834100a0928d`; all 52 current hashes verified.
- Corrected owner spec SHA-256: `7dde9acdcbf2b0917e91e82d1078cb1405d715379eb98f68ea5eff09c48697de`.
**Delta outcome: clean.** The desktop-owner scenario now describes an unanswered pending approval expiring or canceling without opening/changing the agent conversation. The communication capability continues to own the shared managed lifecycle, explicit consent, notification/menu independence and approved independent sessions. The owner scenario expresses the desktop outcome of that contract without duplicating deadlines, callback rules or transport implementation. Hiding/dismissal does not grant consent in either requirement; no inconsistent or competing maintenance rule remains. No actionable binding or heuristic DRY finding.
Read the current closeout requirements addendum and its expected-versus-observed contract mapping. Existing actual Passed deadline/cancellation, workflow-denial and stale-project-association tests support this narrow requirement; the native delegate's ignored dismiss/default actions and passed self-tests support its presentation rule. No tests were rerun for this review. Actual notification dismissal clicks, Windows/Wine runtime discovery/assertions and current CI remain the existing acceptance limits.
**Current DRY outcome: clean for the 52-file identity through carried-forward 51-file coverage plus this bounded owner-spec refresh.** This does not assert full original acceptance or delivery authority.
@@ -1,44 +0,0 @@
# KISS review — Ticket 01 / macOS communication prerequisite
Reviewer: independent KISS agent. Date: 2026-10-03. Final status: clean for the current 52-file candidate; KISS-01 resolved by a bounded specification repair.
## Initial candidate identity
- Git root: `/private/tmp/meeting-ticket01-closeout`.
- Base/checkout HEAD: `1b19b08f2e62398661dc24692ce4b776e90e715f`.
- Scoped diff: `/private/tmp/meeting-ticket01-review-final.diff`, SHA-256 `40c69e017e5047b0d59348d22ec879122a9f6d02f4f232843847e8fdefe98dac`.
- 51-file substantive-content manifest: `/private/tmp/meeting-ticket01-reviewed-content.sha256`, SHA-256 `b411b6ddffc6ca4e7c2a76c37ff48a49253073e0382abd0ccb213bc531be2a82`; every listed file verified against the checkout.
- Generated evidence documents and the 295-file assurance manifest are supplementary evidence, outside that 51-file binding identity. Unrelated future-owner parity work remains excluded.
## Resolved finding
**KISS-01 — [P2] Reconcile the obsolete active approval scenario**
Binding repository rule: `AGENTS.md` makes active OpenSpec requirements an authoritative behavior source; the communication change also explicitly promises this reconciliation in `openspec/changes/add-macos-user-communication/design.md:28`.
Nearby context `openspec/changes/add-macos-desktop-controls/specs/meeting-session/spec.md:30-34` still says unavailable approval notifications cause immediate decline and a direct automatic prompt reports a platform limitation. The reviewed new `desktop-user-requests` capability, implementation and operational documentation instead retain requests in the menu when notification permission/presentation is unavailable and open valid approved prompts independently. The prerequisite therefore leaves contradictory active requirements and its design claims an adjustment absent from the candidate. A subsequent maintainer could reinstate the removed behavior or assess acceptance against the wrong contract.
Expected change: include only the communication-specific scenario correction from the mixed owner file, preserving unrelated future parity work. Describe an unanswered/canceled/expired decision as granting no approval and notification dismissal as preserving the pending menu request. No production change was requested. The final bounded refresh below verifies that repair.
## Unaffected coverage
- KISS naming/readability/minimal structure: production request store, prompt adapters, request endpoints, independent session routing, AppKit/UserNotifications host/state, packaging, test-host isolation, test target composition and artifact verifier inspected with adjacent production interfaces/view-model/service composition. No actionable implementation finding or speculative abstraction recommendation.
- Required lifecycle/concurrency state supports current once-only responses, original lifetimes, menu fallback and independent conversations; the lazy session factory has the documented dependency-cycle reason. The verifier remains a small standard-library C# file-based app. Managed feature logic stays in .NET; Swift/embedded JavaScript are the existing required native/UI bindings.
- Tests use public behavior boundaries and explicit native capability skips; deterministic clocks, blockers and synthetic TRX fixtures support the requirements without introducing an unnecessary framework. Synthetic fixtures are clearly distinguished from actual Windows evidence.
- README, configuration/workflow guides, platform verification entry points and CI/Wine command paths are coherent. Helper purpose, invocation, .NET 10 dependency and read-only side effects are documented. CI retains separate portable/Windows evidence and requires verifier success.
- Existing active changes own the requested work; task annotations preserve the open Windows/UI acceptance boundaries. Historical communication evidence remains dated and current totals have their own evidence. No live application restart, deployment, cleanup or unrelated work mutation occurred in this review.
- Tooling results reused from the current verification packet: portable suite 641 cases (639 passed, 2 reasoned Windows skips, 0 failed); seven native groups; HTTP subset 21 passes; Linux subset 22 cases (15 passes, 7 skips); 38 verifier fixtures; normal unsuppressed Windows application/test cross-builds with 0 errors; strict OpenSpec, workflow syntax and diff-check receipts. No tests repeated by this reviewer.
## Limits and authority
Actual Windows/Wine discovery/assertion execution, CI artifact upload and native notification/menu UI acceptance remain unverified. Their tasks remain open. Review grants no commit, push, merge, archive, tracker closure or live-service action authority.
## Final bounded refresh
Performed after the requirements delta addendum and the same DRY then SOLID reviewers' current bounded refresh receipts were complete. Current diff: `/private/tmp/meeting-ticket01-review-final-v2.diff`, SHA-256 `6a9e70bb5dcf0e20f19dd029bdd251f14e528d03037a0a35140254b03066a28b`. Current 52-file manifest: `/private/tmp/meeting-ticket01-reviewed-content-v2.sha256`, SHA-256 `147f83cfe076e27aa5ff29eedc8e9084e2b2e9ee39f9c7ab8a5d834100a0928d`. Both hashes and all 52 current file entries matched. Generated evidence updates remain outside this binding identity. Initial unaffected coverage carries forward.
The additional five-line owner-spec replacement at `openspec/changes/add-macos-desktop-controls/specs/meeting-session/spec.md:30` now describes an unanswered pending request expiring or canceling without opening/changing an agent conversation. Hiding or dismissing the native notification does not approve it. Corrected spec SHA-256: `7dde9acdcbf2b0917e91e82d1078cb1405d715379eb98f68ea5eff09c48697de`. This removes the obsolete notification-unavailability/unsupported-prompt rule and coheres with the communication capability's independent pending-menu lifecycle and explicit valid consent. Surrounding existing desktop scenarios are retained; unrelated future owner requirements remain excluded. The wording is concise and requires no added abstraction or implementation.
Reviewed the requirements addendum in `artifacts/tests/ticket01/closeout/receipt.md` and the actual Passed TRX record extract `contract-delta-proof.txt`. Existing deadline, origin-cancellation, approval expiry/cancellation, workflow denial/expiration/shutdown and unapproved/stale association tests support the revised contract. Native dismissal/default-action handling and passed state self-tests support its code-level presentation rule; actual native dismissal clicks remain unverified. Corrected owner strict validation passed. No test was repeated, source changed, or application started by this reviewer.
**KISS-01: resolved. Final KISS outcome: clean, with no outstanding binding-rule violation or material heuristic finding in the reviewed 52-file scope.** Windows runtime/CI and native UI acceptance limits, and all delivery-authorization boundaries, remain unchanged.
@@ -1,29 +0,0 @@
# SOLID review receipt — Ticket 01
Reviewer: independent `/root/review_solid`; read-only review on 2026-10-03. No candidate edits, application starts, live-service actions or repeated test runs.
Repository: `/private/tmp/meeting-ticket01-closeout`. Fixed base: `1b19b08f2e62398661dc24692ce4b776e90e715f` (`fork/codex/macos-support`, resolved from refs and confirmed by coordinator).
Candidate identity:
- Diff: `/private/tmp/meeting-ticket01-review-final.diff`, SHA-256 `40c69e017e5047b0d59348d22ec879122a9f6d02f4f232843847e8fdefe98dac`.
- Scoped current-content manifest: `/private/tmp/meeting-ticket01-reviewed-content.sha256`, SHA-256 `b411b6ddffc6ca4e7c2a76c37ff48a49253073e0382abd0ccb213bc531be2a82` (51 code, spec, config and documentation files). All 51 current file checksums match. Generated evidence/hash records are outside this identity.
- Scope: existing macOS communication prerequisite plus Ticket 01 test-target, isolation, verifier and CI changes; future parity-owner implementations excluded.
Result: **clean; no actionable binding-rule violation or material SOLID heuristic finding**.
Coverage: inspected the diff and surrounding production/test code against repository AGENTS.md, README, relevant OpenSpec requirements/design/tasks, and the current Windows assurance evidence. Checked coherent request-lifecycle ownership, existing prompt interface substitution, validation and failure contracts, original callback lifetimes, shutdown/duplicate/expired response behavior, native transport versus managed business ownership, independent session lifetime and prompt startup, lazy dependency resolution, matching application/test TFMs, Windows/native helper isolation, fixture-only hosted-service suppression with explicit re-enablement, and CI/verifier fail-closed evidence contracts. C#/.NET preference is satisfied by the standalone verifier; Swift is confined to the existing Apple-framework helper boundary. Source/CI ownership stays in this repository; no deployment or active-meeting action is introduced by closeout.
Requirements evidence reviewed and reused: 641 portable results (639 passed, 2 reasoned Windows skips); 22 Linux focused cases (15 passed, 7 reasoned skips); 21 HTTP checks; 38 synthetic verifier fixture cases; seven actual native helper groups. Current normal Windows application and test builds pass with zero errors and no resource suppression after the existing core tray-package correction. Coordinator confirmed the refreshed portable suite used the unchanged candidate inputs.
Unverified boundaries remain explicit: real Windows-TFM discovery/assertion execution, CI runner execution/artifact publication, native notification/menu UI acceptance and later parity-ticket operations. An exit-zero discovery with testhost error and no cases is rejected, not counted as evidence. OpenSpec Task 1.1 and original communication Tasks 4.2/4.4 remain unchecked. This receipt grants no commit, push, merge, archive or ticket-close authority.
## Bounded refresh after KISS-01
Current identity: `/private/tmp/meeting-ticket01-reviewed-content-v2.sha256`, SHA-256 `147f83cfe076e27aa5ff29eedc8e9084e2b2e9ee39f9c7ab8a5d834100a0928d` (52 files); `/private/tmp/meeting-ticket01-review-final-v2.diff`, SHA-256 `6a9e70bb5dcf0e20f19dd029bdd251f14e528d03037a0a35140254b03066a28b`. Both identity hashes and all 52 current file checksums matched. Original 51-file coverage above remains valid without reopening unchanged implementation.
Only additional binding delta reviewed: five lines in `openspec/changes/add-macos-desktop-controls/specs/meeting-session/spec.md` correct the existing owner's automatic-approval scenario. Unanswered pending workflow/project requests expire or cancel without mutating an agent conversation; notification hiding/dismissal cannot approve them. Spec SHA-256: `7dde9acdcbf2b0917e91e82d1078cb1405d715379eb98f68ea5eff09c48697de`.
Result: **clean; no new SOLID finding**. The corrected owner scenario is coherent with the managed request lifetime, explicit-Yes adapter contract, workflow owner's guarded window creation, public project-association refusal path, and native delegate's ignored dismiss/default actions. It places the fallback rule with its existing desktop owner and introduces no dependency, responsibility or substitution conflict. Unrelated future parity requirements remain excluded.
Reused actual requirements evidence from `artifacts/tests/ticket01/closeout/receipt.md` and its `contract-delta-proof.txt`: current TRX Passed records for deadline/origin cancellation, both approval expiry/cancellation cases, workflow denied/expired/shutdown, and all four unapproved/stale project-association cases; corrected owner strict validation passed. No test repetition or source edits were performed. Prior Windows runtime/CI and real native UI acceptance limitations are unchanged.
@@ -1,98 +0,0 @@
## ADDED Requirements
### Requirement: Testtarget und Anwendungskomposition stimmen überein
Meeting Assistant SHALL einen portablen Testpfad und einen Testpfad für den echten Windows-TFM anbieten. Jeder Testpfad SHALL die dazugehörige Anwendungskomposition referenzieren und ausführen. Ein Windows-Runtime-Identifier oder ein Windows-Testhost allein SHALL nicht als Windows-Targetnachweis gelten.
Die CI SHALL die Targets explizit auswählen und deren Discovery- und Ausführungsergebnisse getrennt erfassen. Der Windows-Nachweis SHALL die fünf vorhandenen Outlook-Verhaltensfälle entdecken und mit tatsächlich ausgeführten Assertions belegen.
#### Scenario: Windows-Host führt weiterhin einen portablen Target aus
- **GIVEN** ein portabler Testtarget wird durch einen Windows-Host ausgeführt
- **WHEN** seine Ergebnisse berichtet werden
- **THEN** sie werden als portable Ergebnisse gekennzeichnet
- **AND** sie gelten nicht als Nachweis für Windows-bedingt kompilierten Produktions- oder Testcode
#### Scenario: Echter Windows-Testtarget führt Outlook-Fälle aus
- **WHEN** der echte Windows-Testtarget gebaut und ausgeführt wird
- **THEN** er referenziert die Windows-Anwendungskomposition
- **AND** die vorhandenen Fälle für Agenda-Extraktion, Teilnehmernormalisierung und drei Absagepräfixe sind namentlich entdeckt und ausgeführt
- **AND** ein Gesamtergebnis ohne Discovery-Nachweis ersetzt diese Prüfung nicht
### Requirement: Inkompatible Laufzeittests melden keine scheinbaren Erfolge
Ein Test, dessen Assertions eine nicht verfügbare native Plattform voraussetzen, SHALL diese Nichtausführung als echten Skip mit Begründung melden. Ein vorzeitiger erfolgreicher Rücksprung vor den Assertions SHALL nicht als bestandener Test gelten.
Durch Targetbedingungen nicht kompilierte plattformspezifische Fälle SHALL im Nachweis als nicht Bestandteil dieses Targets nachvollziehbar bleiben. Auf der kompatiblen Laufzeit SHALL die betreffende Prüfung ihre Assertions ausführen.
#### Scenario: Windows-Benutzerumgebung oder Windows-Bildrendering ist nicht verfügbar
- **GIVEN** die aktuelle Laufzeit bietet die benötigte Windows-Funktion nicht
- **WHEN** die Benutzerumgebungs- oder Glyphenprüfung im Lauf entdeckt wird
- **THEN** sie wird mit konkretem Plattformgrund übersprungen
- **AND** sie erhöht nicht die Zahl erfolgreich ausgeführter Assertions
#### Scenario: Passende Laufzeit führt dieselbe Prüfung aus
- **GIVEN** eine passende Windows-Laufzeit ist vorhanden
- **WHEN** die Benutzerumgebungs- oder Glyphenprüfung läuft
- **THEN** ihre tatsächlichen Assertions werden ausgewertet
- **AND** ein Fehler führt zu einem fehlgeschlagenen Ergebnis
### Requirement: Native Bildbackends erfüllen den Pixelvertrag
Meeting Assistant SHALL den Crop-Vertrag jedes unterstützten nativen Bildbackends anhand deterministischer Bilddaten prüfen. Die Prüfung SHALL erwartete Abmessungen, Pixelinhalt, Koordinatenursprung und die Ablehnung ungültiger Ausschnitte nachweisen. Ein Ersatzbackend SHALL nicht als Nachweis des tatsächlichen nativen Bildbackends gelten.
#### Scenario: Windows-Cropper verarbeitet bekannte Bilddaten
- **GIVEN** eine deterministische Grafik mit unterscheidbaren Bildbereichen
- **WHEN** der echte Windows-Cropper einen gültigen Ausschnitt verarbeitet
- **THEN** Größe, Ursprung und Pixelinhalt entsprechen dem erwarteten Ausschnitt
- **AND** der Nachweis verwendet die Windows-Anwendungskomposition
#### Scenario: Ungültiger Ausschnitt wird abgelehnt
- **WHEN** ein Ausschnitt negative Koordinaten, leere Abmessungen oder Koordinaten außerhalb des Originals enthält
- **THEN** der native Cropper lehnt ihn ab
- **AND** es wird kein erfolgreicher Ausschnitt ausgewiesen
### Requirement: Nachweise unterscheiden Implementierung, Test und native Abnahme
Die Paritätsverifikation SHALL Implementierungsstand, portable Tests, Windows-Targettests, native Selbst-/Pixeltests und echte operative/UI-Abnahmen getrennt berichten. Sie SHALL tatsächlich verwendeten Host, Target, Laufzeit und Umfang benennen und SHALL fehlende Runner, Berechtigungen, Modelle oder Zugangsdaten als offene Nachweisgrenzen ausweisen.
Historische Ergebnisse SHALL mit ihrem damaligen Umfang erhalten bleiben. Die Baseline von 602 bestandenen portablen Tests SHALL nicht als neuer Gesamttestumfang nach Änderungen an Targets, Discovery oder Skips übernommen werden. Aktuelle Summen SHALL aus dem jeweiligen Ergebnis abgeleitet werden.
Der vollständige Windows-Resource-/Paketbuild SHALL ohne deaktivierte erforderliche Resource-Schritte nachgewiesen werden. Ein eingeschränkter C#-Compiler-Nachweis SHALL als solcher gekennzeichnet bleiben.
Die offenen Tasks 4.2 und 4.4 des bestehenden Changes `add-macos-user-communication` SHALL ihre bisherigen Inhalte und Nachweise behalten. Neue Abnahmeergebnisse SHALL darauf verweisen und SHALL nicht bereits implementierte Features erneut als fehlend darstellen.
#### Scenario: Native Kommunikationsabnahme bleibt nach UI-Timeout offen
- **GIVEN** native Zustellung und HTTP-Entscheidungen sind nachgewiesen
- **AND** echte Notification-/Menüinteraktionen können wegen eines UI-Timeouts nicht ausgeführt werden
- **WHEN** der Abschluss berichtet wird
- **THEN** Zustellung und HTTP-Nachweise bleiben als solche erhalten
- **AND** native Klicks, Dismissal, Berechtigungs-Fallback und Standardentwurf neben zwei Workflow-Fenstern bleiben ausdrücklich unabgenommen
#### Scenario: Vollständiger Windows-Build ist noch nicht möglich
- **GIVEN** C#-Kompilierung besteht nur mit deaktivierter erforderlicher Resource-Erzeugung
- **WHEN** die Windows-Verifikation berichtet wird
- **THEN** der eingeschränkte Compiler-Nachweis wird benannt
- **AND** der vollständige Windows-Build bleibt offen
#### Scenario: Screenshot-Nachweis prüft tatsächlichen Fokus
- **GIVEN** eine kontrolliert fokussierte Anwendung mit eindeutigem Bildinhalt
- **WHEN** der produktive Screenshot-Hotkey ausgelöst wird
- **THEN** die native Abnahme vergleicht das gespeicherte Bild mit dem vordersten zulässigen Fenster dieser Anwendung
- **AND** ein PNG-Existenztest oder Capture-Stub ersetzt diese Beobachtung nicht
#### Scenario: Aktivierter Backendpfad benötigt einen echten Dateilauf
- **GIVEN** ein Transkriptions- oder finaler Diarisierungspfad ist aktiviert
- **WHEN** seine operative Parität abgenommen wird
- **THEN** eine bekannte WAV-Datei wird durch den tatsächlichen Backendpfad verarbeitet
- **AND** Output, Host, Laufzeit und Voraussetzungen werden getrennt je Backend dokumentiert
- **AND** fehlende Zugangsdaten oder Laufzeiten werden nicht stillschweigend angenommen oder durch einen Ersatzprovider verdeckt
@@ -1,41 +0,0 @@
## 1. Testabsicherung und grundlegende Aufnahmekontrolle
- [ ] 1.1 Portable und echte Windows-Testtargets getrennt ausführen; fünf vorhandene Outlook-Fälle entdecken, Assertions ausführen, zwei False-green-Returns ersetzen und den vollständigen Windows-Resourcebuild nachweisen.
Umsetzungsstand Ticket 01 (2026-10-03): explizite Testtargets, isolierte HTTP-Testhosts, echte Plattform-Skips und getrennte CI-Discovery-/TRX-Prüfung umgesetzt. Aktuelle Tests, Inhaltsidentität und offene Windows-Ausführung/Resource-Abnahme stehen in [windows-test-assurance-evidence.md](windows-test-assurance-evidence.md). Task 1.1 bleibt bis zum tatsächlichen Windows-Nachweis offen; der ursprüngliche Kommunikationstask 4.2 bleibt unverändert.
- [ ] 1.2 Den produktiven Windows-Cropper nach verfügbarem Windows-Testtarget mit bekannten Pixeln, Koordinaten und ungültigen Bounds prüfen.
Umsetzungsstand Ticket 02 (2026-10-03): drei Tests über den registrierten Windows-Cropper vorbereitet und normal für den Windows-TFM cross-kompiliert; fail-closed Discovery-/TRX-Prüfung um diese Fälle ergänzt. Der aktuelle portable Lauf zählt 653 bestandene Tests, zwei begründete Windows-Skips und null Fehler. Die echte Windows-/Wine-Ausführung bleibt offen. Matrix, Inhaltsidentität und Einzelbelege: [windows-platform-acceptance-evidence.md](windows-platform-acceptance-evidence.md). Der erfolgreiche normale Cross-Build aus Ticket 01 bleibt als historische Baseline erhalten; Tasks 1.1/1.2 und Kommunikation 4.2/4.4 werden dadurch nicht geschlossen.
Abschluss Ticket 02 (2026-10-06): vorbereiteten Umfang ausdrücklich akzeptiert und lokales Ticket geschlossen. Task 1.2 bleibt bis zum echten Windows-Pixel-/Vertragsnachweis offen. [Abschluss, Review und Auslieferungsstand](windows-platform-acceptance-completion.md).
- [ ] 1.3 Bestehende macOS-Kommunikation mit echten Notification-/Menüaktionen, Dismissal, Berechtigungs-Fallback und Default-Draft neben zwei Workflow-Fenstern abnehmen; ursprüngliche Tasks 4.2/4.4 nicht automatisch schließen.
- [ ] 1.4 Mikrofonwiederherstellung samt Geräteauflösung bei Fehler und gestoppter Engine in demselben Meeting mit fortlaufendem Systemaudio absichern.
- [ ] 1.5 Manuelle Pause und Fortsetzen über macOS-Desktopkontrollen mit erhaltenem Run und wirksamer Aktionslebensdauer absichern.
- [ ] 1.6 Aktive Aufnahme und Nachverarbeitung beim Exit durch ausdrückliche Zustimmung schützen; Idle-Exit und Ablehnung prüfen.
## 2. Weitere Funktionsparität
- [ ] 2.1 Nach verfügbarer Geräteanbindung Mikrofone im Menü auswählen und konfigurierte/Laufzeitwahl für spätere Starts nachweisen.
- [ ] 2.2 Nach verfügbarer Geräteanbindung neue Mikrofone einmal pro Meeting anbieten und nach gültiger Zustimmung nur die Mikrofonquelle wechseln.
- [ ] 2.3 Aktiven Profilwechsel aus dem macOS-Menü mit erhaltenem Audio und denselben Artefakten absichern.
- [ ] 2.4 Abgesagte Kalendertermine anhand Status oder bestehender Betreffpräfixe in beiden öffentlichen Auswahlwegen ausschließen.
- [ ] 2.5 Gespeicherte Sprecherproben über das bestehende Agent-Tool auf macOS geordnet wiedergeben und Fehler/Shutdown prüfen.
- [ ] 2.6 Konfigurierbare Buchstaben-, Ziffern-, Funktions- und Modifier-Shortcuts tatsächlich registrieren und ihre Aktionen nachweisen.
- [ ] 2.7 Unterstütztes Markdown in der macOS-Agent-Präsentation lesbar und auswählbar darstellen.
- [ ] 2.8 Web-, Obsidian- und lokale Artefaktlinks auflösen und zum korrekten Ziel öffnen.
- [ ] 2.9 Bilder und relative Screenshot-Anhänge als Vorschau darstellen und öffnen; fehlende Ziele verständlich behandeln.
- [ ] 2.10 Live-Agent-Aktivität und geordnete abschließende Worked-for-Details konsistent darstellen.
- [ ] 2.11 Leseposition erhalten und nur am bisherigen Ende neue Inhalte automatisch verfolgen.
- [ ] 2.12 Explizit eine frische unabhängige normale Agent-Unterhaltung ohne Modellaufruf öffnen und vorhandene Drafts erhalten.
- [ ] 2.13 Resemblyzer auf unterstütztem frischem macOS isoliert mit kompatiblen Pins provisionieren und echte CPU-Vektoren nachweisen.
## 3. Offene Betriebsnachweise
- [ ] 3.1 Frontmost-Screenshot über den echten Hotkey mit mehreren Fenstern und eindeutigem Bildinhalt sowie verweigerter Berechtigung nachweisen.
- [ ] 3.2 Jeden aktivierten ASR-/Final-Diarization-Pfad mit einer bekannten WAV operativ prüfen; Resemblyzer-Nachweis aus der frischen Provisionierung wiederverwenden.
Windows-Anteil Ticket 02 (2026-10-03): deutsche/englische WAV-Fixtures mit bekannten synthetischen Stimmen, Texterwartungen, Hashes und framebasierten Zeitreferenzen samt C#-Generator und öffentlichem Diagnoseablauf vorbereitet. Windows-Prozess-/Profilinventar und echte Backendläufe bleiben mangels zugänglicher Windows-Laufzeit offen; beobachtete macOS-Einstellungen belegen keine Windows-Aktivierung. Weitere Grenzen und Ergebnisse stehen in [windows-platform-acceptance-evidence.md](windows-platform-acceptance-evidence.md).
Abschluss Ticket 02 (2026-10-06): WAV-/Verfahrensvorbereitung akzeptiert; lokales Ticket auf Benutzerauftrag geschlossen. Task 3.2 einschließlich aller fehlenden Windows-Backendläufe bleibt offen. [Abschluss und offene Abnahmematrix](windows-platform-acceptance-completion.md).
Alle Einträge beschreiben zukünftige Arbeit. Dieser Planungsschritt startet keine Implementierung, Anwendungstests oder Abnahme. Die fachlichen Ergänzungen werden im jeweiligen bestehenden Change geführt; frühere Nachweise und die ursprünglichen Kommunikationsaufgaben bleiben erhalten.
@@ -1,62 +0,0 @@
# Ticket 02 — accepted preparation closeout
Date: 2026-10-06. Local Ticket 02 is **closed on the user's explicit acceptance of its prepared scope**. That scope is technically complete; final document/identity checks are recorded in `closeout/final-checks.md`. **Full original Windows acceptance remains verification-incomplete.** A local implementation commit is authorized, but its necessary pre-existing prerequisite inclusion remains a separate unresolved scope decision. No commit or push is claimed here. This change is not archived.
## Scope and identity
Git root: `meeting-assistant`; requested delivery branch: `codex/macos-support`; unchanged current HEAD: `1164c26846686c1912fd1816cd06de80352e9504`. The review base is this SHA plus the exact recorded pre-Ticket02 dirty baseline, not a claim that HEAD already contains the prerequisites.
The accepted slice adds three tests through the registered Windows crop interface, fail-closed named discovery/TRX rules and their synthetic checker tests, known German/English synthetic two-voice WAVs with reference manifests and a .NET file-based generator, and the documented Windows acceptance procedure. Production crop behavior, backend selection and application semantics are unchanged.
The exact 15-path reviewed write set, four previous implementation/document files, fixed current snapshot and binary diff are retained under ignored `artifacts/tests/ticket02/closeout/`. Four reviewed paths already existed as untracked preparation before Ticket 02: the checker, checker tests, platform verification documentation and parity tasks. The local issue is the fifteenth path and remains ignored; it is not force-added to Git. The versioned preparation comprises the other 14 paths plus this closeout record and its review receipts.
| Identity | SHA-256 |
| --- | --- |
| Fixed review content manifest, `closeout/reviewed-content.sha256` | `b22cf4a6fafd308bdd5537844794a2c9007268ad215fb52b452fe0612de3fb3b` |
| Exact Ticket02 review delta, `closeout/review.diff` | `59806cc2bfafe2713a1d26ea493dc94562ae41a240c038e1579c4d92a5c10fd2` |
| Checked platform input manifest, [windows-platform-acceptance-content.sha256](windows-platform-acceptance-content.sha256) | `aae730c74a8b5f2dd7f4c4596b34459e116578b094c226caeba6c761a1a0a3dd` |
Generated completion documents, reviewer receipts and final-state manifests are separate evidence, outside their own reviewed-content identity. Subsequent changes to the issue status, task notes and evidence header are editorial: they record user acceptance, retained open requirements and actual delivery state. `closeout/editorial-closeout.diff` maps those exact old/new documents; it changes no acceptance checkbox, runtime rule, code, configuration or fixture.
Review receipts retain the original `/private/tmp/meeting-ticket02-closeout/` packet paths for provenance. That packet is now retained byte-for-byte at `artifacts/tests/ticket02/closeout/` (apart from separately checked generated closeout metadata); this explicit path mapping preserves the fixed `current/` content and diff identities.
Unrelated primary-checkout work is excluded. The original Ticket01 file, its historical completion and communication Tasks4.2/4.4 are preserved. Requirements in other parity tickets are not included. The smallest reviewed prerequisite candidate is separately identified in `closeout/prerequisite-delivery-preview.md`; it is not silently included by this closeout.
## Requirements and verification
Sources: accepted local Ticket02, the active [platform-parity-verification specification](specs/platform-parity-verification/spec.md), and the explicit user acceptance/closure/commit/cleanup request of 2026-10-06. Requirements and standards verification is retained in `closeout/requirements.md`; actual test records and the complete host/backend matrix are in [windows-platform-acceptance-evidence.md](windows-platform-acceptance-evidence.md).
| Coverage | Observed result and limits |
| --- | --- |
| Crop preparation W-C01–05 | Three real-registration cases verify every expected pixel at zero/nonzero origins and ten invalid-bound rejections without output. Normal Windows-TFM test/application cross-build succeeds on macOS, without resource suppression. Native assertions have not executed on Windows. |
| Checker contract W-R01–03 / native names | 52 focused synthetic verifier tests pass, including missing/skipped/failed crop evidence and portable contamination. The two independent named pass groups preserve exactly-once/Passed checks. Synthetic artifact tests do not supply native results. |
| Current portable suite | 655 discovered/results, **653 passed, 2 reasoned Windows skips, 0 failed**, `net10.0`, macOS arm64, .NET10.0.7/SDK10.0.203. The actual discovery/TRX checker succeeds. Windows per-user environment and bitmap/glyph assertions remain unexecuted on this host. |
| WAV prerequisites W-B02/03 | Native `say`/`afconvert` generation and PCM/reference/hash validation completed. Generator valid invocation exits0; missing arguments exit2; overwrite attempt exits1 and existing hashes remain unchanged. Known source-clip timing includes synthesis silence; it is not forced word timing or a human diarization result. No ASR/finalizer request was submitted. |
| Host/backend/CI reporting | Actual available service context is macOS, not Windows. Current-content external CI was not observed (unauthenticated access401). Runtime, Outlook, both Windows-only assertions, native crop, Windows profile/backend inventory and real ASR/finalizer results remain open with causes per matrix row. |
| Existing lower-level/native Mac checks | Retained in the successful portable suite and dated Ticket01 records; no transferred Mac answer, stub or compiled method name counts as native Windows evidence. |
All platform inputs and fixture/generator inputs were compared with the archived test/build manifests on 2026-10-06 and matched. The structural closeout changed no substantive input. The existing full suite, normal cross-build and native fixture-generation results are therefore reused. The historical 602/639-pass runs retain their original dates and are not current totals. Feed/NAudio build warnings remain documented in the implementation evidence.
OpenSpec Tasks1.1,1.2 and3.2 stay unchecked. Every original unchecked Ticket02 criterion and matrix row remains unchecked/open. Local ticket closure reflects the user's accepted preparation scope, not completion of the original operational matrix.
## Sequential structural review
Substantive review applies to the prepared code and behavior-bearing procedure/evidence documents. Repository AGENTS and the shared code-review criteria cover surrounding context as well as the exact delta. Standards ownership: repeated acceptance knowledge → DRY; public contracts, composition, dependencies and errors → SOLID; readability, simplest current design, helper documentation and remaining manual standards → KISS. Compilation, behavior tests, artifact/fixture correspondence, whitespace and strict OpenSpec validation provide automated coverage.
| Pass | Independent reviewer / receipt | Outcome |
| --- | --- | --- |
| DRY | `/root/ticket02_dry_review`, [dry.md](windows-platform-review-receipts/dry.md) | No binding violation or current defect. DRY-01, a small shared named-pass helper, is consciously deferred: both explicit groups have independent fail-closed coverage and the extraction supplies none of the missing operational acceptance. Same reviewer confirmed no material unresolved current risk and unchanged content. |
| SOLID | `/root/ticket02_solid_review`, [solid.md](windows-platform-review-receipts/solid.md) | No binding violation or actionable finding; native registration, fixture responsibilities/dependencies and artifact/evidence contracts checked. |
| KISS | `/root/ticket02_kiss_review`, [kiss.md](windows-platform-review-receipts/kiss.md) | No binding violation, actionable simplification or newly discovered behavior defect; public native boundary, helper/documentation coherence and remaining manual standards checked. |
The three receipts cover the fixed manifest/diff above. Editorial acceptance/delivery notes are checked by exact diff, JSON/checkbox consistency, links, whitespace and strict OpenSpec validation; structural review is not required for those status-only changes. No reviewer supplies Windows runtime or external CI evidence.
## Delivery and cleanup
Authorized: close local Ticket02, create the local implementation commit on `codex/macos-support`, and remove unnecessary task-owned worktrees/files. Push, remote issue messaging, whole-change archival and live service restart are outside this request.
**Delivery pending prerequisite scope decision.** Current HEAD lacks the test checker linkage, Windows test target and isolated hosts that Ticket02 uses. The existing composition guard also directly depends on the uncommitted communication implementation. A Ticket02-only allowlist commit on HEAD would not compile its portable checker tests and would compile its native crop tests out. The prepared 52-file Ticket01/communication prerequisite has separately pinned code, reviews and seven evidence documents, with a concrete two-local-commit preview in `closeout/prerequisite-delivery-preview.md`. Ticket01's existing closure explicitly held inclusion of that communication prerequisite in both commit and push pending an independent scope answer. This acceptance has not supplied that separate answer. No broken partial commit or implicit prerequisite inclusion is performed.
Before cleanup, all original raw evidence was confirmed identical to the retained `artifacts/tests/ticket02/` copy. The four exact pre-Ticket02 baselines, allowlists, fixed review snapshot/diff, verification matrix, reviewer receipts and prerequisite preview are preserved in `closeout/`. The task-owned `/private/tmp/meeting-ticket02-20261003` source/build/native/.NET caches and regeneration files are removable; actual removal and retained evidence correspondence are recorded in `closeout/cleanup.md`.
The registered `/private/tmp/meeting-ticket01-closeout` worktree remains necessary for the unresolved delivery step and is retained. The primary checkout, unrelated dirty files, shared caches and live application state are retained. Final delivery can continue from the preserved exact packet once the prerequisite inclusion question is answered, without repeating unchanged implementation checks.
@@ -1,301 +0,0 @@
91e76a154aa4eca8efee1319ac983eae2f5a53928422a9a76788fb01fb6aba1d .gitea/workflows/pr-push-build-and-test.yaml
6dc5d5ebc0aad0daa8b82ad30a8630d46df7a5609290427af13ff4ba971a28d0 MeetingAssistant.Tests/AsrDiagnosticEndpointTests.cs
b2f9cefefa9d0911a69cd667297894854af30b871cf041a54d5885db1aa2c682 MeetingAssistant.Tests/AsrDiagnosticTests.cs
1369e1c268dd37f213afacd04db187951d04317db01fbc0bc43b6a1e59d3056e MeetingAssistant.Tests/AudioMixingTests.cs
60d8f9b46f728210c3cdacf6fec920148b90e98f7772007d3d21b22e35f1c60f MeetingAssistant.Tests/AzureSpeechRecognitionPipelineTests.cs
66bbfbfd71b11e5f6bd0f41c960aeda1eded2ccffe74a8accd84835f1031fbfb MeetingAssistant.Tests/AzureSpeechSpeakerIdentityMatcherTests.cs
e8fb0c55cc0c325f2e286195d991739cb16dbcc25f6e363a2d2c0a7117498b96 MeetingAssistant.Tests/AzureSpeechStreamingTranscriptionProviderTests.cs
2f81593c9d3f15cfede2fdb7bd794b843553529e2e3bb7227164e4b9385e1dc0 MeetingAssistant.Tests/CalendarRecordingPromptSchedulerTests.cs
1f7890ded7455664ee5fe085fa97310057d8fce15265f7e054a709d882986fd8 MeetingAssistant.Tests/ConfiguredSpeechRecognitionPipelineFactoryTests.cs
2cd98f83495aea5e885bf302e985c353b26d9d0c833560447bf2942283954827 MeetingAssistant.Tests/DesktopCommunicationEndpointTests.cs
99ee6e56e8b4cd1807adcef6bc6b8db7dbbc9dd3c2b2cec77d7a7e05b30ca401 MeetingAssistant.Tests/DesktopUserRequestTests.cs
f07b38fb359325ccf53b61bf1ce2fbe4ea1f9cb87a8cda68919f51b8bdc55708 MeetingAssistant.Tests/DictationWordStoreTests.cs
f41a3aa87ce8ed0219484e1fb7af843cfea4fc1c118cd2fa70ca9b63d6f495bd MeetingAssistant.Tests/Fixtures/sample-16khz-mono.wav
b02da1c343e29de47c7d3755923d6e48fd12e30fbef921e60ec6ebd462b91a85 MeetingAssistant.Tests/FunAsrBackendLifecycleTests.cs
2c18e9d6ee9675552d7039924d2d577e794794ed0397e920c58abac9a6103eb3 MeetingAssistant.Tests/FunAsrStreamingTranscriptionProviderTests.cs
62962ffcb2c16bac2f5a659c2b24a9194929610376e62a90ef31482b16ebf36a MeetingAssistant.Tests/FunAsrTranscriptFinalizerTests.cs
df4b0466a3442a74810ed6ceb674df64a74e16b619df6760fde5cae47dbb35a9 MeetingAssistant.Tests/HealthEndpointTests.cs
365c30ecf0dbee5207e56a60a46c7024d94650827b789c4a7e92529b61f287f2 MeetingAssistant.Tests/HotkeyDefinitionTests.cs
e7e86ac28260fed19988e8d364cd2cef16e3e67192d2bf97e5706f89a5d1b7d0 MeetingAssistant.Tests/IsolatedApplicationFactory.cs
003c9a58a9189bb7deaaaa80d2525de07a6e8ed79f2d1ab215083a12da3a8636 MeetingAssistant.Tests/LaunchProfileOptionsProviderTests.cs
45828c997f73ba2084af43fb1c624ef52d2f484d51619eac8d8a60c8774a3075 MeetingAssistant.Tests/LiteLlmResponsesChatClientTests.cs
b4bda0eeeabb0cd7be6f381013339a32b0149bf3a7800b7e217084e49efbaed4 MeetingAssistant.Tests/LiteLlmScreenshotOcrClientTests.cs
18e6058b7214a1da6dfb1242a51bc6efc5995a9c074f2829f926f57029d5fef4 MeetingAssistant.Tests/MacOsDesktopControlManifestTests.cs
6a40760c8fb533ba921ca990ff01068f03db8685900e0bb43d4e9ef74e974e9d MeetingAssistant.Tests/MacOsFactAttribute.cs
8b3e249bf5f68891c394a40997353574503842d19d11fc886fb16599d1f078fe MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs
bb048508a730c4294930f334677832ad298fb5173b7200c754f05d251d8c58b1 MeetingAssistant.Tests/MacOsMeetingIntegrationTests.cs
463b49a3c80664fb0c7b255d506ee54f676f977dc0851bb007372536dc46c126 MeetingAssistant.Tests/MacOsUserPromptTests.cs
be316424619f07f6fd1f740fa01474c00b3a183421c6799f602eedc6f9464d99 MeetingAssistant.Tests/MacOsWorkflowRulesEditorSessionTests.cs
2489b415383c7d2a46a4b3a3b137e04b1289820c226bf05a609be858a3467cea MeetingAssistant.Tests/MeetingArtifactContentDetectorTests.cs
e49350db2ba004eba9465c798b648480375767ab329938f55ae920a95de1619d MeetingAssistant.Tests/MeetingArtifactStoreTests.cs
8474342474be8d8ca05692d6f28baca99c3fa05b69f726f954702444417da049 MeetingAssistant.Tests/MeetingAssistant.Tests.csproj
5d0a362d7b4f9305cbec53876cdaf3383d4d990a1dd8d1d8be7d4233dc4f7a91 MeetingAssistant.Tests/MeetingNoteStoreTests.cs
e9abbc44053247102f0d3a38985d3c49cf2c773207c8864d13c27a411be9a0f3 MeetingAssistant.Tests/MeetingRunArtifactCleanerTests.cs
e69be02e39882a2189538cab9df415490b8e6a2bb6ad5605e6fa6af55f403536 MeetingAssistant.Tests/MeetingScreenshotServiceTests.cs
f28bdfca97561cf1143f72b6b4c3e270ecb51fce5489827bbe058279a347e052 MeetingAssistant.Tests/MeetingSummaryArtifactResolverTests.cs
56027e5ba84aaf0d74af1b28ae70f984c7822b9d3aebf5a18e42186e6705cdb5 MeetingAssistant.Tests/MeetingSummaryFailureWriterTests.cs
6c3cc60f9e9f726b0fc100d804fe969ee631151a6ddd51148b7bff0e21bef7f7 MeetingAssistant.Tests/MeetingSummaryInstructionBuilderTests.cs
4b1b343089b94a7d9f2bdef6d59fdc0c0d33dc95dee268f230116a4af21d201f MeetingAssistant.Tests/MeetingSummaryRetryRunnerTests.cs
458136d08124f8ba01a0d03f882959446096effc11a77b3e15bd15c866b18f6b MeetingAssistant.Tests/MeetingSummaryToolTests.cs
34f02b4bb317265f7c914bf9a473446b2b43102260ada0fb1b45802c1d0ced5d MeetingAssistant.Tests/MeetingWorkflowDiagnosticEndpointTests.cs
59db6f6b8da4e7ba3d1a14b6897300f7d915fac60b5364a29ba4b8f4ae2fad02 MeetingAssistant.Tests/MeetingWorkflowEngineTests.cs
8afaa353e8988894f4a7e1165051f23e1177a7ed802f261f3338c8c4cbe837b4 MeetingAssistant.Tests/MeetingWorkflowTestRules.cs
f31361b087be1cd6402c08e3730930df6ee0ba1bafda5c3fde3a41c1b2e0e54f MeetingAssistant.Tests/MicrophoneAudioSourceTests.cs
972f26c3187f447f24285bd595a36b001c4a12c74029a3f6f23f1077a3c41545 MeetingAssistant.Tests/MicrophoneSelectionTests.cs
e8c3fe4abe0faa0c4fb6486de4af5f24cdeeeeacb98f0fd1cf5badf05ed36eda MeetingAssistant.Tests/MicrophoneSwitchMonitorTests.cs
100d98ec49a43b99dbf29e18197f9217757562edbe4ae8e770fae0460f9a06e1 MeetingAssistant.Tests/NotificationActivationArgumentsTests.cs
099d78c42e882cda0081223cedc207e540954dcc5a643540ee8302fe52e76555 MeetingAssistant.Tests/NotificationExpirationPolicyTests.cs
b1a56fa02037699610ea26e3de10679d9465c539fca4d1b0ff60b2181bad1534 MeetingAssistant.Tests/OutlookClassicMeetingMetadataProviderTests.cs
b4484d7cebfe689431527312a712f468744a3105eb2b6fc08739cd6125b473b5 MeetingAssistant.Tests/OutlookMeetingCandidateSelectorTests.cs
d07b8803684195b0b1984987b80c2f182e1d1d66d966d4932ad2df1a6760cd52 MeetingAssistant.Tests/PastProjectMeetingSummaryToolTests.cs
97ade95eb80ce062417402da826f27bf757e2e88bcc503d620572b80eb01842a MeetingAssistant.Tests/PlatformTestVerificationTests.cs
86a59a89147737ed4b68a079b1725b5f3865c2e4af6bc305066dc6484eb0f020 MeetingAssistant.Tests/PngTestFixture.cs
ee38191b4d90d93cdfc35469ee0790836025ebf2f0a6e65d433c5df42387135c MeetingAssistant.Tests/ProcessCommandRunnerTests.cs
2191cedca7083d31a2477a52587bd30c63f4fea51f7165cd65f6eedee2d0fcd8 MeetingAssistant.Tests/ProjectKnowledgeToolTests.cs
0d3494860ce044a2e73e7edba11017b910ca5c0207e2a79da8ff08bde2ff0dfe MeetingAssistant.Tests/PyannoteDiarizationWarmupHostedServiceTests.cs
90f8743cdae0d049493848990cd75728aad13024da72e6667bea0173867117e1 MeetingAssistant.Tests/PyannoteSpeakerIdentityMatchValidatorTests.cs
2c7c731df13affad5696dffcadd57cc47a348aec17dcf13feb7ebe8803f098e7 MeetingAssistant.Tests/PyannoteTranscriptFinalizerTests.cs
590d10a614db2ed5dbd4c3b392db0acdda07726aff52bd2a799de60d4f39ef48 MeetingAssistant.Tests/RecordingCoordinatorTests.cs
a2c2749df28d3c3db0cfb7de7b20b3a84886f9f91deacbf4e863f84e5bc6ac9e MeetingAssistant.Tests/ResemblyzerSpeakerIdentificationServiceTests.cs
6a5a783f609dfc10082cb66587fe8c1c71c513f12b5aa6024cc33380d4e61ead MeetingAssistant.Tests/ResemblyzerSpeakerIdentityMergeServiceTests.cs
e988d18c86c09e6e5a62e29e43766f09a66a1a95f4ef7cd1b868ce1aa1a09454 MeetingAssistant.Tests/ResemblyzerVoiceClusterMatcherTests.cs
0d46999f8270aa4b2615ef06fa6ed5653e6e9e5400188c96e57b89fe6d3e6a14 MeetingAssistant.Tests/ResemblyzerVoiceVectorOutlierPrunerTests.cs
fd6045211499ffe71abab9dfddb7fe2f89018710d9e9b6f13f2cfc827fb1a52c MeetingAssistant.Tests/ResemblyzerWarmupHostedServiceTests.cs
477c10ba0d0ef0848377fb2632773c9d486c6d302dd6b8421b416e11de7cdc7d MeetingAssistant.Tests/RollingAudioBufferTests.cs
be536394161b9ecdaaed8f956836c0c563e6778c078cf8bfae6b96eaa1511d3e MeetingAssistant.Tests/SampleWavFixtureTests.cs
0a039169a14f7a8d8f9d85b0d675e506f1f910047801a2bce2bc4d4c51c342f4 MeetingAssistant.Tests/ScreenshotOcrOptionsTests.cs
c18d4f57ab6aebdf909670b26ce877c19881b695fd9bcbfc4eb4d8254123662e MeetingAssistant.Tests/SpeakerAudioSampleCollectorTests.cs
7ad3f496aebcdd664f5d674417e05540716a27b5000f50ba60e8104fad759c5e MeetingAssistant.Tests/SpeakerIdentityMergeServiceTests.cs
8922d2e6b4fe12c98ffefb96eda7ce948e161b11f04e3b375bd5579fccfe2607 MeetingAssistant.Tests/SpeakerIdentityMergerTests.cs
018f739add5c74c902b39044a738d58167739680d6549139d581126fca2334d3 MeetingAssistant.Tests/SpeakerIdentityServiceTests.cs
4566877ffcf1b8a48fe662e472cf39e14ec8ed364d87468762c5c551c503e7bf MeetingAssistant.Tests/SpeakerRecognitionFeatureSelectionTests.cs
9562ddc5c921b497aa91ddfdc0b52a8f352e16955130998565a2aa486cd49ece MeetingAssistant.Tests/SpeakerSampleSpanSelectorTests.cs
b8b55effa2910859b25a83d98647d85db408c079115dd7cd43b3b46b66c5afff MeetingAssistant.Tests/SpeakerVoiceVectorPersistenceTests.cs
a3e7156726d6b8efc5d7f55b187f116f8c899b36b803159be492d0ef6ab8ea25 MeetingAssistant.Tests/SpeechRecognitionPipelineHostedServiceTests.cs
27e4c406a277439d7a3f0fdb69800b4cb76901b23a7660cd086c0e4fef24a475 MeetingAssistant.Tests/SummaryAgentRequestTests.cs
ac8bce45a837ff2484f8de0df94ccf4ab64b0ff97113d06e0ca2de339bcc4b8c MeetingAssistant.Tests/TaskbarIconTests.cs
922f766384dc8f70097508afa7c9387a0113fc1b7993a3fbae2dabf24213fc13 MeetingAssistant.Tests/TestResults/macos-main-sync-green.trx
2d031e3b523ece636b6b00049bf3e35071fa518f725f3995b782bf6bed949518 MeetingAssistant.Tests/TestResults/macos-main-sync-red.trx
b27ce20549d34358e130d2e18d5aa00ffec4b371679c20b75c2c204f2bd607f8 MeetingAssistant.Tests/TestResults/macos-startup-and-channel.trx
8c455a2ab8ab243df825bc0b25cff60897dbd4da7c94de659783ed8be9ef47e6 MeetingAssistant.Tests/TestResults/macos-user-channel-full.trx
af2a12575df36e1a1c0c2058f440c99d0b7453eaae9999c93a641130f25d21da MeetingAssistant.Tests/TestResults/managed-channel-final.trx
0fba4d40a3986da1b1a6e300d64210584c12ae97945fb4163ef334b664ec2c43 MeetingAssistant.Tests/TestTargetCompositionTests.cs
535d71a1a76899deed76fca9bdccbc029ea5ced9f58659230faac0148de565af MeetingAssistant.Tests/TransformingAttendeeWorkflowEngine.cs
4de9e646a368dba3a9639982c27edc66e56e834d4f1c1de34c23981fc3dd3fe9 MeetingAssistant.Tests/VaultPathTests.cs
ff386af4e4e51219328b56c85d4b6681950acc1eba72d0e7073050b54e422256 MeetingAssistant.Tests/VenvResemblyzerVoiceEncoderTests.cs
72213205de4bb722c0eeb57d2ac1de9a5c2aff4ed1c09797333610c57b3cb09e MeetingAssistant.Tests/WindowsFactAttribute.cs
afce1492feffa9b172afa2e1862a4c8213804fc21fc12caa1b96fefdbc3235f3 MeetingAssistant.Tests/WindowsScreenshotImageCropperTests.cs
b2c113f67fb0f28c0692e9e49b8d153d3dbf4b5b4232630dd8336ac3fe506365 MeetingAssistant.Tests/WorkflowRulesEditorTests.cs
cbb7fd7ee6e61ba760aaef46d65915a5e9abd5fcdbd13185e4c4ea2c92e1bd23 MeetingAssistant/AgentFileToolContent.cs
516542f16ae3182ebced9526d0bf6f4759ef8611e3ae84c97e204a61cc8cdef9 MeetingAssistant/Assets/meeting-assistant.ico
fb97893a50f252eeccca9eb159797ba548e83a673f91ae1fb9a95802444083f2 MeetingAssistant/Calendar/CalendarRecordingPromptScheduler.cs
7552dd3e1a2a7eb066876f85ae574ec67d647b745da30856304d61ea05f1bc89 MeetingAssistant/Calendar/OutlookClassicCalendarMeetingProvider.Windows.cs
2b8ff020cfbfae1b7ec87ba8b272a521395473babfd9787fa2b200855ef45c67 MeetingAssistant/Calendar/TeamsMeetingMarkerDetector.cs
5d287d08b7c6df618da2cb734ae1ccf4f57a7a9d15829dc58f6d648e7b205ba2 MeetingAssistant/Calendar/WindowsMeetingStartPromptService.Windows.cs
20bad4ec197d033bb997d4d1c7ba1571f882981aa06a9bd523705f3d3b871a78 MeetingAssistant/Content/Project-AGENTS.md
8cc27e25fc569c6eba243f0ebc635ba382f429b482ef3054bc307ebf4ff024fa MeetingAssistant/GlobalUsings.cs
fa73659079a4cab632f6863c837184263889641344de4adf798b08db24bca3af MeetingAssistant/HostOperatingSystem.cs
16b7ee3440ff44a3469c0af9c83e134602c216da59c7a4dd3e5a0d1e1c4a96ce MeetingAssistant/Hotkeys/GlobalHotkeyService.cs
777bb8e179582783543701ed3ef039d3f85b08871065c6e2eabfb5a3fe804102 MeetingAssistant/Hotkeys/HotkeyDefinition.cs
5b06f289817e18f6dc7ce9c6e44e4626cbcc06c7c970af550f30cf77b7644956 MeetingAssistant/LaunchProfiles/LaunchProfileOptionsProvider.cs
bae0a2638d4815a5ce6cf08c45381dfd48d6e48c635fdb2069c7f086e16ea92d MeetingAssistant/Logging/MeetingAssistantFileLoggerProvider.cs
9b7a95a75df54f56110646d88da5271507723e164619161f6beca7d61f94fc85 MeetingAssistant/MacOs/DesktopCommunicationEndpoints.cs
e50b9c92f44da8acac7534cf7674288698c9f5d75b10a50365269b0edc6f4d5f MeetingAssistant/MacOs/MacOsAgentApprovalPromptService.cs
f8070038fcaf85ce7736f2b30e38addcaef6746620fa79959d16ba6510a2bee0 MeetingAssistant/MacOs/MacOsDesktopControlManifest.cs
23358bc155b578605dddcee43be66cd0c8dec89c3c4a6575907b373634f5f26a MeetingAssistant/MacOs/MacOsDesktopControlService.cs
07cbdc06f40ea3053fb09046f807ceca94e7e749193f8e183b224c76f00cdb54 MeetingAssistant/MacOs/MacOsMeetingInactivityPromptService.cs
2bf4a7d1c962eea1dd8797e3a158fb262a0a27b4d6971c487d0a46e29beff307 MeetingAssistant/MacOs/MacOsMeetingIntegrations.cs
06aeec6e98c72f30fcecd169af4f56bcd528dcf2efe1708cc62558e17c6dbc16 MeetingAssistant/MacOs/MacOsWorkflowRulesEditorSessions.cs
fa68b270dc16af4335b41e70a9df6fec7834190cb6060eaa49d5ab620422223e MeetingAssistant/MacOs/MacOsWorkflowRulesEditorWindowService.cs
ede2452b893d4a06e163413386ea971f0d9584a93f9f967026c51be287914dfc MeetingAssistant/MeetingAssistant.csproj
f6fbba44e2791abbb4f89ab3ebfb41c600f46838951a29ecee2ede3dd0e0e730 MeetingAssistant/MeetingAssistantOptions.cs
87141fba571b3300fa706cb0518d49a405350dee1c8be0cf2a86507838a36e90 MeetingAssistant/MeetingNotes/CalendarAppointmentMetadata.cs
25cfc6d4442516b31b82d0d05e1463772f906c64d6ce6ad9615ee4cdf02b6a66 MeetingAssistant/MeetingNotes/FrontmatterYamlDeserializer.cs
172b1baee135c62f48ae6af27a73a82726f9fc67862c2e5784ab48906ed06568 MeetingAssistant/MeetingNotes/IMeetingArtifactStore.cs
e324730f8f309678a792e533b6785e49f7611a24056f61045803b65fce6b7fa6 MeetingAssistant/MeetingNotes/IMeetingMetadataProvider.cs
909b08aaee580f71ef3fd0be2869976f0876cba2421dacbc2dc1d433c2a81b9a MeetingAssistant/MeetingNotes/IMeetingNoteOpener.cs
55024bd4f0b3373dc7f65088f4260668670b8634e9d05cb339ae1730f65e9321 MeetingAssistant/MeetingNotes/IMeetingNoteStore.cs
edc597683705c6c84e25425566b8d72560630c4467ba66a50424c0e5051d76e3 MeetingAssistant/MeetingNotes/MarkdownDocument.cs
e23ecd731ea56f1440c3cf8e50fd224995b1717772bb52ed2445588691099261 MeetingAssistant/MeetingNotes/MarkdownMeetingArtifactStore.cs
78ca508bbee9cc141dd711b5528dceb91df988ce2f826e9adcf463c822bcd270 MeetingAssistant/MeetingNotes/MarkdownMeetingNoteStore.cs
6f739f81496837191aa7a0d81201519667e82c4b7b1e898be53369416d50af7a MeetingAssistant/MeetingNotes/MeetingArtifactFileNames.cs
9f9b34b91930a2c5fee6152c98c52c5523088dd31eccc3994844e46c2d642c71 MeetingAssistant/MeetingNotes/MeetingArtifactFrontmatter.cs
ed90222ccb3dec93613484b36c0150cfa2e5219a426f9336130b0ecb49f235b3 MeetingAssistant/MeetingNotes/MeetingAttendeeNames.cs
ccc2cad4feb201453d52ffc895a3056accfe786a3d1e69cf8771b9b09e7ad4bb MeetingAssistant/MeetingNotes/MeetingMetadataDiagnosticRunner.cs
f6baeeb33d51d9921d15485d28d8416e8793cbf798e890b473b09be95f6670b8 MeetingAssistant/MeetingNotes/MeetingNote.cs
88a26351c048e517838d089b2768790567850139d4061455a91200ef99ad858d MeetingAssistant/MeetingNotes/MeetingNoteActionLinks.cs
78d41cbd7807a4ade3d2a3a4544a1c8ff2d7f7d58e514f37313e718bdbdb2e7b MeetingAssistant/MeetingNotes/MeetingNoteTemplate.cs
4ad0caf3f4735e23ba2bde8195f0a6e152117ff3735fb1ef4fd208f520a6c595 MeetingAssistant/MeetingNotes/MeetingSessionArtifacts.cs
1cf5a4e352207cf80120da1d39606bcbb48eb3abae59c534a39cf864bf420fc5 MeetingAssistant/MeetingNotes/ObsidianLink.cs
469ef250e8ee5cf238b8715e4d0b6461077d80fcb1d162530b6423c11322f53f MeetingAssistant/MeetingNotes/ObsidianMeetingNoteOpener.cs
c6633a79b443e064a84bc3b45b20898ab7b0c8632ce4ef98e7126fd31b64b391 MeetingAssistant/MeetingNotes/OutlookClassicAppointmentMetadata.Windows.cs
17ea2e0973fbd6d66e40a0fa89114d3af4d22b1826bb81f9df0f24c667e534e8 MeetingAssistant/MeetingNotes/OutlookClassicCom.Windows.cs
f1b88c3d9056cc0f48e2bd449c021a1457d1758dea4663b51c8900271e21e9c0 MeetingAssistant/MeetingNotes/OutlookClassicMeetingMetadataProvider.Windows.cs
43a11de56500fb87e75c703629506a752f4cb99d29ee9296f18feb174c9bb820 MeetingAssistant/MeetingNotes/OutlookMeetingCandidateSelector.cs
d15fdad8118db014ec7ff6f46e28fddaa7d47f07113e0b30ab110567111d81f5 MeetingAssistant/MeetingNotes/ScalarStringListYamlTypeConverter.cs
742967d499a35a866c3ef2bd46c54f55a4e8454028f51a0b056881e33acb78c1 MeetingAssistant/Native/MacOsDesktopControls/DesktopUserRequests.swift
1fa93f044a84e8a643950c598c916b13092666b0b20393afb67bfa19bc570588 MeetingAssistant/Native/MacOsDesktopControls/Info.plist
e95d7a2ef3d8ad4ffd5dbfef1459ca14aac59e762933638c4b7adcd9a7098edf MeetingAssistant/Native/MacOsDesktopControls/SelfTests.swift
d7eb25d2de1d5b505f85c8d95b69227a9b1c44e020f7bd4d82bc5e3a766fdb5f MeetingAssistant/Native/MacOsDesktopControls/main.swift
e2dc1350f37b34128ad66c01350e9601277f85048f30940eb3594aa0b46c5673 MeetingAssistant/Native/MacOsLauncher/Info.plist
516c2620f4ea1f9944ecaa65a77c28d69dcf2ec4f6a8aa5fbb8213d6fc9b7140 MeetingAssistant/Native/MacOsLauncher/main.swift
c2874a5aba87e53641498960bee5043278ee4acbee892c5ed39e140713a7255d MeetingAssistant/Native/MacOsMeetingAudioCapture/Info.plist
d8b7e06e668e1bf41204b8499653fcead82c98376681dc0fb1508683ff2d83ef MeetingAssistant/Native/MacOsMeetingAudioCapture/main.swift
84d0a8d921a56a2769025ea053e74c94a14100ccc1a52d2ef7d6a5d64e06e974 MeetingAssistant/Native/MacOsMeetingIntegrations/Info.plist
c8e3a33cfc807b98e5ad34d50fda9b082c5e04fb974b03649ab4e53b1adabc82 MeetingAssistant/Native/MacOsMeetingIntegrations/calendar-events.js
04b49f5d16451acff42fd026c1b00f0e98ebc10d04f12ad4f6279d6508cd30d6 MeetingAssistant/Native/MacOsMeetingIntegrations/main.swift
6f2c47799cff89f752ed5dd2d235642386f5df0352bbccb0565489c741e67721 MeetingAssistant/Notifications/AgentApprovalService.cs
79535bb1822ad987151ab736e10c2da82a535a8ada89793c7d6d4dd8eccbd7a2 MeetingAssistant/Notifications/DesktopUserRequestStore.cs
7318479dfabaacc63a0c8aced14b831adbbba9efbd2ee5698af5a6e304199206 MeetingAssistant/Notifications/MeetingToastExpirationPolicy.cs
0adc0c87784f945644c50496a67d8f08e9a2cf83aefea98ce23651b1fa61d215 MeetingAssistant/Notifications/WindowsAgentApprovalPromptService.Windows.cs
7c74775d7b9024b09fa2f25aed1b1574683d1f48fc259ed1af287861ab55d901 MeetingAssistant/PlatformCapabilityReport.cs
91e83b6f9ef0e488008ffaa6033db641f73d64048c04a52c7a7f6de061ee03be MeetingAssistant/Program.cs
2ccce08eab7223123b83fc7cd8f00ff727502378060437fbe2d798b6b9fd286b MeetingAssistant/Properties/AssemblyInfo.cs
f28ba8d26146ae7ad396b47c859b16162569602016da18b2c103d6f1ea011c84 MeetingAssistant/Properties/launchSettings.json
027ad2b30cd13b01dd0d51d73e444faf5b6dfd6728872fd29bb5fb7efdfe4850 MeetingAssistant/Recording/AcousticEchoCanceller.cs
934e10efd3a2cc1c3f56bfa3d4d650b2babf32036c28c5c53d65714e92e05c3e MeetingAssistant/Recording/AudioChunk.cs
a4effa6c9ef767b2d5035c15412b5ace9c320a09af67da63cce0ba814ee9f0c4 MeetingAssistant/Recording/CompositeMeetingAudioSource.cs
8d61fbffd603704caadc45994afff4bf10f53e7060969372ef6b556a9fcc338b MeetingAssistant/Recording/FileOfflineTranscriptionBacklog.cs
6270a1fd33185ada5439d9f91396e4598120b5ca1a2647b02c948682abdbb8f6 MeetingAssistant/Recording/IMeetingAudioSource.cs
a4ccc4d6255496abed6be42fe8f016d4714e7634c740f8ff744a8d151c6ecccf MeetingAssistant/Recording/IMeetingInactivityClock.cs
defc0b99ccfda6c5f620e9fd5710036e3a874ebfc86f132594f5fd09597de1c6 MeetingAssistant/Recording/IMeetingInactivityPromptService.cs
9ea689fd1a46ff402a3428c6188f6ee92d15b1e6f19fd788e5742bfe785ae765 MeetingAssistant/Recording/IMicrophoneCaptureSourceFactory.cs
ec9b627cee40776b3d8b7ee00c00d3e8512be0412ee9d1f98f1ed80d75a107b5 MeetingAssistant/Recording/IMicrophoneDeviceProvider.cs
6c65b8cc00f61c61f8b2056426c1b57f5720962a5c97a6435c4d58a11d973318 MeetingAssistant/Recording/IMicrophoneSwitchPromptService.cs
5bd543f5fecb497f31201a19597ca593dd91b5296b8a776c3b273f4a865d6463 MeetingAssistant/Recording/IRecordedAudioStore.cs
79a1814dbc9f4a6509f977da433ff8c750d3b6b5686133476774d5cccf853f9d MeetingAssistant/Recording/ITranscriptStore.cs
7ac4a953bc9d1d632a24a98822d15a6d4029b5e7ca30d16de6486c354016e8a3 MeetingAssistant/Recording/MacOsMeetingAudioSource.cs
5fcb1dc87b74b3e76be55eab1ebcc92881d05091b501f4648923b32019e506ce MeetingAssistant/Recording/MeetingArtifactContentDetector.cs
11958d15bfe9d86a76b8272813c2c6ccf66c4c8a606863ba7c1b7baa837c9e15 MeetingAssistant/Recording/MeetingRecordingCoordinator.cs
96df6ea20efd3bca7261a55694d54bcf91efdfb3c33f6119b483f73b7630162f MeetingAssistant/Recording/MeetingRunArtifactCleaner.cs
9e0ef94354d6c2f4f93db879f4fc54a665774ce8b30069d724a6e5847db49a9f MeetingAssistant/Recording/MicrophoneAudioSource.cs
0891c7a1fba548b2a6e157f9d1709cbfbf7e5b500663d06a63a42930f4b24d1a MeetingAssistant/Recording/MicrophoneDevice.cs
7e9ece8761f3df34ee5fd1dbd529eb8e0223e68159e1ef746d371a8fa2538937 MeetingAssistant/Recording/MicrophoneDeviceSelection.cs
9ff676e8d41be39be1896491fe2a0565af229584658988a33340d7840210e4c3 MeetingAssistant/Recording/MicrophoneSwitchMonitor.cs
f15b35b6587e71b32b9ae6c1f82456e9602898f742975f18c1803eabede6fd25 MeetingAssistant/Recording/NaudioCaptureSource.cs
ab83970d673cfb3686ac2095f3132efefca8c6cb1c1a8e0878a8c5c0b422a05d MeetingAssistant/Recording/NotificationActivationArguments.cs
9affafe45bdcb3f8bb99a50732f1ea4ba015bfc047add89b629dc3ed1e31be59 MeetingAssistant/Recording/OfflineTranscriptionBacklog.cs
d53258404e65af3c37eace6aeea4881faef44a8eb73af8ec0a2da2ba7145313d MeetingAssistant/Recording/OfflineTranscriptionBacklogHostedService.cs
361be18bc3d9f967647ed37455cb1ec0653c6c829f4abb58a32360b0241ae310 MeetingAssistant/Recording/OfflineTranscriptionBacklogProcessor.cs
7125e3fd8485daacfcb968740a7577fb2914b84efcf4b5101768318f33587efd MeetingAssistant/Recording/PcmWavAudioChunkReader.cs
e33796a3cc4eee1c20018ae483599552d3e4d6f705a0797094bba023e834bcd7 MeetingAssistant/Recording/PortableMeetingAudioRegistration.cs
fefda6ed3bdf3f791a0037fc16aebe2e3354caaa75b1d08403179c9aaf90e6cb MeetingAssistant/Recording/RecordingDictationWordProvider.cs
5a32e8d16093b93758ad0dd7ad1229131418db340ef909b3666e3a05f6b716bd MeetingAssistant/Recording/RecordingSpeechRecognitionPipelineOptions.cs
eaa17f0d18acac22aea02596b702c2fe7d67af2e8e5fdee56fbd838defa4fa76 MeetingAssistant/Recording/RollingAudioBuffer.cs
e70a88fc679ff0554020d06ea0ae91b2762435d30447f9d711c332c9cc686a01 MeetingAssistant/Recording/SpeakerAudioSampleCollector.cs
ec2f97e27e5d017c6d2b760633dd599b9f7f2b9fdf61d7a836d3b3da7216fc0e MeetingAssistant/Recording/TemporaryRecordedAudioStore.cs
8062822cd28410a3e02b5a8e73b461999a7bbc431ffadcbe66aebc4d0e9c43e1 MeetingAssistant/Recording/TemporaryRecordingCleanupHostedService.cs
6532e4f2e834e5205dd3a11ac8d8c952e3e681acc10fd9ebab77368363bdd99f MeetingAssistant/Recording/UnavailableMeetingAudioSource.cs
7a481ebc4a62713736a1cd2d552df6fe4df978fe4ac617cee0b6454f6f91c86e MeetingAssistant/Recording/VaultTranscriptStore.cs
94ba33e045746add4dde99f8e5f78967d43816b79408bc08fee2663ca7a3ee8b MeetingAssistant/Recording/WindowsMeetingInactivityPromptService.Windows.cs
2c44fd6f5a0308cbd743247ca4472145091f36cbf60138015e2f6091b754db95 MeetingAssistant/Recording/WindowsMicrophoneDeviceProvider.cs
8314134f5fb3e8b15e4a087fc4a9b12bb9c64602ca2c7e3932c3dae6a9557163 MeetingAssistant/Recording/WindowsMicrophoneSwitchPromptService.Windows.cs
48628fcdc67a4d320758c26ba08b4a06a168e0fe758649605625d9b6e8cc1c91 MeetingAssistant/RuntimeContentLocator.cs
eccfdc0f8807a58fd410d44d869849df10efe4254428fb11a0595f11b117d9f7 MeetingAssistant/Screenshots/ActiveWindowScreenshotCapture.Windows.cs
9db1820ea279a663ab4e6482cd0d025374b9c0eb9fc0142f612bb4a32615f8c5 MeetingAssistant/Screenshots/LiteLlmScreenshotOcrClient.cs
a979a9b5c7e56967b3bcb84b4fe56bb6c906f65f289e5344a688832a24269fba MeetingAssistant/Screenshots/MeetingScreenshotService.cs
562133a693db91480ce122bd4ecf061203a1f5bcc52765fb9acad1f5449be354 MeetingAssistant/Screenshots/UnavailableActiveWindowScreenshotCapture.cs
6b877b57d03441ad41b83e047f8cece044ca837518ab0e5b14e0f6f498c15c24 MeetingAssistant/Screenshots/WindowsScreenshotImageCropper.Windows.cs
619a53c536fc746f6de99926442053102bb0cb5a5d5400c24283cba53e5c45e7 MeetingAssistant/Speakers/AzureSpeechSpeakerIdentityDiarizationClient.cs
f448d3e2830c879d81ccb7722b026534d48d4d6c5683151fc261e9192a466be0 MeetingAssistant/Speakers/AzureSpeechSpeakerIdentityMatcher.cs
2061e8495e8f63eee07414f8a16bd9902d22f2bad3812ef772a5523edab30fff MeetingAssistant/Speakers/NoopSpeakerIdentityMatchValidator.cs
743e5ca2605e41993b7c0e9005bcdb63d69fcb11b97626bf6fd04a420d479b20 MeetingAssistant/Speakers/NoopSpeakerIdentityMatcher.cs
c753e294ba3eac5fdbb66df7aaea0df05e3f4294d3d33d03e4ab541f58fa34f2 MeetingAssistant/Speakers/PyannoteSpeakerIdentityMatchValidator.cs
d5f49795e96976a4a223d79e870ea74552f57894c3e588af0d703a96d2fd3086 MeetingAssistant/Speakers/ResemblyzerSpeakerIdentificationService.cs
f10688955119050ab552cd5a6c3c8e36d59d9fbeebd912716f7fafc918fd9f7c MeetingAssistant/Speakers/ResemblyzerSpeakerIdentityMergeService.cs
e86a69f38ad99662c88b663e74ee74066bf8328ce7872d458231904d1b49a2f2 MeetingAssistant/Speakers/ResemblyzerVoiceClusterMatcher.cs
d1a2c388f3ca7f7d136f23a97813557db09a0248e24ed522a1251fc7dca0a80a MeetingAssistant/Speakers/ResemblyzerVoiceVectorOutlierPruner.cs
2adf8fc145e58a36aaf3097e747765c1a7b8ed01b10c9b95885929c65aa6eec7 MeetingAssistant/Speakers/ResemblyzerWarmupHostedService.cs
7575b039037e69817750451e15cc8562d022bcc4606330f1e5cacc3f2b613e2c MeetingAssistant/Speakers/SpeakerCompositeWav.cs
33d99ddfa3509840a5729ec37e64448577ae48a4a9fa2cefaf4653a5b4edb91a MeetingAssistant/Speakers/SpeakerIdentificationModels.cs
57878fbc1fa6b1117d0e6e0222ac94d122dc713c6245c8e47663c556f20a6c23 MeetingAssistant/Speakers/SpeakerIdentity.cs
d3bca7930c8ca2a0237545ab6e8b87c94f5650b6a2f9d847a3e5fd33fa930fc4 MeetingAssistant/Speakers/SpeakerIdentityAttendeeCanonicalizer.cs
c8a00bafea1d8d9ad4dd04bcc6d6fdb1b269eba57ab3f59825b9a580aeedac4b MeetingAssistant/Speakers/SpeakerIdentityDatabaseInitializer.cs
945e494173147db82fd0410a50a55cb708539b2af16bb1f085cec8b55cc7742f MeetingAssistant/Speakers/SpeakerIdentityMergeService.cs
a2a559cac7bd668c9499d7287dbd11522b98ab2d460da0ab95edfc346bcc5894 MeetingAssistant/Speakers/SpeakerIdentityMerger.cs
5171e7426675eb40f6d23c418baa9cd074eaa66512d307979bca26f469a60690 MeetingAssistant/Speakers/SpeakerIdentityNaming.cs
d6d6f6fe3522d729d07938b40faeb784fd987c9bf8c4f59063b90d94c52c413b MeetingAssistant/Speakers/SpeakerIdentitySchema.cs
23b26e217fb531237f890b2c4ec0131b334b93024ad1e2584b407b5b68432328 MeetingAssistant/Speakers/SpeakerIdentityService.cs
b7792e8830e3d354a687066f10d6c35d43c3fe8f2e81aced65f20b011a3b2794 MeetingAssistant/Speakers/SpeakerIdentityTranscriptAudit.cs
c3e8478ceb77d4e17a5b755db1cb5fa697857b5ce1f20e4c5a34741e7cd0fe2c MeetingAssistant/Speakers/SpeakerOverrideArtifacts.cs
532a5c3b061811f2a7fa1cde9160fdf5e37ab6ec842da908fb3a7351f262c360 MeetingAssistant/Speakers/SpeakerSampleCollectionPolicy.cs
e4736f52ed2dfa3c699dea41d301f1149f9a4351275ba821f2219934db426c43 MeetingAssistant/Speakers/SpeakerSampleSpan.cs
259ddfa895275e8e5a689f70b287950ff9936a5da6717944863e187395833006 MeetingAssistant/Speakers/SpeakerSampleSpanSelector.cs
9530846face5f9d6ad04a69bdc5f153626ee7c7a26a3f55fe169b888074ccb74 MeetingAssistant/Speakers/SpeakerVoiceVectors.cs
97fda26c99c60ff458d64827ea7a5c970b343491469267984fd2eff1280032fa MeetingAssistant/Speakers/VenvResemblyzerVoiceEncoder.cs
b76ebbae06324ea54c568515bd520601086986cbd006791f036545f00bc6bea5 MeetingAssistant/Speakers/WavSpeakerSnippetExtractor.cs
6a24f4733facb0a4c9b6666dce543cd3fe37590923ce9c9d8a2f12c0aede8e3a MeetingAssistant/Summary/BoundMeetingProjectResolver.cs
6c09fc419330cb01765748302b1e7531822e4d3bc9b3d4a5146022e06da4dbdf MeetingAssistant/Summary/FunctionInvocationGuard.cs
a8cb023db11732272f2db18bb65e9eea2e0cbdc8b40561e49a33b04eec937b25 MeetingAssistant/Summary/IMeetingSummaryInstructionBuilder.cs
f680116de80e4b2a7beb1a6e2de93d2edf050655c281f6452132fb7b08a10e0e MeetingAssistant/Summary/IMeetingSummaryPipeline.cs
99d5d7d9c27f9df39c6422ceb78909d41a4978ff19fff6559c29549cae361658 MeetingAssistant/Summary/LiteLlmResponsesChatClient.cs
d99f3bececeb552df96b9ea689ecd39d5406eefda73f6204f1ec345eb2e41804 MeetingAssistant/Summary/LiteLlmResponsesCompactionOptions.cs
9c3ece46e2b79c5b4b54bf01b6152cb13814197e964cbbdf9c1d432183f3beaf MeetingAssistant/Summary/MeetingSummaryArtifactResolver.cs
68286f2775ed400144b4e711f34ba7a9a21cf6ebe19212780158c09dcd9d0bc7 MeetingAssistant/Summary/MeetingSummaryFailureWriter.cs
906d07160665322bbcefadf2f79b44965a7ccaf5ae8572aa2c13d55c3080463f MeetingAssistant/Summary/MeetingSummaryFrontmatterFactory.cs
7d17e0b9db47e758bab4b16de998b9285143d86dadf05f6bdc699c53a363625d MeetingAssistant/Summary/MeetingSummaryInstructionBuilder.cs
f995ac0064f4fa8f90ccc72298fa1906685f23de084b81eb1731fcb554b7fb0b MeetingAssistant/Summary/MeetingSummaryRetryRunner.cs
a526243294e82f62dbbd17ff71c6ae53bbc0adfac121841db11883bef87b5c51 MeetingAssistant/Summary/MeetingSummaryTools.cs
6a6b0a9aad5b6dd46d72dc1077e0badb37790c3ac6b0a240269d84bc4e98cfc2 MeetingAssistant/Summary/OpenAiMeetingSummaryAgentPipeline.cs
b92c985bddbd2b62865604a0d5599fe8480ebd67ad6ac15e7673f991f70ae724 MeetingAssistant/Summary/ProjectMetadata.cs
935ff2c384c6214b3134daef47c8c14ccaec37a3cd92a786ed4ba4c775457eea MeetingAssistant/Summary/SummaryAgentWriteAudit.cs
02f90f9c4460d1396b4e3c4cdf15b3a482dde1f42d0fc98eaa53f6d5025a2516 MeetingAssistant/Summary/WorkflowChangeRequestService.cs
7f15c861be19ce596a2bd70d73e91395e299d744b4ffacb5c2ac188180274bbe MeetingAssistant/Taskbar/MeetingTaskbarMenu.cs
df9d3ce85068fc925ae5502987eba1155df640a350afe6ea5fdf9cda9519a355 MeetingAssistant/Taskbar/TaskbarIconRenderer.cs
076cde3c56640e7977ab16ccbc90be8ea35f6b8eb817480df40d77d6f02bbe91 MeetingAssistant/Taskbar/UnoTaskbarIconService.Windows.cs
829801d111380b645b2c85520d4b4d0e1198149d090fd74f22f323195660dc8b MeetingAssistant/Transcription/AsrDiagnosticService.cs
4f95dd715f75ddd1ec1d9f667a37dfaa1fc98035b1cc0b8c763b2f19e464580c MeetingAssistant/Transcription/AzureSpeechRecognitionPipeline.cs
861f0102e4aa0c8e356dc1e91c84e7fa213dd153e9902284c03b33bac16796e8 MeetingAssistant/Transcription/AzureSpeechRecognitionPipelineBuilder.cs
03ca23c1c72be97fc5c3190d465bc779d871e9fed2457b5a4404b207a74ca9b5 MeetingAssistant/Transcription/AzureSpeechStreamingTranscriptionProvider.cs
6051aed0cc1ddbda5c4e26fc141f06c9d5600a289c595fa4f1946bb7072bd767 MeetingAssistant/Transcription/ClientFunAsrWebSocketConnection.cs
66f4eed765a67954dd0a235a74de364a7cd0ed0eecc966bfb2831506f311d679 MeetingAssistant/Transcription/CommandRunner.cs
4d45b7d16796a4b3555644685d25c624aa5d978ec84f0b3d1e3854518aec0b54 MeetingAssistant/Transcription/ConfiguredSpeechRecognitionPipelineFactory.cs
0ec609ee9a51324f9b6afb3d6cb704c5e08021b7e3bd7ce1fdb095df10c0eaa0 MeetingAssistant/Transcription/FunAsrDockerBackendLifecycle.cs
b1cfdc15ca110ceecaaef81f20964543125c49b0193b78c920f591db13f1c522 MeetingAssistant/Transcription/FunAsrMessageParser.cs
15191a475a02288f94193f6cecfbd2fc264bfa112928836fcc2050ddbb3c1d1b MeetingAssistant/Transcription/FunAsrSpeechRecognitionPipeline.cs
52ca4389b6d9b439f660cc9c8f4b06e3f188e06c2c178736353d8c664c8cdf59 MeetingAssistant/Transcription/FunAsrSpeechRecognitionPipelineBuilder.cs
264447ce064784edf7afd0c2ad436d18954780f082869d7ea04b7d42e5ae46b3 MeetingAssistant/Transcription/FunAsrStreamingTranscriptionProvider.cs
1bee4ac3d9c8ce73d2f9730ba29e790e8603686377e57cf427b742d17989217d MeetingAssistant/Transcription/FunAsrTranscriptFinalizer.cs
1972697d7a367ed787fd1585d426c507601ec662d59d66b794d47b11b79d6be6 MeetingAssistant/Transcription/IDictationWordStore.cs
028710669270ffb702e1832a56741564cea6b8ea3a1e91b3ac2826510648502e MeetingAssistant/Transcription/IFunAsrBackendReadinessProbe.cs
a5eba20e4752055e8f91f5dac1a1dfad42d90896b45741f6d87550d2d120baa0 MeetingAssistant/Transcription/IFunAsrWebSocketConnection.cs
b2e4f00cab55fa3a6c4e98b29f87fbf10fc3deb7a664ffd2dd2e6814d7339ec0 MeetingAssistant/Transcription/ISpeechRecognitionPipeline.cs
f2cebf0b63d3fab1ac18e14f418a541038e40969a19ed491954c09c30b8fc82a MeetingAssistant/Transcription/ISpeechRecognitionPipelineBuilder.cs
29d141c6f63d77a226cb5b0c67b2096a30f5eeacbe27c4e6e775b5c3e4a69c2f MeetingAssistant/Transcription/IStreamingTranscriptionProvider.cs
42a9147566b25db683f94763739360f11e858021ad0effc0dc2b93287d563bcc MeetingAssistant/Transcription/MarkdownDictationWordStore.cs
bcabf6007c9c942845336c1dd958852a8d6dcc383225b4bb1ea97903ef2848e8 MeetingAssistant/Transcription/PyannoteDiarizationWarmupHostedService.cs
7b33738b53c6311a40efa036fdf314e0873bfab9e15fc7ea7a3a99948fad2efd MeetingAssistant/Transcription/PyannoteTranscriptFinalizer.cs
155ac72b0d7792891f5ed1f600f972fa5cd6f650f2f740786e4838fd20a56590 MeetingAssistant/Transcription/SpeechRecognitionPipelineBuilder.cs
ecd227674a327a3db898c256c7d5e9957df2e4fdb309dd6aab980e4e8f8213b7 MeetingAssistant/Transcription/SpeechRecognitionPipelineHostedService.cs
147c4277e1f9118ad63d0eb2cc7864363f262f901088daedfe5b3dc5f3e3c919 MeetingAssistant/Transcription/StreamingSpeechRecognitionPipeline.cs
9c667c025d7140cc1d076ce375521857ccf5869d9d3ad661bf09e8b040e32ee8 MeetingAssistant/Transcription/TranscriptionSegment.cs
aa9d58bf3f970bc7575ac28da415e3988cce825b4978010b4bdc87e88fba60ce MeetingAssistant/Transcription/WhisperLocalSpeechRecognitionPipeline.cs
a41885889c9e6223263bb52db8e9fb7ce59bdf3f85b05fb5e70ae9a285a880d2 MeetingAssistant/Transcription/WhisperLocalSpeechRecognitionPipelineBuilder.cs
7d213545d4d70e905088dc3171e3a7b2586540e9400ff44e52148273dc13d713 MeetingAssistant/Transcription/WhisperLocalStreamingTranscriptionProvider.cs
114eba73bede65102c4763ffc7cbb8287666910090f4516ce4180e537536ce47 MeetingAssistant/VaultPath.cs
eb46fef7a48a0878159cdb6fda34534e9a7b908ed0bd2cd975eed64d08399bed MeetingAssistant/Workflow/FileMeetingWorkflowRulesProvider.cs
6cc629b70fe63e6a61fc46ac31af647ccae322da9c1122066e6956ecba4d69ac MeetingAssistant/Workflow/IWorkflowRulesEditorWindowService.cs
a308fcf7aa67880d7d316052134bd730702e5b09ec3e369c8c74ab0901536d1c MeetingAssistant/Workflow/MeetingWorkflowEngine.cs
decc7081e84d41bbecbae69b7e390f1ac8c80e8b24431a52dd5b2daa3c1f239a MeetingAssistant/Workflow/MeetingWorkflowEvent.cs
1e4364858518ae7e25740a9ae965ebef4f858b5f518f337d70081bef82f60bc2 MeetingAssistant/Workflow/MeetingWorkflowModels.cs
1da03e36e83edc29e50dcfa3f8d1552636f370dc1666a5150a47d936cc02a36c MeetingAssistant/Workflow/MeetingWorkflowRazorEngineFactory.cs
bfb63abd6050f2762a030aa2505c945d4e8cf9b4dd43c6b7e40aecf96e789010 MeetingAssistant/Workflow/MeetingWorkflowRuleSchema.cs
82bfcde8e3f97394acdbcf098e7dd813e62a9c96de33d5687e52235b44f49785 MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs
26b6d177484cedeeafb19013709cddd0933d7beb66a3acfbf2e6646d1eb57873 MeetingAssistant/Workflow/MeetingWorkflowTemplateRenderer.cs
a4a8cfcbadeaa466f74855043b6c714b92cfc3da2cde308259c73411e45cf4d3 MeetingAssistant/Workflow/WorkflowRulesEditorChatModels.cs
cdf09bcfe9b13a898738a72ea027d6de23c5a148dd525ce53466d04965ca24a9 MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs
6712327ad9805e555653c5a97cc8954833ce72cfa9703c7cd16abe3d452c2eb5 MeetingAssistant/Workflow/WorkflowRulesEditorChatViewModel.cs
538a1d6aabc3071db8aaa19ba1432c3905177daa6d53f82e0259d11395ee308d MeetingAssistant/Workflow/WorkflowRulesEditorInstructionBuilder.cs
adf27ccf5237abe6147f60770d3b35e2998c331c9c3ee9cb7f84a3424606fb18 MeetingAssistant/Workflow/WorkflowRulesEditorMarkdown.cs
7acd3c2deef6eddd781fcb26d362cec7ecd85600483b771a1d87281f6830676b MeetingAssistant/Workflow/WorkflowRulesEditorMarkdownLinkResolver.cs
47e84f70864c630aed94cabb4e8f0094c4340fcd7283c77b8c0d3be524ce6ef9 MeetingAssistant/Workflow/WorkflowRulesEditorMarkdownRenderer.Windows.cs
b8ea5923d3fbe9a675e6a2e1f0650508afda29e134d949884d475d94d8b66791 MeetingAssistant/Workflow/WorkflowRulesEditorSamplePlaybackQueue.cs
87a1c633c155e67dbf7ee9bb41c83223e0eb85ee27f7a7daa81740aae0338936 MeetingAssistant/Workflow/WorkflowRulesEditorScrollPolicy.cs
502e21d9e70e87279b113877ab84daea13b61617a259534557adee7299803494 MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs
ac3adb674090da96a4a91d95abfa174c8ec56b5a77a391e4c00edc3c4329f81a MeetingAssistant/Workflow/WorkflowRulesEditorWpfTheme.Windows.cs
c4c856a493a9916fff81de04a7f5fa22bca07ef2acfd4698f1827587102b6913 MeetingAssistant/Workflow/WorkflowRulesPathResolver.cs
241607e1699d025a5197119117a76a66447f46e71e3a9cd6b1b253e4aa099d09 MeetingAssistant/Workflow/WpfWorkflowRulesEditorWindowService.Windows.cs
975ad5d8cbce83db14ff1d3f2814bc7aa710231f2567d7a62076386c2ccdb16b MeetingAssistant/appsettings.Development.json
ef7923c202faa265d8820bbc3a4d50df246a9610a60d052e82f1da4e5d808dfd MeetingAssistant/appsettings.json
6f0dda573027b05dd761b625cd944ca25e510190df13d139f8d6ddc46c92cd1d scripts/wine-sdk-trust.cs
056ccdaefa4c0b6280d8d91435952a8546cebc43bda19c19e877b8c4a45c0a99 tools/VerifyPlatformTests.cs
@@ -1,69 +0,0 @@
# Ticket 02 — Windows platform acceptance evidence
Implementation evidence date: 2026-10-03; closeout update: 2026-10-06. Current status: **prepared slice accepted and local Ticket 02 closed; actual Windows verification incomplete; local commit pending prerequisite inclusion decision**. No Windows criterion is marked passed from a portable run or cross-build. Ticket 01, its comments/completion evidence and the original communication Tasks 4.2/4.4 are preserved.
The implementation observations below retain their original date and scope. The user's later acceptance explicitly closes the local ticket for this preparation without claiming original operational acceptance. Requirements verification, sequential structural reviews, cleanup and separate delivery status are recorded in [windows-platform-acceptance-completion.md](windows-platform-acceptance-completion.md).
## Ownership, routing and content
Owning Git root: `meeting-assistant`. Current and intended delivery branch: `codex/macos-support`; initial/current HEAD: `1164c26846686c1912fd1816cd06de80352e9504`, with extensive pre-existing uncommitted work. Changes were prepared and checked in an isolated source copy before transfer. Existing files were verified against the initial baseline before transfer. No commit, push, merge, deployment, live restart or ticket closure is part of this implementation request.
`openspec list --json`, `openspec status` and `openspec instructions apply` select the active `complete-macos-feature-parity` change and its `platform-parity-verification` contracts. The repository-local `openspec-apply-change` and `tdd` skills were used. No new proposal is needed: the accepted Ticket 02 and this active change already define the crop, target and backend verification scope. No production API, persistence, backend-selection or rejection semantics were changed.
The [platform input manifest](windows-platform-acceptance-content.sha256) identifies the application, native, test, fixture, build, CI and verifier inputs used by the current platform checks. Manifest SHA-256: `aae730c74a8b5f2dd7f4c4596b34459e116578b094c226caeba6c761a1a0a3dd`. The separately developed speech-fixture generator is outside the platform project inputs and has its own final fixture/source manifest under `artifacts/tests/ticket02/`. Its final file-based app setting was corrected while the project checks were running; all platform-project input hashes still match after the checks.
## Actual implementation and observed preparation checks
- Three Windows-only public-interface tests resolve the actual registered `WindowsScreenshotImageCropper` from the application composition and verify every expected pixel/dimension at origins (0,0) and (1,1), plus ten invalid-bound cases with no output file. Tests log actual host, architecture, runtime and registration. They are excluded from portable compilation.
- The existing C# artifact verifier requires those three names in matching Windows discovery/TRX with exactly one passed result each. Omitted, skipped and failed native cases cannot produce a pass. Portable artifacts reject the Windows crop class even when their runtime marker says Windows. Existing Outlook and runtime guards remain required.
- Known German/English two-source WAV files, reference manifests, a .NET 10 file-based generator and the [Windows acceptance procedure](../../../docs/windows-platform-acceptance.md) are prepared. Each fixture contains six fixed utterances alternating two known synthetic voices, with its own SHA-256, 16-kHz mono 16-bit PCM format, exact duration and frame-derived reference intervals. This supplies inputs for later real backend execution.
Artifact paths below are relative to the repository root, under ignored `artifacts/tests/ticket02/`; they are local evidence, not an external CI upload.
| Check | Actual host/target | Observed result | Evidence |
| --- | --- | --- | --- |
| Verifier TDD, missing crop slice | macOS arm64; portable synthetic artifact tests | Old verifier accepted missing native crop evidence: 3 expected failures, then 41 passes | `verifier/verifier-red-missing-crop.trx`, `verifier/verifier-green-missing-crop.trx` |
| Verifier TDD, portable contamination slice | Same host; synthetic Linux/Windows runtime markers | Old verifier accepted crop class in portable evidence: 2 expected failures, then 43 passes | `verifier/verifier-red-portable-crop.trx`, `verifier/verifier-green-portable-crop.trx` |
| Final focused verifier coverage | macOS arm64; `net10.0` | 52 passed, 0 skipped, 0 failed; synthetic checker coverage only | `verifier/verifier-final.trx` |
| Normal portable Release build | macOS 26.6.2 arm64; SDK 10.0.203; `net10.0` | 0 errors; native Swift helpers built/signed and seven desktop helper self-tests passed | `portable-build.log` |
| Full current portable discovery/execution | macOS arm64; .NET runtime 10.0.7; `net10.0` | **655 discovered/results, 653 passed, 2 reasoned Windows skips, 0 failed**; CLI checker passes | `portable-discovery.txt`, `portable/portable.trx`, `portable-execution.log`, `portable-verification.txt` |
| Normal Windows test/application cross-build | macOS arm64; SDK 10.0.203; Windows TFM; `win-x64` | 0 errors; normal resource generation, no suppression; new crop methods compiled | `windows-restore.log`, `windows-tests-build.log`, `windows-compiled-test-symbols.txt` |
| Known speech fixture preparation | macOS native speech plus .NET generator | Two checked WAV/reference inputs; **no ASR/finalizer request submitted** | `fixture-manifest-validation.txt`, `fixture-generator-verification.md`, `fixture-source-content.sha256` |
| Actual available service inventory | Installed macOS portable application, .NET 10.0.7 | Healthy/idle; actual default and english profiles select Azure, de-DE/en-US; process Azure key present, value excluded; Windows context unknown | `backend-inventory.md`, safe `backend-live-*` receipts |
Two NU1900 warnings in the portable build reflect unavailable vulnerability-feed access; restore used cached packages. Windows restore was cache-only with auditing disabled and its build has four existing NAudio deprecation warnings. Initial local test-harness attempts hit denied Swift cache/TCP access; redirected temporary native caches and scoped test execution resolved those harness limits. A Release `--no-build` attempt before its assembly existed executed no tests; the subsequent normal Release build and the single full executed suite provide the actual evidence.
The new crop cases could not run here because no compatible Windows runtime is available. Compiled method names are **compiled-symbol evidence**, not Windows discovery or assertion execution. The existing native macOS crop and common screenshot pipeline tests remain in the successful portable suite. Historical 602-pass and 639-pass portable baselines remain in their original dated records and are not reused as current totals.
## Windows acceptance matrix
There is no observed Windows process/host/architecture/runtime/profile/model context in this session. `Windows — unknown` below means those fields remain unobserved, not a claim of a provisioned host. The cross-build's x64 RID is documented above and does not populate an execution row. The inventory must expand backend rows into each actual profile/backend/model combination once a Windows process is accessible.
| Row | Actual host / target / runtime | Profile/backend/model and prerequisites | Input/result evidence | Acceptance state |
| --- | --- | --- | --- | --- |
| W01 | Windows — unknown; required Windows TFM | Compatible Windows Desktop/.NET or demonstrated Wine runtime absent locally | Composition test prepared/cross-built; no Windows execution | **open — not executed: compatible runtime unavailable** |
| W02 | Windows — unknown; same required TFM | Five Outlook cases remain mandatory in verifier | Compiled existing methods only; no actual Windows discovery/TRX | **open — not executed: compatible runtime unavailable** |
| W03 | Windows — unknown | Windows per-user environment API | Current portable result is a reasoned skip; no Windows assertion result | **open — not executed: compatible runtime unavailable** |
| W04 | Windows — unknown | Native bitmap/glyph rendering | Current portable result is a reasoned skip; no Windows assertion result | **open — not executed: compatible runtime unavailable** |
| W05 | External CI — current candidate unobserved | Exact candidate revision, explicit portable/Windows jobs, artifact access required | External observation receipt discussed below | **open — no current-content external run/artifact proof** |
| W06 | Windows — unknown; required Windows composition | Real registered Windows cropper | Known 4×3 pixels; two tests cross-built, neither executed on Windows | **open — not executed: compatible runtime unavailable** |
| W07 | Windows — unknown; required Windows composition | Existing invalid-bound contract | Ten rejection cases cross-built, no native execution | **open — not executed: compatible runtime unavailable** |
| W-ASR-Azure | Windows — unknown | Effective profiles/languages/models and actual-process key availability unknown | Known WAVs prepared; no Windows request/result | **open — inventory and compatible Windows host required** |
| W-ASR-FunASR | Windows — unknown | Effective profiles/models, service, Docker architecture/model access unknown | Known de/en inputs; a Mandarin-only model also needs a suitable known fixture | **open — inventory and compatible Windows host required** |
| W-ASR-Whisper | Windows — unknown | Effective profile/model file/runtime unknown | Known WAVs prepared; no Windows request/result | **open — inventory and compatible Windows host required** |
| W-DIAR-FunASR | Windows — unknown | Effective finalizer activation/model/service unknown | Known alternating source/time references; no final Windows result | **open — inventory and compatible Windows host required** |
| W-DIAR-Pyannote | Windows — unknown | Effective finalizer activation/model/token/Docker unknown | Known references; require actual finalizer runtime proof beyond HTTP 200/live fallback | **open — inventory and compatible Windows host required** |
| W-BACKEND-weitere | Windows — unknown | Other effective combinations unknown until actual process inventory | No omitted combination is implicitly passed or disabled | **open — Windows inventory required** |
| W-RES | Windows — unknown | Actual Resemblyzer activation/Python/encoder/model unknown | Mac Resemblyzer state or Ticket 12 is not a Windows encoder result | **open — Windows activation and compatible encoder evidence required if enabled** |
W-R00 remains the valid historical normal cross-build from Ticket 01; this session additionally compiled changed test inputs normally. W-R05/W-C07 evidence separation is maintained, but it is not a whole-ticket pass. W-B01–W-B09 remain unaccepted for Windows. No unknown Windows backend is classified disabled from the Mac configuration, and no stub/provider replacement or transported Mac answer is substituted.
## Operational boundaries and external CI
The actual available service is a portable macOS process. Its effective process/configuration was inspected read-only and credential **presence** recorded without values. Its enabled FunASR/Whisper capability flags do not mean those providers are selected by its available profiles; Resemblyzer is disabled in that Mac process only. Docker 29.4.3 serves Linux arm64, Python is 3.14.8, and no compatible Wine/Windows executable or running guest was found. These are Mac-host observations, not Windows prerequisites.
External current-candidate CI execution and artifact publication remain unverified. The candidate includes pre-existing uncommitted prerequisites; a prior remote revision cannot prove these current explicit targets or crop cases. Read-only observation findings, accessed revision and any access limitation are retained in `ci-observation.md`. No workflow dispatch or publication was performed solely to manufacture an observed result.
The final-diarization procedure requires proof that the selected finalizer actually ran. Existing Whisper/pyannote behavior can keep live segments when no final turns are returned; a missing token can therefore leave an HTTP-success response without a finalizer pass. That source-derived limitation is explicitly documented; it is not an observed Windows failure or a newly changed fallback contract.
No Windows functional failure was observed because no Windows execution occurred. Ticket 02's Windows runtime/crop/backend acceptance and external CI row remain open. Tasks 1.1, 1.2 and the Windows portion of 3.2 remain unchecked. This evidence supplements their existing owners without rewriting the closed Ticket 01 or communication history. The prepared implementation is ready for user acceptance of its bounded scope, with the concrete missing Windows host/process and current-content CI evidence recorded above.
@@ -1,30 +0,0 @@
# Ticket 02 structural review receipt — DRY
Reviewer: independent delegated agent `/root/ticket02_dry_review`; first pass in DRY → SOLID → KISS. Date: 2026-10-06. Repository: `/Users/dh/Documents/DanielsVault/_ops/meeting-assistant`.
## Identity and provenance
- Fixed Git base: `1164c26846686c1912fd1816cd06de80352e9504`, plus the recorded pre-Ticket02 dirty baseline under `/private/tmp/meeting-ticket02-closeout/baseline`.
- Review delta: `/private/tmp/meeting-ticket02-closeout/review.diff`; SHA-256 `59806cc2bfafe2713a1d26ea493dc94562ae41a240c038e1579c4d92a5c10fd2` (independently verified).
- Current 15-file manifest: `/private/tmp/meeting-ticket02-closeout/reviewed-content.sha256`; SHA-256 `b22cf4a6fafd308bdd5537844794a2c9007268ad215fb52b452fe0612de3fb3b` (independently verified). `shasum -a 256 -c` matched every listed current file.
- Assigned criteria: `/Users/dh/.codex/skills/code-review/references/review-criteria.md` DRY brief. Sources read: vault navigation index, repository AGENTS and README, platform-parity-verification specification, Ticket02, closeout requirements evidence, changed sources/procedures/evidence, neighboring Outlook verifier gate, PNG/WAV utilities, Windows crop contract and native Mac crop context.
## Properties checked
Repeated acceptance knowledge and predicates, fixture recipes versus generated references, independent test oracles, adjacent utilities and native crop contracts were compared for shared meaning and change reasons. The generator follows the C#/.NET file-based application preference and keeps its preparation slice together. Reusing CLI tools `say`/`afconvert` fits their actual native synthesis dependency.
## Findings
**DRY-01 — P3 heuristic: share the named Windows pass gate.** `tools/VerifyPlatformTests.cs:148–157` copies the existing Outlook loop at lines 138–147: find the exact test record, require one discovery and exactly one Passed record, then append the named pass line. The sole semantic difference is the group label in the diagnostic. These two groups implement the same fail-closed acceptance rule and will need the same fixes whenever that rule changes. Use one small private method taking the names and diagnostic label, invoked for Outlook and Windows crop; preserve current labels/order and public behavior. Benefit: one authoritative predicate for the required named Windows assertions. Material risk addressed: a later verifier correction being applied to one group while the other retains weaker acceptance. This is a maintenance suggestion, not a present correctness defect or a binding-rule violation; no broader framework or shared runtime abstraction is warranted.
No other actionable DRY finding. Keep literal case names in synthetic tests independent from verifier-owned case lists so an accidental removed requirement cannot silently update the oracle. Keep the new per-coordinate opaque native image distinct from `PngTestFixture` solid images, which cannot detect a shifted crop origin. The standalone fixed PCM helper and existing runtime WAV writers have different dependency/usage boundaries; moving fixture generation into the application would add coupling. The generated WAV/JSON records and their evidence hashes are intentional immutable references, not a second editable implementation.
## Result and limits
No binding DRY violation or discovered behavior defect. One bounded heuristic improvement, DRY-01, is suitable for the authorized refactoring pass. No source edits, tests, backend requests, service operations, remote operations or archive actions were performed. Only this review receipt was written. Original Windows runtime/native/backend/CI acceptance remains UNVERIFIED; this review does not supply that evidence. Any implementation repair changes the reviewed content identity and needs a focused DRY follow-up before treating this receipt as covering that repair.
## Bounded follow-up disposition — 2026-10-06
The implementing coordinator consciously defers DRY-01 at the accepted preparation closeout. The short explicit groups retain separately named diagnostics and independently tested fail-closed behavior. Extracting the predicate now would change the exact compiled/tested source identity without satisfying any currently missing Windows runtime/native/backend/CI acceptance criterion. This is a bounded maintenance tradeoff; the suggestion remains useful if this verifier predicate next changes.
Independent reassessment: deferral leaves no binding DRY violation and introduces no material unresolved current risk. The present predicates agree, their behavior has explicit synthetic coverage, and no present correctness defect was found. The remaining risk is future inconsistent maintenance, already described in DRY-01; it does not prevent closure of the accepted preparation scope. Rechecking both packet digests and all 15 manifest entries confirmed unchanged reviewed content identity (`59806cc2bfafe2713a1d26ea493dc94562ae41a240c038e1579c4d92a5c10fd2` diff; `b22cf4a6fafd308bdd5537844794a2c9007268ad215fb52b452fe0612de3fb3b` manifest). No repository content was changed. DRY pass disposition: complete with DRY-01 consciously deferred; SOLID may proceed on this same candidate.
@@ -1,32 +0,0 @@
# Ticket 02 KISS review receipt — 2026-10-06
Outcome: **clean for the accepted preparation scope**. No actionable binding violation, heuristic simplification finding, or newly discovered behavioral defect. This is an independent third sequential structural pass; the DRY/SOLID conclusions were not read or used as a desired result.
## Pinned candidate and scope
- Repository: `/Users/dh/Documents/DanielsVault/_ops/meeting-assistant`.
- Base: HEAD `1164c26846686c1912fd1816cd06de80352e9504`, plus the recorded pre-Ticket02 dirty baseline. This candidate has no identifying commit yet.
- Diff: `/private/tmp/meeting-ticket02-closeout/review.diff`; SHA-256 `59806cc2bfafe2713a1d26ea493dc94562ae41a240c038e1579c4d92a5c10fd2`.
- Reviewed-content manifest: `/private/tmp/meeting-ticket02-closeout/reviewed-content.sha256`; SHA-256 `b22cf4a6fafd308bdd5537844794a2c9007268ad215fb52b452fe0612de3fb3b`.
- Both identity hashes were checked. All 15 files in the `current/` snapshot passed the manifest check. The diff's text changes and corresponding snapshot contents were inspected; the two WAV binary additions were identified by their checked hashes and source-reference manifests.
- Adjacent crop implementation, crop interface, production registration/ASR endpoints, isolated factory, WindowsFact, test project, and macOS native crop test were read only as relevant context. Their hashes match the candidate's platform input manifest. Newer closure bookkeeping was excluded.
The requirements/verification packet expressly scopes acceptance to crop-test preparation, fail-closed verifier behavior, known fixture/generator preparation, procedures and honest evidence. Actual Windows/Wine assertions, Windows backend inventory/results and current-content external CI remain unverified. A clean structural result does not close those original operational criteria or authorize OpenSpec archive.
## Assigned standards and properties
| ID | Standard/property | Evidence and assessment |
| --- | --- | --- |
| KISS-C01 | Current smallest clear design; meaningful naming | `MeetingAssistant.Tests/WindowsScreenshotImageCropperTests.cs:13` exposes the three required named cases. Its local fixture and shared valid-crop assertion keep preparation, invocation, verification and disposal close together. Names distinguish origin, pixel preservation and rejection. The every-pixel assertions and ten invalid cases are meaningful contract coverage, without a new framework or production seam. |
| KISS-C02 | Public behavior tests; genuine platform boundaries | Crop tests use the registered public `IScreenshotImageCropper` and assert the required real implementation. `#if WINDOWS` plus `WindowsFact` reflect target and compatible-runtime requirements. The existing isolated factory suppresses background work while preserving registrations; creating its client starts that test composition. Existing macOS native crop and common pipeline tests are preserved. No early successful return substitutes for native assertions. |
| KISS-C03 | Understandable verifier logic and test scope | `tools/VerifyPlatformTests.cs:46` adds an explicit set of three required names to the existing small checker. Required passed records and portable class exclusion are direct decisions. `MeetingAssistant.Tests/PlatformTestVerificationTests.cs:146` covers missing discovery/execution, reasoned native skips, and class contamination on two reported hosts through the public verifier. Synthetic XML data stays labeled as checker fixtures. No speculative configurable evidence engine is needed. |
| KISS-C04 | Personal C#/.NET, file-app and vertical-slice preferences | `tools/CreateBackendSpeechFixtures.cs:1` is a .NET 10 file-based C# app. Its complete use case remains in one file: fixed recipes, native clips/conversion, checked mono PCM, WAV/manifest output and temporary cleanup. Two recipe/source records describe actual inputs; the small native-process and PCM helpers express real technical work. Native macOS tools are documented dependencies. There is no new separate project, unnecessary layer, or language deviation. |
| KISS-C05 | Delivered CLI purpose, entry point, invocation, dependencies and side effects | `docs/fixtures/backend-acceptance/README.md:18` documents the generator's purpose, exact entry point/command, .NET/native tools/voices, output/temp effects, existing-output refusal and exit codes, and synthesis variability. `docs/platform-test-verification.md:52` documents the existing verifier's corresponding contract and read-only artifact behavior. The helper implementations agree with these descriptions. |
| KISS-C06 | Source/evidence clarity and native-vs-synthetic separation | `docs/windows-platform-acceptance.md:17` separates cross-compilation, native crop execution and synthetic checker coverage. The fixture README and both JSON manifests consistently identify synthetic source voices and frame-derived whole-clip intervals, rather than natural-human or forced-alignment accuracy. The evidence document identifies actual macOS host/target counts, compiled-symbol-only Windows preparation, unknown Windows process settings, unavailable CI proof, and finalizer/live-fallback limits. No generated WAV or transported response is represented as native Windows backend acceptance. |
| KISS-C07 | Remaining manually checked repository rules | Relevant OpenSpec ownership is retained; `tasks.md` records preparation while leaving original runtime tasks unchecked. Ticket 01 and communication 4.2/4.4 are referenced without scope/history rewrites. Source/test ownership remains in this repository; no deployment ownership or workflow-engine behavior is changed, so the workflow reference update rule is not triggered. Procedures respect active recording/finalization and restrict credential evidence to availability. |
## Verification and findings
Compilation, actual artifact/fixture checks, TDD receipts, strict OpenSpec validation and whitespace checks are assigned to the preceding requirements verification; their recorded results were reused. No tests, live requests, remote actions, source changes or repository cleanup were performed by this reviewer. Only this receipt was written.
Binding findings: none. Heuristic suggestions: none. No material maintenance or regression risk justifies a simplification change to the pinned preparation. Remaining material uncertainty is the already declared Windows/Wine/backend/CI execution gap, outside this accepted bounded scope.
@@ -1,20 +0,0 @@
# Ticket 02 — independent SOLID receipt
Reviewer: `/root/ticket02_solid_review`; date: 2026-10-06. Second sequential structural pass, SOLID only. Review basis: repository AGENTS.md, `code-review` skill and SOLID criteria, and the actual verification in `requirements.md`. No prior reviewer findings were read to guide the result.
Repository: `/Users/dh/Documents/DanielsVault/_ops/meeting-assistant`. Fixed base: `1164c26846686c1912fd1816cd06de80352e9504` plus the recorded pre-Ticket02 dirty baseline. Candidate: the 15 fixed files in `current/`, compared with `baseline/`; no commit identifies it. Diff SHA-256: `59806cc2bfafe2713a1d26ea493dc94562ae41a240c038e1579c4d92a5c10fd2`. Content-manifest SHA-256: `b22cf4a6fafd308bdd5537844794a2c9007268ad215fb52b452fe0612de3fb3b`. Both packet hashes and all 15 current-file hashes matched during this review. Later ticket-closeout editorial bookkeeping is outside this fixed candidate.
## Assigned standards and inspected boundaries
- Public crop interface and Windows host composition: `WindowsScreenshotImageCropperTests.cs:24–79`, production `IScreenshotImageCropper`, Windows registration in `Program.cs`, `WindowsScreenshotImageCropper.Windows.cs`, `IsolatedApplicationFactory`, `TestTargetCompositionTests`, `WindowsFactAttribute` and test-project target/reference composition. Tests exercise the registered interface without replacing the crop implementation; the concrete type assertion establishes the native-backend requirement. The existing factory removes background activities while retaining this registration.
- Fixture responsibilities, dependencies and error handling: `CreateBackendSpeechFixtures.cs`, fixture README and both reference manifests, with adjacent `SpeakerCompositeWav`, `PcmWavAudioChunkReader` and `SampleWavFixtureTests` as context. The file-based helper owns one coherent generation use case; native process invocation and PCM validation have bounded functions. Standard-library code plus required native speech/conversion tools avoids importing the application or its NAudio/backend dependencies. CLI prerequisites, non-overwrite validation and native/conversion failures are explicit for the supported invocation.
- Verifier artifact interface and fail-closed integrity: `VerifyPlatformTests.cs:54–215`, its narrow diff and `PlatformTestVerificationTests`. The verification method accepts supplied discovery/XML independently of CLI file access. Existing matching-name, duplicate, outcome, host/target, skip-reason and counter checks remain intact. Added native crop evidence requires each named Passed result, and portable evidence excludes the entire native class, including a portable run on Windows.
- Honest host/backend evidence contracts: platform-verification documentation, Windows procedure, fixture metadata, active-change evidence and issue/task annotations. Source-clip timing and synthetic voices are qualified; native execution, current external CI, actual Windows process/profile configuration and real backend/finalizer results remain separate from compilation and synthetic artifact tests. No interface or evidence contract silently supplies Windows acceptance from the portable suite.
## Result
Binding-rule violations: none found in the assigned scope. Actionable SOLID heuristic findings: none. Responsibilities and dependency seams fit the finite prepared slice; adding replaceable synthesis/provider abstractions or splitting this verifier into a framework has no demonstrated maintenance benefit for the current requirements.
## Limits
This is static review of the fixed candidate and nearby relevant context, with identity checks only. No source edits, new tests, live requests, remote changes or service actions were performed. Existing requirement-verification evidence was reused: native Windows crop/runtime assertions, actual Windows backend inventory/results and current-content external CI remain unverified. The result supports the accepted bounded preparation, not full original Windows operational acceptance or OpenSpec archival.
@@ -1,295 +0,0 @@
91e76a154aa4eca8efee1319ac983eae2f5a53928422a9a76788fb01fb6aba1d .gitea/workflows/pr-push-build-and-test.yaml
6dc5d5ebc0aad0daa8b82ad30a8630d46df7a5609290427af13ff4ba971a28d0 MeetingAssistant.Tests/AsrDiagnosticEndpointTests.cs
b2f9cefefa9d0911a69cd667297894854af30b871cf041a54d5885db1aa2c682 MeetingAssistant.Tests/AsrDiagnosticTests.cs
1369e1c268dd37f213afacd04db187951d04317db01fbc0bc43b6a1e59d3056e MeetingAssistant.Tests/AudioMixingTests.cs
60d8f9b46f728210c3cdacf6fec920148b90e98f7772007d3d21b22e35f1c60f MeetingAssistant.Tests/AzureSpeechRecognitionPipelineTests.cs
66bbfbfd71b11e5f6bd0f41c960aeda1eded2ccffe74a8accd84835f1031fbfb MeetingAssistant.Tests/AzureSpeechSpeakerIdentityMatcherTests.cs
e8fb0c55cc0c325f2e286195d991739cb16dbcc25f6e363a2d2c0a7117498b96 MeetingAssistant.Tests/AzureSpeechStreamingTranscriptionProviderTests.cs
2f81593c9d3f15cfede2fdb7bd794b843553529e2e3bb7227164e4b9385e1dc0 MeetingAssistant.Tests/CalendarRecordingPromptSchedulerTests.cs
1f7890ded7455664ee5fe085fa97310057d8fce15265f7e054a709d882986fd8 MeetingAssistant.Tests/ConfiguredSpeechRecognitionPipelineFactoryTests.cs
2cd98f83495aea5e885bf302e985c353b26d9d0c833560447bf2942283954827 MeetingAssistant.Tests/DesktopCommunicationEndpointTests.cs
99ee6e56e8b4cd1807adcef6bc6b8db7dbbc9dd3c2b2cec77d7a7e05b30ca401 MeetingAssistant.Tests/DesktopUserRequestTests.cs
f07b38fb359325ccf53b61bf1ce2fbe4ea1f9cb87a8cda68919f51b8bdc55708 MeetingAssistant.Tests/DictationWordStoreTests.cs
f41a3aa87ce8ed0219484e1fb7af843cfea4fc1c118cd2fa70ca9b63d6f495bd MeetingAssistant.Tests/Fixtures/sample-16khz-mono.wav
b02da1c343e29de47c7d3755923d6e48fd12e30fbef921e60ec6ebd462b91a85 MeetingAssistant.Tests/FunAsrBackendLifecycleTests.cs
2c18e9d6ee9675552d7039924d2d577e794794ed0397e920c58abac9a6103eb3 MeetingAssistant.Tests/FunAsrStreamingTranscriptionProviderTests.cs
62962ffcb2c16bac2f5a659c2b24a9194929610376e62a90ef31482b16ebf36a MeetingAssistant.Tests/FunAsrTranscriptFinalizerTests.cs
df4b0466a3442a74810ed6ceb674df64a74e16b619df6760fde5cae47dbb35a9 MeetingAssistant.Tests/HealthEndpointTests.cs
365c30ecf0dbee5207e56a60a46c7024d94650827b789c4a7e92529b61f287f2 MeetingAssistant.Tests/HotkeyDefinitionTests.cs
e7e86ac28260fed19988e8d364cd2cef16e3e67192d2bf97e5706f89a5d1b7d0 MeetingAssistant.Tests/IsolatedApplicationFactory.cs
003c9a58a9189bb7deaaaa80d2525de07a6e8ed79f2d1ab215083a12da3a8636 MeetingAssistant.Tests/LaunchProfileOptionsProviderTests.cs
45828c997f73ba2084af43fb1c624ef52d2f484d51619eac8d8a60c8774a3075 MeetingAssistant.Tests/LiteLlmResponsesChatClientTests.cs
b4bda0eeeabb0cd7be6f381013339a32b0149bf3a7800b7e217084e49efbaed4 MeetingAssistant.Tests/LiteLlmScreenshotOcrClientTests.cs
18e6058b7214a1da6dfb1242a51bc6efc5995a9c074f2829f926f57029d5fef4 MeetingAssistant.Tests/MacOsDesktopControlManifestTests.cs
6a40760c8fb533ba921ca990ff01068f03db8685900e0bb43d4e9ef74e974e9d MeetingAssistant.Tests/MacOsFactAttribute.cs
8b3e249bf5f68891c394a40997353574503842d19d11fc886fb16599d1f078fe MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs
bb048508a730c4294930f334677832ad298fb5173b7200c754f05d251d8c58b1 MeetingAssistant.Tests/MacOsMeetingIntegrationTests.cs
463b49a3c80664fb0c7b255d506ee54f676f977dc0851bb007372536dc46c126 MeetingAssistant.Tests/MacOsUserPromptTests.cs
be316424619f07f6fd1f740fa01474c00b3a183421c6799f602eedc6f9464d99 MeetingAssistant.Tests/MacOsWorkflowRulesEditorSessionTests.cs
2489b415383c7d2a46a4b3a3b137e04b1289820c226bf05a609be858a3467cea MeetingAssistant.Tests/MeetingArtifactContentDetectorTests.cs
e49350db2ba004eba9465c798b648480375767ab329938f55ae920a95de1619d MeetingAssistant.Tests/MeetingArtifactStoreTests.cs
8474342474be8d8ca05692d6f28baca99c3fa05b69f726f954702444417da049 MeetingAssistant.Tests/MeetingAssistant.Tests.csproj
5d0a362d7b4f9305cbec53876cdaf3383d4d990a1dd8d1d8be7d4233dc4f7a91 MeetingAssistant.Tests/MeetingNoteStoreTests.cs
e9abbc44053247102f0d3a38985d3c49cf2c773207c8864d13c27a411be9a0f3 MeetingAssistant.Tests/MeetingRunArtifactCleanerTests.cs
e69be02e39882a2189538cab9df415490b8e6a2bb6ad5605e6fa6af55f403536 MeetingAssistant.Tests/MeetingScreenshotServiceTests.cs
f28bdfca97561cf1143f72b6b4c3e270ecb51fce5489827bbe058279a347e052 MeetingAssistant.Tests/MeetingSummaryArtifactResolverTests.cs
56027e5ba84aaf0d74af1b28ae70f984c7822b9d3aebf5a18e42186e6705cdb5 MeetingAssistant.Tests/MeetingSummaryFailureWriterTests.cs
6c3cc60f9e9f726b0fc100d804fe969ee631151a6ddd51148b7bff0e21bef7f7 MeetingAssistant.Tests/MeetingSummaryInstructionBuilderTests.cs
4b1b343089b94a7d9f2bdef6d59fdc0c0d33dc95dee268f230116a4af21d201f MeetingAssistant.Tests/MeetingSummaryRetryRunnerTests.cs
458136d08124f8ba01a0d03f882959446096effc11a77b3e15bd15c866b18f6b MeetingAssistant.Tests/MeetingSummaryToolTests.cs
34f02b4bb317265f7c914bf9a473446b2b43102260ada0fb1b45802c1d0ced5d MeetingAssistant.Tests/MeetingWorkflowDiagnosticEndpointTests.cs
59db6f6b8da4e7ba3d1a14b6897300f7d915fac60b5364a29ba4b8f4ae2fad02 MeetingAssistant.Tests/MeetingWorkflowEngineTests.cs
8afaa353e8988894f4a7e1165051f23e1177a7ed802f261f3338c8c4cbe837b4 MeetingAssistant.Tests/MeetingWorkflowTestRules.cs
f31361b087be1cd6402c08e3730930df6ee0ba1bafda5c3fde3a41c1b2e0e54f MeetingAssistant.Tests/MicrophoneAudioSourceTests.cs
972f26c3187f447f24285bd595a36b001c4a12c74029a3f6f23f1077a3c41545 MeetingAssistant.Tests/MicrophoneSelectionTests.cs
e8c3fe4abe0faa0c4fb6486de4af5f24cdeeeeacb98f0fd1cf5badf05ed36eda MeetingAssistant.Tests/MicrophoneSwitchMonitorTests.cs
100d98ec49a43b99dbf29e18197f9217757562edbe4ae8e770fae0460f9a06e1 MeetingAssistant.Tests/NotificationActivationArgumentsTests.cs
099d78c42e882cda0081223cedc207e540954dcc5a643540ee8302fe52e76555 MeetingAssistant.Tests/NotificationExpirationPolicyTests.cs
b1a56fa02037699610ea26e3de10679d9465c539fca4d1b0ff60b2181bad1534 MeetingAssistant.Tests/OutlookClassicMeetingMetadataProviderTests.cs
b4484d7cebfe689431527312a712f468744a3105eb2b6fc08739cd6125b473b5 MeetingAssistant.Tests/OutlookMeetingCandidateSelectorTests.cs
d07b8803684195b0b1984987b80c2f182e1d1d66d966d4932ad2df1a6760cd52 MeetingAssistant.Tests/PastProjectMeetingSummaryToolTests.cs
c4db24e8033ddd2d2b00bbc6209bd263b8bf0d53a0f8f8d365dd63e20ca0d570 MeetingAssistant.Tests/PlatformTestVerificationTests.cs
86a59a89147737ed4b68a079b1725b5f3865c2e4af6bc305066dc6484eb0f020 MeetingAssistant.Tests/PngTestFixture.cs
ee38191b4d90d93cdfc35469ee0790836025ebf2f0a6e65d433c5df42387135c MeetingAssistant.Tests/ProcessCommandRunnerTests.cs
2191cedca7083d31a2477a52587bd30c63f4fea51f7165cd65f6eedee2d0fcd8 MeetingAssistant.Tests/ProjectKnowledgeToolTests.cs
0d3494860ce044a2e73e7edba11017b910ca5c0207e2a79da8ff08bde2ff0dfe MeetingAssistant.Tests/PyannoteDiarizationWarmupHostedServiceTests.cs
90f8743cdae0d049493848990cd75728aad13024da72e6667bea0173867117e1 MeetingAssistant.Tests/PyannoteSpeakerIdentityMatchValidatorTests.cs
2c7c731df13affad5696dffcadd57cc47a348aec17dcf13feb7ebe8803f098e7 MeetingAssistant.Tests/PyannoteTranscriptFinalizerTests.cs
590d10a614db2ed5dbd4c3b392db0acdda07726aff52bd2a799de60d4f39ef48 MeetingAssistant.Tests/RecordingCoordinatorTests.cs
a2c2749df28d3c3db0cfb7de7b20b3a84886f9f91deacbf4e863f84e5bc6ac9e MeetingAssistant.Tests/ResemblyzerSpeakerIdentificationServiceTests.cs
6a5a783f609dfc10082cb66587fe8c1c71c513f12b5aa6024cc33380d4e61ead MeetingAssistant.Tests/ResemblyzerSpeakerIdentityMergeServiceTests.cs
e988d18c86c09e6e5a62e29e43766f09a66a1a95f4ef7cd1b868ce1aa1a09454 MeetingAssistant.Tests/ResemblyzerVoiceClusterMatcherTests.cs
0d46999f8270aa4b2615ef06fa6ed5653e6e9e5400188c96e57b89fe6d3e6a14 MeetingAssistant.Tests/ResemblyzerVoiceVectorOutlierPrunerTests.cs
fd6045211499ffe71abab9dfddb7fe2f89018710d9e9b6f13f2cfc827fb1a52c MeetingAssistant.Tests/ResemblyzerWarmupHostedServiceTests.cs
477c10ba0d0ef0848377fb2632773c9d486c6d302dd6b8421b416e11de7cdc7d MeetingAssistant.Tests/RollingAudioBufferTests.cs
be536394161b9ecdaaed8f956836c0c563e6778c078cf8bfae6b96eaa1511d3e MeetingAssistant.Tests/SampleWavFixtureTests.cs
0a039169a14f7a8d8f9d85b0d675e506f1f910047801a2bce2bc4d4c51c342f4 MeetingAssistant.Tests/ScreenshotOcrOptionsTests.cs
c18d4f57ab6aebdf909670b26ce877c19881b695fd9bcbfc4eb4d8254123662e MeetingAssistant.Tests/SpeakerAudioSampleCollectorTests.cs
7ad3f496aebcdd664f5d674417e05540716a27b5000f50ba60e8104fad759c5e MeetingAssistant.Tests/SpeakerIdentityMergeServiceTests.cs
8922d2e6b4fe12c98ffefb96eda7ce948e161b11f04e3b375bd5579fccfe2607 MeetingAssistant.Tests/SpeakerIdentityMergerTests.cs
018f739add5c74c902b39044a738d58167739680d6549139d581126fca2334d3 MeetingAssistant.Tests/SpeakerIdentityServiceTests.cs
4566877ffcf1b8a48fe662e472cf39e14ec8ed364d87468762c5c551c503e7bf MeetingAssistant.Tests/SpeakerRecognitionFeatureSelectionTests.cs
9562ddc5c921b497aa91ddfdc0b52a8f352e16955130998565a2aa486cd49ece MeetingAssistant.Tests/SpeakerSampleSpanSelectorTests.cs
b8b55effa2910859b25a83d98647d85db408c079115dd7cd43b3b46b66c5afff MeetingAssistant.Tests/SpeakerVoiceVectorPersistenceTests.cs
a3e7156726d6b8efc5d7f55b187f116f8c899b36b803159be492d0ef6ab8ea25 MeetingAssistant.Tests/SpeechRecognitionPipelineHostedServiceTests.cs
27e4c406a277439d7a3f0fdb69800b4cb76901b23a7660cd086c0e4fef24a475 MeetingAssistant.Tests/SummaryAgentRequestTests.cs
ac8bce45a837ff2484f8de0df94ccf4ab64b0ff97113d06e0ca2de339bcc4b8c MeetingAssistant.Tests/TaskbarIconTests.cs
0fba4d40a3986da1b1a6e300d64210584c12ae97945fb4163ef334b664ec2c43 MeetingAssistant.Tests/TestTargetCompositionTests.cs
535d71a1a76899deed76fca9bdccbc029ea5ced9f58659230faac0148de565af MeetingAssistant.Tests/TransformingAttendeeWorkflowEngine.cs
4de9e646a368dba3a9639982c27edc66e56e834d4f1c1de34c23981fc3dd3fe9 MeetingAssistant.Tests/VaultPathTests.cs
ff386af4e4e51219328b56c85d4b6681950acc1eba72d0e7073050b54e422256 MeetingAssistant.Tests/VenvResemblyzerVoiceEncoderTests.cs
72213205de4bb722c0eeb57d2ac1de9a5c2aff4ed1c09797333610c57b3cb09e MeetingAssistant.Tests/WindowsFactAttribute.cs
b2c113f67fb0f28c0692e9e49b8d153d3dbf4b5b4232630dd8336ac3fe506365 MeetingAssistant.Tests/WorkflowRulesEditorTests.cs
cbb7fd7ee6e61ba760aaef46d65915a5e9abd5fcdbd13185e4c4ea2c92e1bd23 MeetingAssistant/AgentFileToolContent.cs
516542f16ae3182ebced9526d0bf6f4759ef8611e3ae84c97e204a61cc8cdef9 MeetingAssistant/Assets/meeting-assistant.ico
fb97893a50f252eeccca9eb159797ba548e83a673f91ae1fb9a95802444083f2 MeetingAssistant/Calendar/CalendarRecordingPromptScheduler.cs
7552dd3e1a2a7eb066876f85ae574ec67d647b745da30856304d61ea05f1bc89 MeetingAssistant/Calendar/OutlookClassicCalendarMeetingProvider.Windows.cs
2b8ff020cfbfae1b7ec87ba8b272a521395473babfd9787fa2b200855ef45c67 MeetingAssistant/Calendar/TeamsMeetingMarkerDetector.cs
5d287d08b7c6df618da2cb734ae1ccf4f57a7a9d15829dc58f6d648e7b205ba2 MeetingAssistant/Calendar/WindowsMeetingStartPromptService.Windows.cs
20bad4ec197d033bb997d4d1c7ba1571f882981aa06a9bd523705f3d3b871a78 MeetingAssistant/Content/Project-AGENTS.md
8cc27e25fc569c6eba243f0ebc635ba382f429b482ef3054bc307ebf4ff024fa MeetingAssistant/GlobalUsings.cs
fa73659079a4cab632f6863c837184263889641344de4adf798b08db24bca3af MeetingAssistant/HostOperatingSystem.cs
16b7ee3440ff44a3469c0af9c83e134602c216da59c7a4dd3e5a0d1e1c4a96ce MeetingAssistant/Hotkeys/GlobalHotkeyService.cs
777bb8e179582783543701ed3ef039d3f85b08871065c6e2eabfb5a3fe804102 MeetingAssistant/Hotkeys/HotkeyDefinition.cs
5b06f289817e18f6dc7ce9c6e44e4626cbcc06c7c970af550f30cf77b7644956 MeetingAssistant/LaunchProfiles/LaunchProfileOptionsProvider.cs
bae0a2638d4815a5ce6cf08c45381dfd48d6e48c635fdb2069c7f086e16ea92d MeetingAssistant/Logging/MeetingAssistantFileLoggerProvider.cs
9b7a95a75df54f56110646d88da5271507723e164619161f6beca7d61f94fc85 MeetingAssistant/MacOs/DesktopCommunicationEndpoints.cs
e50b9c92f44da8acac7534cf7674288698c9f5d75b10a50365269b0edc6f4d5f MeetingAssistant/MacOs/MacOsAgentApprovalPromptService.cs
f8070038fcaf85ce7736f2b30e38addcaef6746620fa79959d16ba6510a2bee0 MeetingAssistant/MacOs/MacOsDesktopControlManifest.cs
23358bc155b578605dddcee43be66cd0c8dec89c3c4a6575907b373634f5f26a MeetingAssistant/MacOs/MacOsDesktopControlService.cs
07cbdc06f40ea3053fb09046f807ceca94e7e749193f8e183b224c76f00cdb54 MeetingAssistant/MacOs/MacOsMeetingInactivityPromptService.cs
2bf4a7d1c962eea1dd8797e3a158fb262a0a27b4d6971c487d0a46e29beff307 MeetingAssistant/MacOs/MacOsMeetingIntegrations.cs
06aeec6e98c72f30fcecd169af4f56bcd528dcf2efe1708cc62558e17c6dbc16 MeetingAssistant/MacOs/MacOsWorkflowRulesEditorSessions.cs
fa68b270dc16af4335b41e70a9df6fec7834190cb6060eaa49d5ab620422223e MeetingAssistant/MacOs/MacOsWorkflowRulesEditorWindowService.cs
ede2452b893d4a06e163413386ea971f0d9584a93f9f967026c51be287914dfc MeetingAssistant/MeetingAssistant.csproj
f6fbba44e2791abbb4f89ab3ebfb41c600f46838951a29ecee2ede3dd0e0e730 MeetingAssistant/MeetingAssistantOptions.cs
87141fba571b3300fa706cb0518d49a405350dee1c8be0cf2a86507838a36e90 MeetingAssistant/MeetingNotes/CalendarAppointmentMetadata.cs
25cfc6d4442516b31b82d0d05e1463772f906c64d6ce6ad9615ee4cdf02b6a66 MeetingAssistant/MeetingNotes/FrontmatterYamlDeserializer.cs
172b1baee135c62f48ae6af27a73a82726f9fc67862c2e5784ab48906ed06568 MeetingAssistant/MeetingNotes/IMeetingArtifactStore.cs
e324730f8f309678a792e533b6785e49f7611a24056f61045803b65fce6b7fa6 MeetingAssistant/MeetingNotes/IMeetingMetadataProvider.cs
909b08aaee580f71ef3fd0be2869976f0876cba2421dacbc2dc1d433c2a81b9a MeetingAssistant/MeetingNotes/IMeetingNoteOpener.cs
55024bd4f0b3373dc7f65088f4260668670b8634e9d05cb339ae1730f65e9321 MeetingAssistant/MeetingNotes/IMeetingNoteStore.cs
edc597683705c6c84e25425566b8d72560630c4467ba66a50424c0e5051d76e3 MeetingAssistant/MeetingNotes/MarkdownDocument.cs
e23ecd731ea56f1440c3cf8e50fd224995b1717772bb52ed2445588691099261 MeetingAssistant/MeetingNotes/MarkdownMeetingArtifactStore.cs
78ca508bbee9cc141dd711b5528dceb91df988ce2f826e9adcf463c822bcd270 MeetingAssistant/MeetingNotes/MarkdownMeetingNoteStore.cs
6f739f81496837191aa7a0d81201519667e82c4b7b1e898be53369416d50af7a MeetingAssistant/MeetingNotes/MeetingArtifactFileNames.cs
9f9b34b91930a2c5fee6152c98c52c5523088dd31eccc3994844e46c2d642c71 MeetingAssistant/MeetingNotes/MeetingArtifactFrontmatter.cs
ed90222ccb3dec93613484b36c0150cfa2e5219a426f9336130b0ecb49f235b3 MeetingAssistant/MeetingNotes/MeetingAttendeeNames.cs
ccc2cad4feb201453d52ffc895a3056accfe786a3d1e69cf8771b9b09e7ad4bb MeetingAssistant/MeetingNotes/MeetingMetadataDiagnosticRunner.cs
f6baeeb33d51d9921d15485d28d8416e8793cbf798e890b473b09be95f6670b8 MeetingAssistant/MeetingNotes/MeetingNote.cs
88a26351c048e517838d089b2768790567850139d4061455a91200ef99ad858d MeetingAssistant/MeetingNotes/MeetingNoteActionLinks.cs
78d41cbd7807a4ade3d2a3a4544a1c8ff2d7f7d58e514f37313e718bdbdb2e7b MeetingAssistant/MeetingNotes/MeetingNoteTemplate.cs
4ad0caf3f4735e23ba2bde8195f0a6e152117ff3735fb1ef4fd208f520a6c595 MeetingAssistant/MeetingNotes/MeetingSessionArtifacts.cs
1cf5a4e352207cf80120da1d39606bcbb48eb3abae59c534a39cf864bf420fc5 MeetingAssistant/MeetingNotes/ObsidianLink.cs
469ef250e8ee5cf238b8715e4d0b6461077d80fcb1d162530b6423c11322f53f MeetingAssistant/MeetingNotes/ObsidianMeetingNoteOpener.cs
c6633a79b443e064a84bc3b45b20898ab7b0c8632ce4ef98e7126fd31b64b391 MeetingAssistant/MeetingNotes/OutlookClassicAppointmentMetadata.Windows.cs
17ea2e0973fbd6d66e40a0fa89114d3af4d22b1826bb81f9df0f24c667e534e8 MeetingAssistant/MeetingNotes/OutlookClassicCom.Windows.cs
f1b88c3d9056cc0f48e2bd449c021a1457d1758dea4663b51c8900271e21e9c0 MeetingAssistant/MeetingNotes/OutlookClassicMeetingMetadataProvider.Windows.cs
43a11de56500fb87e75c703629506a752f4cb99d29ee9296f18feb174c9bb820 MeetingAssistant/MeetingNotes/OutlookMeetingCandidateSelector.cs
d15fdad8118db014ec7ff6f46e28fddaa7d47f07113e0b30ab110567111d81f5 MeetingAssistant/MeetingNotes/ScalarStringListYamlTypeConverter.cs
742967d499a35a866c3ef2bd46c54f55a4e8454028f51a0b056881e33acb78c1 MeetingAssistant/Native/MacOsDesktopControls/DesktopUserRequests.swift
1fa93f044a84e8a643950c598c916b13092666b0b20393afb67bfa19bc570588 MeetingAssistant/Native/MacOsDesktopControls/Info.plist
e95d7a2ef3d8ad4ffd5dbfef1459ca14aac59e762933638c4b7adcd9a7098edf MeetingAssistant/Native/MacOsDesktopControls/SelfTests.swift
d7eb25d2de1d5b505f85c8d95b69227a9b1c44e020f7bd4d82bc5e3a766fdb5f MeetingAssistant/Native/MacOsDesktopControls/main.swift
e2dc1350f37b34128ad66c01350e9601277f85048f30940eb3594aa0b46c5673 MeetingAssistant/Native/MacOsLauncher/Info.plist
516c2620f4ea1f9944ecaa65a77c28d69dcf2ec4f6a8aa5fbb8213d6fc9b7140 MeetingAssistant/Native/MacOsLauncher/main.swift
c2874a5aba87e53641498960bee5043278ee4acbee892c5ed39e140713a7255d MeetingAssistant/Native/MacOsMeetingAudioCapture/Info.plist
d8b7e06e668e1bf41204b8499653fcead82c98376681dc0fb1508683ff2d83ef MeetingAssistant/Native/MacOsMeetingAudioCapture/main.swift
84d0a8d921a56a2769025ea053e74c94a14100ccc1a52d2ef7d6a5d64e06e974 MeetingAssistant/Native/MacOsMeetingIntegrations/Info.plist
c8e3a33cfc807b98e5ad34d50fda9b082c5e04fb974b03649ab4e53b1adabc82 MeetingAssistant/Native/MacOsMeetingIntegrations/calendar-events.js
04b49f5d16451acff42fd026c1b00f0e98ebc10d04f12ad4f6279d6508cd30d6 MeetingAssistant/Native/MacOsMeetingIntegrations/main.swift
6f2c47799cff89f752ed5dd2d235642386f5df0352bbccb0565489c741e67721 MeetingAssistant/Notifications/AgentApprovalService.cs
79535bb1822ad987151ab736e10c2da82a535a8ada89793c7d6d4dd8eccbd7a2 MeetingAssistant/Notifications/DesktopUserRequestStore.cs
7318479dfabaacc63a0c8aced14b831adbbba9efbd2ee5698af5a6e304199206 MeetingAssistant/Notifications/MeetingToastExpirationPolicy.cs
0adc0c87784f945644c50496a67d8f08e9a2cf83aefea98ce23651b1fa61d215 MeetingAssistant/Notifications/WindowsAgentApprovalPromptService.Windows.cs
7c74775d7b9024b09fa2f25aed1b1574683d1f48fc259ed1af287861ab55d901 MeetingAssistant/PlatformCapabilityReport.cs
91e83b6f9ef0e488008ffaa6033db641f73d64048c04a52c7a7f6de061ee03be MeetingAssistant/Program.cs
2ccce08eab7223123b83fc7cd8f00ff727502378060437fbe2d798b6b9fd286b MeetingAssistant/Properties/AssemblyInfo.cs
f28ba8d26146ae7ad396b47c859b16162569602016da18b2c103d6f1ea011c84 MeetingAssistant/Properties/launchSettings.json
027ad2b30cd13b01dd0d51d73e444faf5b6dfd6728872fd29bb5fb7efdfe4850 MeetingAssistant/Recording/AcousticEchoCanceller.cs
934e10efd3a2cc1c3f56bfa3d4d650b2babf32036c28c5c53d65714e92e05c3e MeetingAssistant/Recording/AudioChunk.cs
a4effa6c9ef767b2d5035c15412b5ace9c320a09af67da63cce0ba814ee9f0c4 MeetingAssistant/Recording/CompositeMeetingAudioSource.cs
8d61fbffd603704caadc45994afff4bf10f53e7060969372ef6b556a9fcc338b MeetingAssistant/Recording/FileOfflineTranscriptionBacklog.cs
6270a1fd33185ada5439d9f91396e4598120b5ca1a2647b02c948682abdbb8f6 MeetingAssistant/Recording/IMeetingAudioSource.cs
a4ccc4d6255496abed6be42fe8f016d4714e7634c740f8ff744a8d151c6ecccf MeetingAssistant/Recording/IMeetingInactivityClock.cs
defc0b99ccfda6c5f620e9fd5710036e3a874ebfc86f132594f5fd09597de1c6 MeetingAssistant/Recording/IMeetingInactivityPromptService.cs
9ea689fd1a46ff402a3428c6188f6ee92d15b1e6f19fd788e5742bfe785ae765 MeetingAssistant/Recording/IMicrophoneCaptureSourceFactory.cs
ec9b627cee40776b3d8b7ee00c00d3e8512be0412ee9d1f98f1ed80d75a107b5 MeetingAssistant/Recording/IMicrophoneDeviceProvider.cs
6c65b8cc00f61c61f8b2056426c1b57f5720962a5c97a6435c4d58a11d973318 MeetingAssistant/Recording/IMicrophoneSwitchPromptService.cs
5bd543f5fecb497f31201a19597ca593dd91b5296b8a776c3b273f4a865d6463 MeetingAssistant/Recording/IRecordedAudioStore.cs
79a1814dbc9f4a6509f977da433ff8c750d3b6b5686133476774d5cccf853f9d MeetingAssistant/Recording/ITranscriptStore.cs
7ac4a953bc9d1d632a24a98822d15a6d4029b5e7ca30d16de6486c354016e8a3 MeetingAssistant/Recording/MacOsMeetingAudioSource.cs
5fcb1dc87b74b3e76be55eab1ebcc92881d05091b501f4648923b32019e506ce MeetingAssistant/Recording/MeetingArtifactContentDetector.cs
11958d15bfe9d86a76b8272813c2c6ccf66c4c8a606863ba7c1b7baa837c9e15 MeetingAssistant/Recording/MeetingRecordingCoordinator.cs
96df6ea20efd3bca7261a55694d54bcf91efdfb3c33f6119b483f73b7630162f MeetingAssistant/Recording/MeetingRunArtifactCleaner.cs
9e0ef94354d6c2f4f93db879f4fc54a665774ce8b30069d724a6e5847db49a9f MeetingAssistant/Recording/MicrophoneAudioSource.cs
0891c7a1fba548b2a6e157f9d1709cbfbf7e5b500663d06a63a42930f4b24d1a MeetingAssistant/Recording/MicrophoneDevice.cs
7e9ece8761f3df34ee5fd1dbd529eb8e0223e68159e1ef746d371a8fa2538937 MeetingAssistant/Recording/MicrophoneDeviceSelection.cs
9ff676e8d41be39be1896491fe2a0565af229584658988a33340d7840210e4c3 MeetingAssistant/Recording/MicrophoneSwitchMonitor.cs
f15b35b6587e71b32b9ae6c1f82456e9602898f742975f18c1803eabede6fd25 MeetingAssistant/Recording/NaudioCaptureSource.cs
ab83970d673cfb3686ac2095f3132efefca8c6cb1c1a8e0878a8c5c0b422a05d MeetingAssistant/Recording/NotificationActivationArguments.cs
9affafe45bdcb3f8bb99a50732f1ea4ba015bfc047add89b629dc3ed1e31be59 MeetingAssistant/Recording/OfflineTranscriptionBacklog.cs
d53258404e65af3c37eace6aeea4881faef44a8eb73af8ec0a2da2ba7145313d MeetingAssistant/Recording/OfflineTranscriptionBacklogHostedService.cs
361be18bc3d9f967647ed37455cb1ec0653c6c829f4abb58a32360b0241ae310 MeetingAssistant/Recording/OfflineTranscriptionBacklogProcessor.cs
7125e3fd8485daacfcb968740a7577fb2914b84efcf4b5101768318f33587efd MeetingAssistant/Recording/PcmWavAudioChunkReader.cs
e33796a3cc4eee1c20018ae483599552d3e4d6f705a0797094bba023e834bcd7 MeetingAssistant/Recording/PortableMeetingAudioRegistration.cs
fefda6ed3bdf3f791a0037fc16aebe2e3354caaa75b1d08403179c9aaf90e6cb MeetingAssistant/Recording/RecordingDictationWordProvider.cs
5a32e8d16093b93758ad0dd7ad1229131418db340ef909b3666e3a05f6b716bd MeetingAssistant/Recording/RecordingSpeechRecognitionPipelineOptions.cs
eaa17f0d18acac22aea02596b702c2fe7d67af2e8e5fdee56fbd838defa4fa76 MeetingAssistant/Recording/RollingAudioBuffer.cs
e70a88fc679ff0554020d06ea0ae91b2762435d30447f9d711c332c9cc686a01 MeetingAssistant/Recording/SpeakerAudioSampleCollector.cs
ec2f97e27e5d017c6d2b760633dd599b9f7f2b9fdf61d7a836d3b3da7216fc0e MeetingAssistant/Recording/TemporaryRecordedAudioStore.cs
8062822cd28410a3e02b5a8e73b461999a7bbc431ffadcbe66aebc4d0e9c43e1 MeetingAssistant/Recording/TemporaryRecordingCleanupHostedService.cs
6532e4f2e834e5205dd3a11ac8d8c952e3e681acc10fd9ebab77368363bdd99f MeetingAssistant/Recording/UnavailableMeetingAudioSource.cs
7a481ebc4a62713736a1cd2d552df6fe4df978fe4ac617cee0b6454f6f91c86e MeetingAssistant/Recording/VaultTranscriptStore.cs
94ba33e045746add4dde99f8e5f78967d43816b79408bc08fee2663ca7a3ee8b MeetingAssistant/Recording/WindowsMeetingInactivityPromptService.Windows.cs
2c44fd6f5a0308cbd743247ca4472145091f36cbf60138015e2f6091b754db95 MeetingAssistant/Recording/WindowsMicrophoneDeviceProvider.cs
8314134f5fb3e8b15e4a087fc4a9b12bb9c64602ca2c7e3932c3dae6a9557163 MeetingAssistant/Recording/WindowsMicrophoneSwitchPromptService.Windows.cs
48628fcdc67a4d320758c26ba08b4a06a168e0fe758649605625d9b6e8cc1c91 MeetingAssistant/RuntimeContentLocator.cs
eccfdc0f8807a58fd410d44d869849df10efe4254428fb11a0595f11b117d9f7 MeetingAssistant/Screenshots/ActiveWindowScreenshotCapture.Windows.cs
9db1820ea279a663ab4e6482cd0d025374b9c0eb9fc0142f612bb4a32615f8c5 MeetingAssistant/Screenshots/LiteLlmScreenshotOcrClient.cs
a979a9b5c7e56967b3bcb84b4fe56bb6c906f65f289e5344a688832a24269fba MeetingAssistant/Screenshots/MeetingScreenshotService.cs
562133a693db91480ce122bd4ecf061203a1f5bcc52765fb9acad1f5449be354 MeetingAssistant/Screenshots/UnavailableActiveWindowScreenshotCapture.cs
6b877b57d03441ad41b83e047f8cece044ca837518ab0e5b14e0f6f498c15c24 MeetingAssistant/Screenshots/WindowsScreenshotImageCropper.Windows.cs
619a53c536fc746f6de99926442053102bb0cb5a5d5400c24283cba53e5c45e7 MeetingAssistant/Speakers/AzureSpeechSpeakerIdentityDiarizationClient.cs
f448d3e2830c879d81ccb7722b026534d48d4d6c5683151fc261e9192a466be0 MeetingAssistant/Speakers/AzureSpeechSpeakerIdentityMatcher.cs
2061e8495e8f63eee07414f8a16bd9902d22f2bad3812ef772a5523edab30fff MeetingAssistant/Speakers/NoopSpeakerIdentityMatchValidator.cs
743e5ca2605e41993b7c0e9005bcdb63d69fcb11b97626bf6fd04a420d479b20 MeetingAssistant/Speakers/NoopSpeakerIdentityMatcher.cs
c753e294ba3eac5fdbb66df7aaea0df05e3f4294d3d33d03e4ab541f58fa34f2 MeetingAssistant/Speakers/PyannoteSpeakerIdentityMatchValidator.cs
d5f49795e96976a4a223d79e870ea74552f57894c3e588af0d703a96d2fd3086 MeetingAssistant/Speakers/ResemblyzerSpeakerIdentificationService.cs
f10688955119050ab552cd5a6c3c8e36d59d9fbeebd912716f7fafc918fd9f7c MeetingAssistant/Speakers/ResemblyzerSpeakerIdentityMergeService.cs
e86a69f38ad99662c88b663e74ee74066bf8328ce7872d458231904d1b49a2f2 MeetingAssistant/Speakers/ResemblyzerVoiceClusterMatcher.cs
d1a2c388f3ca7f7d136f23a97813557db09a0248e24ed522a1251fc7dca0a80a MeetingAssistant/Speakers/ResemblyzerVoiceVectorOutlierPruner.cs
2adf8fc145e58a36aaf3097e747765c1a7b8ed01b10c9b95885929c65aa6eec7 MeetingAssistant/Speakers/ResemblyzerWarmupHostedService.cs
7575b039037e69817750451e15cc8562d022bcc4606330f1e5cacc3f2b613e2c MeetingAssistant/Speakers/SpeakerCompositeWav.cs
33d99ddfa3509840a5729ec37e64448577ae48a4a9fa2cefaf4653a5b4edb91a MeetingAssistant/Speakers/SpeakerIdentificationModels.cs
57878fbc1fa6b1117d0e6e0222ac94d122dc713c6245c8e47663c556f20a6c23 MeetingAssistant/Speakers/SpeakerIdentity.cs
d3bca7930c8ca2a0237545ab6e8b87c94f5650b6a2f9d847a3e5fd33fa930fc4 MeetingAssistant/Speakers/SpeakerIdentityAttendeeCanonicalizer.cs
c8a00bafea1d8d9ad4dd04bcc6d6fdb1b269eba57ab3f59825b9a580aeedac4b MeetingAssistant/Speakers/SpeakerIdentityDatabaseInitializer.cs
945e494173147db82fd0410a50a55cb708539b2af16bb1f085cec8b55cc7742f MeetingAssistant/Speakers/SpeakerIdentityMergeService.cs
a2a559cac7bd668c9499d7287dbd11522b98ab2d460da0ab95edfc346bcc5894 MeetingAssistant/Speakers/SpeakerIdentityMerger.cs
5171e7426675eb40f6d23c418baa9cd074eaa66512d307979bca26f469a60690 MeetingAssistant/Speakers/SpeakerIdentityNaming.cs
d6d6f6fe3522d729d07938b40faeb784fd987c9bf8c4f59063b90d94c52c413b MeetingAssistant/Speakers/SpeakerIdentitySchema.cs
23b26e217fb531237f890b2c4ec0131b334b93024ad1e2584b407b5b68432328 MeetingAssistant/Speakers/SpeakerIdentityService.cs
b7792e8830e3d354a687066f10d6c35d43c3fe8f2e81aced65f20b011a3b2794 MeetingAssistant/Speakers/SpeakerIdentityTranscriptAudit.cs
c3e8478ceb77d4e17a5b755db1cb5fa697857b5ce1f20e4c5a34741e7cd0fe2c MeetingAssistant/Speakers/SpeakerOverrideArtifacts.cs
532a5c3b061811f2a7fa1cde9160fdf5e37ab6ec842da908fb3a7351f262c360 MeetingAssistant/Speakers/SpeakerSampleCollectionPolicy.cs
e4736f52ed2dfa3c699dea41d301f1149f9a4351275ba821f2219934db426c43 MeetingAssistant/Speakers/SpeakerSampleSpan.cs
259ddfa895275e8e5a689f70b287950ff9936a5da6717944863e187395833006 MeetingAssistant/Speakers/SpeakerSampleSpanSelector.cs
9530846face5f9d6ad04a69bdc5f153626ee7c7a26a3f55fe169b888074ccb74 MeetingAssistant/Speakers/SpeakerVoiceVectors.cs
97fda26c99c60ff458d64827ea7a5c970b343491469267984fd2eff1280032fa MeetingAssistant/Speakers/VenvResemblyzerVoiceEncoder.cs
b76ebbae06324ea54c568515bd520601086986cbd006791f036545f00bc6bea5 MeetingAssistant/Speakers/WavSpeakerSnippetExtractor.cs
6a24f4733facb0a4c9b6666dce543cd3fe37590923ce9c9d8a2f12c0aede8e3a MeetingAssistant/Summary/BoundMeetingProjectResolver.cs
6c09fc419330cb01765748302b1e7531822e4d3bc9b3d4a5146022e06da4dbdf MeetingAssistant/Summary/FunctionInvocationGuard.cs
a8cb023db11732272f2db18bb65e9eea2e0cbdc8b40561e49a33b04eec937b25 MeetingAssistant/Summary/IMeetingSummaryInstructionBuilder.cs
f680116de80e4b2a7beb1a6e2de93d2edf050655c281f6452132fb7b08a10e0e MeetingAssistant/Summary/IMeetingSummaryPipeline.cs
99d5d7d9c27f9df39c6422ceb78909d41a4978ff19fff6559c29549cae361658 MeetingAssistant/Summary/LiteLlmResponsesChatClient.cs
d99f3bececeb552df96b9ea689ecd39d5406eefda73f6204f1ec345eb2e41804 MeetingAssistant/Summary/LiteLlmResponsesCompactionOptions.cs
9c3ece46e2b79c5b4b54bf01b6152cb13814197e964cbbdf9c1d432183f3beaf MeetingAssistant/Summary/MeetingSummaryArtifactResolver.cs
68286f2775ed400144b4e711f34ba7a9a21cf6ebe19212780158c09dcd9d0bc7 MeetingAssistant/Summary/MeetingSummaryFailureWriter.cs
906d07160665322bbcefadf2f79b44965a7ccaf5ae8572aa2c13d55c3080463f MeetingAssistant/Summary/MeetingSummaryFrontmatterFactory.cs
7d17e0b9db47e758bab4b16de998b9285143d86dadf05f6bdc699c53a363625d MeetingAssistant/Summary/MeetingSummaryInstructionBuilder.cs
f995ac0064f4fa8f90ccc72298fa1906685f23de084b81eb1731fcb554b7fb0b MeetingAssistant/Summary/MeetingSummaryRetryRunner.cs
a526243294e82f62dbbd17ff71c6ae53bbc0adfac121841db11883bef87b5c51 MeetingAssistant/Summary/MeetingSummaryTools.cs
6a6b0a9aad5b6dd46d72dc1077e0badb37790c3ac6b0a240269d84bc4e98cfc2 MeetingAssistant/Summary/OpenAiMeetingSummaryAgentPipeline.cs
b92c985bddbd2b62865604a0d5599fe8480ebd67ad6ac15e7673f991f70ae724 MeetingAssistant/Summary/ProjectMetadata.cs
935ff2c384c6214b3134daef47c8c14ccaec37a3cd92a786ed4ba4c775457eea MeetingAssistant/Summary/SummaryAgentWriteAudit.cs
02f90f9c4460d1396b4e3c4cdf15b3a482dde1f42d0fc98eaa53f6d5025a2516 MeetingAssistant/Summary/WorkflowChangeRequestService.cs
7f15c861be19ce596a2bd70d73e91395e299d744b4ffacb5c2ac188180274bbe MeetingAssistant/Taskbar/MeetingTaskbarMenu.cs
df9d3ce85068fc925ae5502987eba1155df640a350afe6ea5fdf9cda9519a355 MeetingAssistant/Taskbar/TaskbarIconRenderer.cs
076cde3c56640e7977ab16ccbc90be8ea35f6b8eb817480df40d77d6f02bbe91 MeetingAssistant/Taskbar/UnoTaskbarIconService.Windows.cs
829801d111380b645b2c85520d4b4d0e1198149d090fd74f22f323195660dc8b MeetingAssistant/Transcription/AsrDiagnosticService.cs
4f95dd715f75ddd1ec1d9f667a37dfaa1fc98035b1cc0b8c763b2f19e464580c MeetingAssistant/Transcription/AzureSpeechRecognitionPipeline.cs
861f0102e4aa0c8e356dc1e91c84e7fa213dd153e9902284c03b33bac16796e8 MeetingAssistant/Transcription/AzureSpeechRecognitionPipelineBuilder.cs
03ca23c1c72be97fc5c3190d465bc779d871e9fed2457b5a4404b207a74ca9b5 MeetingAssistant/Transcription/AzureSpeechStreamingTranscriptionProvider.cs
6051aed0cc1ddbda5c4e26fc141f06c9d5600a289c595fa4f1946bb7072bd767 MeetingAssistant/Transcription/ClientFunAsrWebSocketConnection.cs
66f4eed765a67954dd0a235a74de364a7cd0ed0eecc966bfb2831506f311d679 MeetingAssistant/Transcription/CommandRunner.cs
4d45b7d16796a4b3555644685d25c624aa5d978ec84f0b3d1e3854518aec0b54 MeetingAssistant/Transcription/ConfiguredSpeechRecognitionPipelineFactory.cs
0ec609ee9a51324f9b6afb3d6cb704c5e08021b7e3bd7ce1fdb095df10c0eaa0 MeetingAssistant/Transcription/FunAsrDockerBackendLifecycle.cs
b1cfdc15ca110ceecaaef81f20964543125c49b0193b78c920f591db13f1c522 MeetingAssistant/Transcription/FunAsrMessageParser.cs
15191a475a02288f94193f6cecfbd2fc264bfa112928836fcc2050ddbb3c1d1b MeetingAssistant/Transcription/FunAsrSpeechRecognitionPipeline.cs
52ca4389b6d9b439f660cc9c8f4b06e3f188e06c2c178736353d8c664c8cdf59 MeetingAssistant/Transcription/FunAsrSpeechRecognitionPipelineBuilder.cs
264447ce064784edf7afd0c2ad436d18954780f082869d7ea04b7d42e5ae46b3 MeetingAssistant/Transcription/FunAsrStreamingTranscriptionProvider.cs
1bee4ac3d9c8ce73d2f9730ba29e790e8603686377e57cf427b742d17989217d MeetingAssistant/Transcription/FunAsrTranscriptFinalizer.cs
1972697d7a367ed787fd1585d426c507601ec662d59d66b794d47b11b79d6be6 MeetingAssistant/Transcription/IDictationWordStore.cs
028710669270ffb702e1832a56741564cea6b8ea3a1e91b3ac2826510648502e MeetingAssistant/Transcription/IFunAsrBackendReadinessProbe.cs
a5eba20e4752055e8f91f5dac1a1dfad42d90896b45741f6d87550d2d120baa0 MeetingAssistant/Transcription/IFunAsrWebSocketConnection.cs
b2e4f00cab55fa3a6c4e98b29f87fbf10fc3deb7a664ffd2dd2e6814d7339ec0 MeetingAssistant/Transcription/ISpeechRecognitionPipeline.cs
f2cebf0b63d3fab1ac18e14f418a541038e40969a19ed491954c09c30b8fc82a MeetingAssistant/Transcription/ISpeechRecognitionPipelineBuilder.cs
29d141c6f63d77a226cb5b0c67b2096a30f5eeacbe27c4e6e775b5c3e4a69c2f MeetingAssistant/Transcription/IStreamingTranscriptionProvider.cs
42a9147566b25db683f94763739360f11e858021ad0effc0dc2b93287d563bcc MeetingAssistant/Transcription/MarkdownDictationWordStore.cs
bcabf6007c9c942845336c1dd958852a8d6dcc383225b4bb1ea97903ef2848e8 MeetingAssistant/Transcription/PyannoteDiarizationWarmupHostedService.cs
7b33738b53c6311a40efa036fdf314e0873bfab9e15fc7ea7a3a99948fad2efd MeetingAssistant/Transcription/PyannoteTranscriptFinalizer.cs
155ac72b0d7792891f5ed1f600f972fa5cd6f650f2f740786e4838fd20a56590 MeetingAssistant/Transcription/SpeechRecognitionPipelineBuilder.cs
ecd227674a327a3db898c256c7d5e9957df2e4fdb309dd6aab980e4e8f8213b7 MeetingAssistant/Transcription/SpeechRecognitionPipelineHostedService.cs
147c4277e1f9118ad63d0eb2cc7864363f262f901088daedfe5b3dc5f3e3c919 MeetingAssistant/Transcription/StreamingSpeechRecognitionPipeline.cs
9c667c025d7140cc1d076ce375521857ccf5869d9d3ad661bf09e8b040e32ee8 MeetingAssistant/Transcription/TranscriptionSegment.cs
aa9d58bf3f970bc7575ac28da415e3988cce825b4978010b4bdc87e88fba60ce MeetingAssistant/Transcription/WhisperLocalSpeechRecognitionPipeline.cs
a41885889c9e6223263bb52db8e9fb7ce59bdf3f85b05fb5e70ae9a285a880d2 MeetingAssistant/Transcription/WhisperLocalSpeechRecognitionPipelineBuilder.cs
7d213545d4d70e905088dc3171e3a7b2586540e9400ff44e52148273dc13d713 MeetingAssistant/Transcription/WhisperLocalStreamingTranscriptionProvider.cs
114eba73bede65102c4763ffc7cbb8287666910090f4516ce4180e537536ce47 MeetingAssistant/VaultPath.cs
eb46fef7a48a0878159cdb6fda34534e9a7b908ed0bd2cd975eed64d08399bed MeetingAssistant/Workflow/FileMeetingWorkflowRulesProvider.cs
6cc629b70fe63e6a61fc46ac31af647ccae322da9c1122066e6956ecba4d69ac MeetingAssistant/Workflow/IWorkflowRulesEditorWindowService.cs
a308fcf7aa67880d7d316052134bd730702e5b09ec3e369c8c74ab0901536d1c MeetingAssistant/Workflow/MeetingWorkflowEngine.cs
decc7081e84d41bbecbae69b7e390f1ac8c80e8b24431a52dd5b2daa3c1f239a MeetingAssistant/Workflow/MeetingWorkflowEvent.cs
1e4364858518ae7e25740a9ae965ebef4f858b5f518f337d70081bef82f60bc2 MeetingAssistant/Workflow/MeetingWorkflowModels.cs
1da03e36e83edc29e50dcfa3f8d1552636f370dc1666a5150a47d936cc02a36c MeetingAssistant/Workflow/MeetingWorkflowRazorEngineFactory.cs
bfb63abd6050f2762a030aa2505c945d4e8cf9b4dd43c6b7e40aecf96e789010 MeetingAssistant/Workflow/MeetingWorkflowRuleSchema.cs
82bfcde8e3f97394acdbcf098e7dd813e62a9c96de33d5687e52235b44f49785 MeetingAssistant/Workflow/MeetingWorkflowRulesValidator.cs
26b6d177484cedeeafb19013709cddd0933d7beb66a3acfbf2e6646d1eb57873 MeetingAssistant/Workflow/MeetingWorkflowTemplateRenderer.cs
a4a8cfcbadeaa466f74855043b6c714b92cfc3da2cde308259c73411e45cf4d3 MeetingAssistant/Workflow/WorkflowRulesEditorChatModels.cs
cdf09bcfe9b13a898738a72ea027d6de23c5a148dd525ce53466d04965ca24a9 MeetingAssistant/Workflow/WorkflowRulesEditorChatPipeline.cs
6712327ad9805e555653c5a97cc8954833ce72cfa9703c7cd16abe3d452c2eb5 MeetingAssistant/Workflow/WorkflowRulesEditorChatViewModel.cs
538a1d6aabc3071db8aaa19ba1432c3905177daa6d53f82e0259d11395ee308d MeetingAssistant/Workflow/WorkflowRulesEditorInstructionBuilder.cs
adf27ccf5237abe6147f60770d3b35e2998c331c9c3ee9cb7f84a3424606fb18 MeetingAssistant/Workflow/WorkflowRulesEditorMarkdown.cs
7acd3c2deef6eddd781fcb26d362cec7ecd85600483b771a1d87281f6830676b MeetingAssistant/Workflow/WorkflowRulesEditorMarkdownLinkResolver.cs
47e84f70864c630aed94cabb4e8f0094c4340fcd7283c77b8c0d3be524ce6ef9 MeetingAssistant/Workflow/WorkflowRulesEditorMarkdownRenderer.Windows.cs
b8ea5923d3fbe9a675e6a2e1f0650508afda29e134d949884d475d94d8b66791 MeetingAssistant/Workflow/WorkflowRulesEditorSamplePlaybackQueue.cs
87a1c633c155e67dbf7ee9bb41c83223e0eb85ee27f7a7daa81740aae0338936 MeetingAssistant/Workflow/WorkflowRulesEditorScrollPolicy.cs
502e21d9e70e87279b113877ab84daea13b61617a259534557adee7299803494 MeetingAssistant/Workflow/WorkflowRulesEditorTools.cs
ac3adb674090da96a4a91d95abfa174c8ec56b5a77a391e4c00edc3c4329f81a MeetingAssistant/Workflow/WorkflowRulesEditorWpfTheme.Windows.cs
c4c856a493a9916fff81de04a7f5fa22bca07ef2acfd4698f1827587102b6913 MeetingAssistant/Workflow/WorkflowRulesPathResolver.cs
241607e1699d025a5197119117a76a66447f46e71e3a9cd6b1b253e4aa099d09 MeetingAssistant/Workflow/WpfWorkflowRulesEditorWindowService.Windows.cs
975ad5d8cbce83db14ff1d3f2814bc7aa710231f2567d7a62076386c2ccdb16b MeetingAssistant/appsettings.Development.json
ef7923c202faa265d8820bbc3a4d50df246a9610a60d052e82f1da4e5d808dfd MeetingAssistant/appsettings.json
b48d6da8fa62c0da7cc8a2da4ea036ac55836660a448ba533135abb5b8451b73 global.json
cac189a038192e1dca8e9f616fb767c92e5fdb89ebd03aaddcca791986e3f5b1 tools/VerifyPlatformTests.cs
@@ -1,57 +0,0 @@
# Ticket 01 implementation and verification evidence
Date: 2026-10-03. The user accepted the implemented scope and requested ticket closure, commit, push and cleanup. Actual Windows test execution remains unverified, so OpenSpec Task 1.1 remains open. The original `add-macos-user-communication` tasks and historical evidence are unchanged. The [completion record](completion.md) records the subsequent reviews and delivery state.
## Scope and identity
Owning Git root: the `meeting-assistant` repository. Delivery target: `fork/codex/macos-support`. Initial implementation used HEAD `1164c26846686c1912fd1816cd06de80352e9504` plus the existing uncommitted communication implementation. Closeout integrates the five remote commits through `1b19b08`, preserving their Wine 11, SDK trust and core tray package fixes. An isolated checkout prepares the combined candidate; delivery status belongs to the completion record. No deployment or live-service restart is part of this task.
The existing `complete-macos-feature-parity` change owns PARITY-TEST-01, PARITY-TEST-02 and PARITY-EVIDENCE-01. OpenSpec apply routing selected this change; no new change or skipped OpenSpec workflow was needed.
The [content manifest](windows-test-assurance-content.sha256) identifies 295 application, native-helper, test, fixture, build, CI and verifier files. Current manifest SHA-256: `19d7f4afae918d1fc3df24bb400e27ade2546d00d94d071467191dc3525bed00`. It excludes build outputs and personal local configuration. Documentation/task annotations are outside this code-content identity. The initial manifest was `05f078215a6bdabcc763a131f71544272d1b7bcd72d6b9a0d33295893505ab92`; only the integrated Windows package and CI inputs changed within those entries.
Implemented:
- Both explicit test TFMs select the matching application TFM; Windows uses x64/WPF with the usual IO/HTTP test imports retained.
- A passing public composition test checks target/platform assembly metadata, health/capability endpoints and actual platform service registrations; its output identifies target and runtime host.
- Both false-green early returns are replaced by Windows facts with capability-specific skip reasons. Native macOS facts require the portable macOS target. Portable-only endpoint assertions are excluded from Windows compilation.
- HTTP fixtures retain production registrations while isolating application background services. Tests explicitly re-enable only the cleanup or temporary-database activity they exercise.
- CI builds, discovers, executes and retains portable/Windows artifacts separately. The Windows application step uses normal resource generation. The C# file-based verifier validates named discovery against result records, the five exact Outlook cases, real skips/passes and current counters.
The entry points, helper dependencies/effects and runner commands are documented in `docs/platform-test-verification.md`.
## Executed results
Evidence paths below are relative to the repository root, under ignored `artifacts/tests/ticket01/`. These local artifacts retain actual test/build logs and TRX results; they are not a remote CI run.
| Surface | Host and target | Actual result | Evidence |
| --- | --- | --- | --- |
| Complete portable suite | macOS 26.6, arm64; SDK 10.0.203/runtime 10.0.7; `net10.0` | Initial and final candidate runs each have 641 discovered/results, **639 passed, 2 skipped, 0 failed** | Initial `portable/`; current `closeout/portable/`, `closeout/portable-full-suite.log`, `closeout/focused-proof.txt` |
| Actual native helper checks | Same macOS portable build | All seven existing desktop-helper self-test groups pass; native packaging/pixel tests also execute in the portable suite | `portable/full-suite.log`, `portable/portable.trx` |
| Focused HTTP composition | Same macOS portable target | 21 passed, 0 failed/skipped | `isolated-http-focused/isolated-http-focused.trx` |
| Linux portable CI branch | Cached Ubuntu 24.04/linux-x64 container under amd64 emulation; SDK 10.0.401/runtime 10.0.12; `TZ=Europe/Berlin`; `net10.0` | 22 cases: **15 passed, 7 reasoned skips, 0 failed**; actual Linux Noop composition and HTTP fixtures pass | `linux-focused/receipt.md`, `linux-focused/linux-focused.trx`, `linux-focused/source-hashes.sha256` |
| Initial Windows compiler check | Initial pre-closeout package graph, macOS cross-compilation; Windows TFM | Application and test assembly compiled, **PRI disabled**; historical limited result | `windows-limited-compile-final.log`, `windows-compiled-outlook-symbols.txt` |
| Initial Windows application/resource build | Initial graph with `H.NotifyIcon.Uno.WinUI`, macOS host | Historical failure at `MakePri.exe`; superseded by the remote package correction below | `windows-full-build.log` |
| Current normal Windows application and test build | Integrated `H.NotifyIcon` core package graph, macOS host; Windows TFM | Both normal builds pass, **0 errors**, without disabling resource generation. Current graph has no WindowsAppSDK/MSIX/PRI dependency | `closeout/windows-application-build.log`, `closeout/windows-application-build.binlog`, `closeout/windows-tests-build.log`, `closeout/windows-application-assets.json` |
| Windows runtime discovery/execution | macOS arm64 lacks a compatible Windows/x64 testhost | **No Windows cases discovered or executed**. Discovery prints a test-platform error despite exit code 0; empty discovery is not successful evidence | `windows-discovery-unavailable.log` |
The Linux subset used cache-only restore and no network or downloads. Its first cache miss and successful retry are retained. The tested HTTP/platform sources match the final content; independent verifier-fixture edits were outside that subset. Its temporary containers were removed. macOS Windows-target restore emitted NU1900 for an unavailable NuGet vulnerability feed; compilation succeeded. The final portable run used cached dependencies and disabled NuGet auditing after stopping a stalled restore; this affects feed access rather than test behavior. Its before/after input manifest matched.
The current two portable skips are:
- `ResolveKeyFallsBackToUserEnvironment`: requires Windows per-user environment variables.
- `TaskbarIconGlyphsAreVisuallyCentered`: requires Windows System.Drawing bitmap rendering.
The earlier focused baseline falsely reported both as passed on macOS (`red-skips/before-skips.trx`). The corrected focused run records one passing composition guard and two reasoned skips (`platform-focused/platform-guard-focused.trx`). Six verifier red/green slices finish with **38 passing synthetic fixture cases** (`helper-green-06.trx`); those fixtures validate the checker, not Windows execution. The final CLI check ran against the actual 641-case portable discovery/TRX and passed.
Strict validation passes for `complete-macos-feature-parity` and the unchanged `add-macos-user-communication`; workflow YAML parsing and all embedded Bash syntax checks pass. Evidence: `openspec-validation.log`, `communication-validation.log`, `workflow-syntax.log`.
## Remaining acceptance boundaries
The real Windows target compiles the existing Outlook test class and methods. All five named cases are required by the CI verifier, but their discovery and executed assertions still need a compatible Windows/Wine runner. Compiled symbols and synthetic verifier fixtures do not satisfy that requirement. The two Windows-only checks likewise need actual assertion execution there.
The current normal Windows builds pass on the macOS cross-compilation host. The remote `H.NotifyIcon` core package correction removes the accidental WinUI/WindowsAppSDK/MSIX dependency, so this graph no longer requires the previously failing PRI tool. No resource-suppression property was used for the current build. Actual Windows/Wine execution must still demonstrate the runtime and CI path. No local Wine installation or reachable Windows test runtime was available. Historical Wine results from a different commit or portable TFM were not substituted for current results. CI execution and artifact upload remain unverified.
The historical **602 passed / 0 skipped** portable baseline remains dated 2026-10-03 in `add-macos-user-communication/implementation-evidence.md`. Current totals above come from their own TRX records and do not replace that history. This evidence supplements the old communication Task 4.2 without editing or closing it. Task 4.4, real notification/menu interaction, Windows/native crop acceptance, screenshot focus and activated-backend operations remain with their respective owners/tickets.
A portable green run, native helper self-tests and normal Windows cross-compilation establish these recorded checks. They do not establish complete Windows runtime or operational platform acceptance. The user-requested ticket closure records acceptance of that bounded result; it does not mark the outstanding OpenSpec criteria passed.
-217
View File
@@ -1,217 +0,0 @@
using System.Xml.Linq;
#if !PLATFORM_TEST_VERIFIER_LIBRARY
if (args.Length != 3)
{
Console.Error.WriteLine("Usage: dotnet run tools/VerifyPlatformTests.cs -- <portable|windows> <discovery.txt> <results.trx>");
return 2;
}
try
{
var verification = PlatformTestVerification.Verify(args[0], File.ReadAllText(args[1]), XDocument.Load(args[2]));
foreach (var line in verification.Lines)
{
Console.WriteLine(line);
}
return 0;
}
catch (Exception exception) when (exception is IOException or InvalidDataException or System.Xml.XmlException or ArgumentException)
{
Console.Error.WriteLine($"Platform test assurance failed: {exception.Message}");
return 1;
}
#endif
public sealed record PlatformTestVerificationResult(int Total, int Passed, int Skipped, IReadOnlyList<string> Lines);
public static class PlatformTestVerification
{
private static readonly XNamespace Trx = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
private const string CompositionGuard = "MeetingAssistant.Tests.TestTargetCompositionTests.TestAndApplicationTargetsUseMatchingComposition";
private static readonly string[] WindowsRuntimeTests =
[
"MeetingAssistant.Tests.AzureSpeechStreamingTranscriptionProviderTests.ResolveKeyFallsBackToUserEnvironment",
"MeetingAssistant.Tests.TaskbarIconTests.TaskbarIconGlyphsAreVisuallyCentered"
];
private const string OutlookClass = "MeetingAssistant.Tests.OutlookClassicMeetingMetadataProviderTests.";
private static readonly string[] OutlookCases =
[
OutlookClass + "ExtractAgendaStopsBeforeTeamsJoinInformation",
OutlookClass + "NormalizeAttendeesDeduplicatesOrganizerAndRecipientEmail",
OutlookClass + "SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: \"Canceled: Project sync\")",
OutlookClass + "SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: \"Cancelled: Project sync\")",
OutlookClass + "SubjectIndicatesCancellationRecognizesOutlookCanceledPrefixes(subject: \"Abgesagt: Project sync\")"
];
private const string WindowsCropClass = "MeetingAssistant.Tests.WindowsScreenshotImageCropperTests.";
private static readonly string[] WindowsCropCases =
[
WindowsCropClass + "WindowsCropUsesOcrPixelCoordinates",
WindowsCropClass + "WindowsCropPreservesExpectedPixelsAtNonzeroOrigin",
WindowsCropClass + "WindowsCropRejectsInvalidBounds"
];
public static PlatformTestVerificationResult Verify(string target, string discovery, XDocument trx)
{
if (target is not ("portable" or "windows"))
{
throw new ArgumentException("Target must be portable or windows.", nameof(target));
}
var records = trx.Root?.Element(Trx + "Results")?.Elements(Trx + "UnitTestResult").ToArray() ?? [];
if (records.Length == 0)
{
throw new InvalidDataException("TRX has no execution records.");
}
var guards = records.Where(record => (string?)record.Attribute("testName") == CompositionGuard).ToArray();
if (guards.Length != 1 || (string?)guards[0].Attribute("outcome") != "Passed")
{
throw new InvalidDataException("The composition guard must have exactly one Passed result.");
}
var markers = guards[0].Descendants().Where(element => element.Name.LocalName == "StdOut")
.SelectMany(element => element.Value.Split('\n', StringSplitOptions.TrimEntries))
.Where(line => line.StartsWith("platform-assurance:", StringComparison.Ordinal)).ToArray();
var prefix = $"platform-assurance: target={target}; host=";
if (markers.Length != 1 || !markers[0].StartsWith(prefix, StringComparison.Ordinal)
|| markers[0][prefix.Length..] is not ("windows" or "macos" or "linux" or "other"))
{
throw new InvalidDataException("The composition guard must report one matching target and runtime host marker.");
}
var host = markers[0][prefix.Length..];
if (target == "windows" && host != "windows")
{
throw new InvalidDataException("The Windows target requires a Windows runtime reported by the passed composition guard.");
}
var lines = new List<string>
{
$"Target: {target}; runtime host reported by passed composition guard: {host}.",
$"Passed: {CompositionGuard}"
};
var discovered = discovery.Split('\n', StringSplitOptions.TrimEntries)
.Where(line => line.StartsWith("MeetingAssistant.Tests.", StringComparison.Ordinal)).ToArray();
var names = records.Select(record => (string?)record.Attribute("testName") ?? "").ToArray();
if (discovered.Length == 0 || names.Any(string.IsNullOrWhiteSpace))
{
throw new InvalidDataException("Discovery and execution records must contain named tests.");
}
foreach (var set in new[] { discovered, names })
{
var duplicate = set.GroupBy(name => name, StringComparer.Ordinal).FirstOrDefault(group => group.Count() != 1);
if (duplicate is not null)
{
throw new InvalidDataException($"Duplicate discovery or execution case: {duplicate.Key}");
}
}
var missing = discovered.Except(names, StringComparer.Ordinal).FirstOrDefault()
?? names.Except(discovered, StringComparer.Ordinal).FirstOrDefault();
if (missing is not null)
{
throw new InvalidDataException($"Discovery and execution cases differ: {missing}");
}
foreach (var record in records)
{
if ((string?)record.Attribute("outcome") is not ("Passed" or "NotExecuted"))
{
throw new InvalidDataException($"Case did not pass or skip: {(string?)record.Attribute("testName")} ({(string?)record.Attribute("outcome")}).");
}
if ((string?)record.Attribute("outcome") == "NotExecuted" && string.IsNullOrWhiteSpace(SkipReason(record)))
{
throw new InvalidDataException($"Skipped case has no reason: {(string?)record.Attribute("testName")}");
}
}
if (target == "portable")
{
if (discovered.Any(name => name.StartsWith(OutlookClass, StringComparison.Ordinal))
|| records.Any(record => ((string?)record.Attribute("testName"))?.StartsWith(OutlookClass, StringComparison.Ordinal) == true))
{
throw new InvalidDataException("The portable target must exclude the Windows-only Outlook cases.");
}
lines.Add("Outlook cases: excluded from portable target (5 Windows-only cases).");
if (discovered.Any(name => name.StartsWith(WindowsCropClass, StringComparison.Ordinal))
|| records.Any(record => ((string?)record.Attribute("testName"))?.StartsWith(WindowsCropClass, StringComparison.Ordinal) == true))
{
throw new InvalidDataException("The portable target must exclude the native Windows crop cases.");
}
lines.Add("Windows crop cases: excluded from portable target (3 Windows-only native cases).");
}
else
{
foreach (var name in OutlookCases)
{
var matches = records.Where(record => (string?)record.Attribute("testName") == name).ToArray();
if (discovered.Count(discoveredName => discoveredName == name) != 1
|| matches.Length != 1 || (string?)matches[0].Attribute("outcome") != "Passed")
{
throw new InvalidDataException($"Outlook case must be discovered once and have exactly one Passed result: {name}");
}
lines.Add($"Passed: {name}");
}
foreach (var name in WindowsCropCases)
{
var matches = records.Where(record => (string?)record.Attribute("testName") == name).ToArray();
if (discovered.Count(discoveredName => discoveredName == name) != 1
|| matches.Length != 1 || (string?)matches[0].Attribute("outcome") != "Passed")
{
throw new InvalidDataException($"Windows crop case must be discovered once and have exactly one Passed result: {name}");
}
lines.Add($"Passed: {name}");
}
}
foreach (var name in WindowsRuntimeTests)
{
var matches = records.Where(record => (string?)record.Attribute("testName") == name).ToArray();
var expectedOutcome = host == "windows" ? "Passed" : "NotExecuted";
if (matches.Length != 1 || (string?)matches[0].Attribute("outcome") != expectedOutcome)
{
throw new InvalidDataException($"{name} must have exactly one {expectedOutcome} result on runtime host {host}.");
}
if (expectedOutcome == "NotExecuted" && !SkipReason(matches[0]).Contains("Windows", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException($"{name} must report its incompatible Windows capability as the skip reason.");
}
if (expectedOutcome == "Passed")
{
lines.Add($"Passed: {name}");
}
}
foreach (var record in records.Where(record => (string?)record.Attribute("outcome") == "NotExecuted"))
{
lines.Add($"Skipped: {(string?)record.Attribute("testName")} — {SkipReason(record)}");
}
var passed = records.Count(record => (string?)record.Attribute("outcome") == "Passed");
var skipped = records.Count(record => (string?)record.Attribute("outcome") == "NotExecuted");
var summary = trx.Root?.Element(Trx + "ResultSummary");
var counters = summary?.Element(Trx + "Counters");
if ((string?)summary?.Attribute("outcome") != "Completed" || counters is null)
{
throw new InvalidDataException("TRX summary must report a completed run with counters.");
}
CheckCounter(counters, "total", records.Length);
CheckCounter(counters, "executed", passed);
CheckCounter(counters, "passed", passed);
CheckCounter(counters, "failed", 0);
foreach (var counter in new[] { "error", "timeout", "aborted", "inconclusive", "passedButRunAborted", "notRunnable", "disconnected", "inProgress", "pending" })
{
if (counters.Attribute(counter) is not null)
{
CheckCounter(counters, counter, 0);
}
}
// xUnit VSTest emits notExecuted=0 even for NotExecuted result records.
// Count actual skip records rather than trusting this adapter summary field.
lines.Add($"Totals: {discovered.Length} discovered, {records.Length} results, {passed} passed, {skipped} skipped, {records.Length - passed - skipped} failed.");
lines.Add("Test artifacts only; full Windows resource/PRI builds and native operational acceptance require separate evidence.");
return new(records.Length, passed, skipped, lines);
}
private static string SkipReason(XElement result) => string.Join(" ", result.Descendants()
.Where(element => element.Name.LocalName is "Message" or "StdOut")
.Select(element => element.Value.Trim()).Where(value => value.Length > 0));
private static void CheckCounter(XElement counters, string name, int expected)
{
if (!int.TryParse((string?)counters.Attribute(name), out var actual) || actual != expected)
{
throw new InvalidDataException($"TRX summary counter {name} must match the execution records ({expected}).");
}
}
}
File diff suppressed because it is too large. Load diff
-602
View File
@@ -1,602 +0,0 @@
#:property PublishAot=false
using System.Diagnostics;
using System.Formats.Tar;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
// Installed-guest CI slice. --validate never invokes macOS, dotnet build/test, or a VM.
return await NativeGuest.Execute(args);
static class NativeGuest
{
const string SdkVersion = "10.0.401";
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
const int FixtureTests = 12;
const string CompositionGuard = "MeetingAssistant.Tests.TestTargetCompositionTests.TestAndApplicationTargetsUseMatchingComposition";
const int MaximumLogBytes = 8 * 1024 * 1024;
static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
static readonly string[] RequiredNativeTests =
[
"MeetingAssistant.Tests.MacOsMeetingAudioSourceTests.NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.MacOsCapabilityEndpointReportsEnabledRealProviders",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperCropsPngUsingOcrPixelCoordinates",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.CalendarClientFallsBackToCalendarAutomationWhenEventKitIsDenied"
];
static readonly string[] NativeNames =
[
"MeetingAssistantAudioCapture.app/Contents/MacOS/macos-meeting-audio-capture",
"MeetingAssistantDesktopControls.app/Contents/MacOS/macos-desktop-controls", "macos-meeting-integrations", "macos-meeting-assistant-launcher"
];
static readonly string[] WindowsOnlyTests =
[
"MeetingAssistant.Tests.AzureSpeechStreamingTranscriptionProviderTests.ResolveKeyFallsBackToUserEnvironment",
"MeetingAssistant.Tests.TaskbarIconTests.TaskbarIconGlyphsAreVisuallyCentered"
];
public static async Task<int> Execute(string[] args)
{
if (args.Length == 0 || args.SequenceEqual(["--help"]))
{
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeGuest.cs -- --validate [--archive <source.tar> --source-commit <SHA>] | --run --state /Volumes/installstate --work /private/var/tmp/meeting-assistant-native-<runToken>");
return 0;
}
try
{
if (args.SequenceEqual(["--validate"]) || args.Length == 5 && args[0] == "--validate" && args[1] == "--archive" && args[3] == "--source-commit")
{
ValidateContracts();
if (args.Length == 5)
{
if (!Hex(args[4], 40)) throw new ArgumentException("Source commit must be the full lowercase Git SHA.");
using var archive = File.OpenRead(Path.GetFullPath(args[2]));
ValidateArchive(archive, args[4]);
}
Console.WriteLine("Guest payload, safe Git tar, fresh TRX and native receipt contracts passed; no native execution occurred.");
return 0;
}
if (args.Length != 5 || args[0] != "--run" || args[1] != "--state" || args[3] != "--work")
throw new ArgumentException("Choose --validate or --run --state <mounted9pdir> --work <ownedAPFSdir>.");
return await Run(Path.GetFullPath(args[2]), Path.GetFullPath(args[4]));
}
catch (Exception exception)
{
Console.Error.WriteLine(exception.Message);
return 1;
}
}
static async Task<int> Run(string state, string work)
{
if (!OperatingSystem.IsMacOS() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
throw new InvalidOperationException("--run is restricted to the installed macOS x86_64 guest.");
RequireNoLinks(state);
RequireNoLinks(work);
var manifestPath = Path.Combine(state, "payload.json");
RequireNoLinks(manifestPath);
var payload = ReadPayload(File.ReadAllText(manifestPath));
if (work != "/private/var/tmp/meeting-assistant-native-" + payload.RunToken || !Directory.Exists(work))
throw new InvalidOperationException("Guest work directory does not match this run's owned APFS location.");
var owner = Path.Combine(work, "run.owner");
RequireNoLinks(owner);
if (File.ReadAllText(owner).TrimEnd('\r', '\n') != payload.RunToken)
throw new InvalidOperationException("Guest work directory has a different run owner.");
var started = DateTimeOffset.UtcNow;
var summary = new TestSummary(0, 0, 0, 0, 0, []);
var native = new List<NativeArtifact>();
var osVersion = "";
var architecture = "";
var actualSdk = "";
var trxSha256 = "";
var discoverySha256 = "";
var expectedTests = 0;
var audioCodeSignExit = -1;
var desktopCodeSignExit = -1;
var success = false;
var reason = "";
var logs = Path.Combine(state, "guest-logs");
var results = Path.Combine(state, "test-results");
var source = Path.Combine(work, "source");
var dotnet = Path.Combine(work, "dotnet", "dotnet");
// Guest checks/restore/build/tests: 25 minutes within the host's 172-minute total.
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(25));
using var signal = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); });
ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); };
Console.CancelKeyPress += cancelHandler;
void Phase(string phase, string stage) => Save(Path.Combine(state, "guest-phase.json"), new { token = payload.RunToken, phase, stage, updatedUtc = DateTimeOffset.UtcNow });
async Task<CommandResult> Cmd(string executable, string[] arguments, string label, bool requireSuccess = true) =>
await Command(executable, arguments, source, work, logs, label, deadline.Token, requireSuccess);
try
{
RequireAbsent(Path.Combine(state, "full-result.json"));
RequireAbsent(logs);
RequireAbsent(results);
RequireAbsent(source);
Directory.CreateDirectory(logs);
Directory.CreateDirectory(results);
foreach (var directory in new[] { "home", "packages", "tmp" })
{
RequireNoLinks(Path.Combine(work, directory));
Directory.CreateDirectory(Path.Combine(work, directory));
}
Phase("tests-running", "installed-guest-checks");
osVersion = (await Cmd("/usr/bin/sw_vers", ["-productVersion"], "os-version")).Output.Trim();
architecture = (await Cmd("/usr/bin/uname", ["-m"], "architecture")).Output.Trim();
var uid = (await Cmd("/usr/bin/id", ["-u"], "uid")).Output.Trim();
RequirePlatform(osVersion, architecture, uid);
foreach (var volume in new[] { ("/", "system-volume"), (work, "work-volume") })
RequireApfs((await Cmd("/usr/sbin/diskutil", ["info", "-plist", volume.Item1], volume.Item2)).Output);
await Cmd("/usr/bin/xcode-select", ["-p"], "clt-location");
await Cmd("/usr/sbin/pkgutil", ["--pkg-info", "com.apple.pkg.CLTools_Executables"], "clt-package");
await Cmd("/usr/bin/xcrun", ["swiftc", "--version"], "swift-version");
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "apple-sdk-version");
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "apple-sdk-path");
RequireNoLinks(dotnet);
actualSdk = (await Cmd(dotnet, ["--version"], "sdk-version")).Output.Trim();
var sdkInfo = (await Cmd(dotnet, ["--info"], "sdk-info")).Output;
if (actualSdk != SdkVersion || !Regex.IsMatch(sdkInfo, @"(?m)^\s*Architecture:\s*x64\s*$"))
throw new InvalidOperationException("Guest .NET SDK is not the pinned 10.0.401/x64 toolchain.");
Phase("tests-running", "payload-verification");
var archive = Path.Combine(state, "source.tar");
var sdkArchive = Path.Combine(state, "sdk.tar.gz");
RequireNoLinks(archive);
RequireNoLinks(sdkArchive);
if (await HashFile(archive, false, deadline.Token) != payload.ArchiveSha256 || await HashFile(sdkArchive, true, deadline.Token) != payload.SdkSha512)
throw new InvalidOperationException("Guest payload hash does not match the pinned manifest.");
using (var stream = File.OpenRead(archive)) ValidateArchive(stream, payload.SourceCommit);
// All members were checked before native tar can write anything; the destination is new.
Directory.CreateDirectory(source);
await Cmd("/usr/bin/tar", ["-xf", archive, "-C", source], "source-extraction");
var project = Path.Combine(source, "MeetingAssistant.Tests", "MeetingAssistant.Tests.csproj");
if (!File.Exists(project)) throw new InvalidOperationException("Source archive lacks the test project.");
var nativeRoot = Path.Combine(source, "MeetingAssistant", "bin", "Release", "net10.0", "Native");
RequireAbsent(nativeRoot);
Phase("tests-running", "restore");
await Cmd(dotnet, ["restore", project, "-p:EnableWindowsTargeting=true", "-p:TargetFramework=net10.0"], "restore");
Phase("tests-running", "build");
var buildStarted = DateTimeOffset.UtcNow;
await Cmd(dotnet, ["build", project, "--no-restore", "-f", "net10.0", "-c", "Release", "-p:EnableWindowsTargeting=true"], "build");
Phase("tests-running", "native-artifacts");
foreach (var name in NativeNames)
{
var path = Path.Combine(nativeRoot, name);
RequireNoLinks(path);
RequireFreshFile(path, buildStarted);
var fileOutput = (await Cmd("/usr/bin/file", ["-b", path], "native-file-" + native.Count)).Output;
var arch = (await Cmd("/usr/bin/xcrun", ["lipo", "-archs", path], "native-architecture-" + native.Count)).Output.Trim();
RequireNativeArtifact(fileOutput, arch);
native.Add(new(name, await HashFile(path, false, deadline.Token), arch));
}
var signing = await Cmd("/usr/bin/codesign", ["--verify", "--deep", "--strict", Path.Combine(nativeRoot, "MeetingAssistantAudioCapture.app")], "audio-code-sign", false);
audioCodeSignExit = signing.ExitCode;
if (audioCodeSignExit != 0) throw new InvalidOperationException("Fresh audio app did not pass strict code-signature verification.");
desktopCodeSignExit = (await Cmd("/usr/bin/codesign", ["--verify", "--deep", "--strict", Path.Combine(nativeRoot, "MeetingAssistantDesktopControls.app")], "desktop-code-sign", false)).ExitCode;
if (desktopCodeSignExit != 0) throw new InvalidOperationException("Fresh desktop app did not pass strict code-signature verification.");
Phase("tests-running", "discovery");
var discovery = (await Cmd(dotnet, ["test", project, "--no-build", "--no-restore", "-f", "net10.0", "-c", "Release", "-p:EnableWindowsTargeting=true", "--list-tests", "--nologo"], "discovery")).Output;
expectedTests = DiscoveredTests(discovery).Length;
var discoveryPath = Path.Combine(results, "discovery.txt");
File.WriteAllText(discoveryPath, discovery, new UTF8Encoding(false));
discoverySha256 = await HashFile(discoveryPath, false, deadline.Token);
Phase("tests-running", "test");
var testStarted = DateTimeOffset.UtcNow;
await Cmd(dotnet, ["test", project, "--no-build", "--no-restore", "-f", "net10.0", "-c", "Release", "-p:EnableWindowsTargeting=true", "--logger", "trx;LogFileName=native.trx", "--results-directory", results], "test");
var trxPath = Path.Combine(results, "native.trx");
RequireNoLinks(trxPath);
RequireFreshFile(trxPath, testStarted, 16 * 1024 * 1024);
var trxBytes = File.ReadAllBytes(trxPath);
var verifier = Path.Combine(source, "tools", "VerifyPlatformTests.cs");
RequireNoLinks(verifier);
await Cmd(dotnet, ["run", "--file", verifier, "--", "portable", discoveryPath, trxPath], "platform-test-verification");
summary = ValidateTrx(trxBytes, discovery, testStarted);
trxSha256 = Convert.ToHexStringLower(SHA256.HashData(trxBytes));
success = true;
}
catch (Exception exception)
{
reason = exception is OperationCanceledException ? "The explicit 25-minute guest deadline or cancellation was reached." : exception.Message;
if (reason.Length > 4096) reason = reason[..4096];
Console.Error.WriteLine(reason);
}
finally
{
Console.CancelKeyPress -= cancelHandler;
var result = new FullResult(payload.RunToken, success, payload.SourceCommit, payload.ArchiveSha256, osVersion, architecture, actualSdk, expectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, native.ToArray(), audioCodeSignExit, desktopCodeSignExit, trxSha256, discoverySha256, reason, started, DateTimeOffset.UtcNow);
if (success)
{
try { ValidateResult(result, payload); }
catch (InvalidOperationException exception)
{
success = false;
result = result with { Success = false, Reason = exception.Message };
}
}
Save(Path.Combine(state, "full-result.json"), result);
Phase(success ? "tests-passed" : "tests-failed", "complete");
}
Console.WriteLine(success ? $"Native macOS guest: {summary.Total} discovered/results, {summary.Passed} passed, exactly two reasoned Windows skips; all five native tests passed and both apps signed." : "Native guest validation failed; full-result.json and bounded logs retain the evidence.");
return success ? 0 : 1;
}
static Payload ReadPayload(string json)
{
using var document = JsonDocument.Parse(json);
if (document.RootElement.ValueKind != JsonValueKind.Object || document.RootElement.EnumerateObject().Select(property => property.Name).Distinct(StringComparer.Ordinal).Count() != document.RootElement.EnumerateObject().Count())
throw new InvalidOperationException("Payload manifest must contain one unambiguous JSON object.");
var payload = JsonSerializer.Deserialize<Payload>(json, JsonOptions) ?? throw new InvalidOperationException("Missing guest payload manifest.");
if (!Hex(payload.RunToken, 32) || !Hex(payload.SourceCommit, 40) || !Hex(payload.ArchiveSha256, 64) || payload.SdkVersion != SdkVersion || payload.SdkSha512 != SdkSha512)
throw new InvalidOperationException("Guest payload identity or SDK pin is invalid.");
return payload;
}
static void ValidateArchive(Stream stream, string commit)
{
using var reader = new TarReader(stream, leaveOpen: true);
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var commits = new List<string>();
long totalBytes = 0;
while (reader.GetNextEntry() is { } entry)
{
if (entry is PaxGlobalExtendedAttributesTarEntry global)
{
if (global.GlobalExtendedAttributes.TryGetValue("comment", out var value)) commits.Add(value);
if (global.GlobalExtendedAttributes.Keys.Any(key => key is "path" or "linkpath"))
throw new InvalidOperationException("Global tar path overrides are not supported.");
continue;
}
if (entry.EntryType is not (TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.Directory))
throw new InvalidOperationException("Source tar contains a link or unsupported entry type: " + entry.Name);
var name = entry.Name.TrimEnd('/');
if (name.Length == 0 || name.StartsWith('/') || name.Contains('\\') || name.Contains('\0') || name.Split('/').Any(part => part.Length == 0 || part is "." or ".." or ".git" or "bin" or "obj") || !names.Add(name))
throw new InvalidOperationException("Source tar path is unsafe, repeated or contains generated output: " + entry.Name);
totalBytes = checked(totalBytes + entry.Length);
if (names.Count > 100_000 || totalBytes > 2L * 1024 * 1024 * 1024)
throw new InvalidOperationException("Source tar exceeds its explicit entry/size budget.");
}
if (names.Count == 0 || commits.Count != 1 || commits[0] != commit || !names.Contains("MeetingAssistant.Tests/MeetingAssistant.Tests.csproj"))
throw new InvalidOperationException("Source tar does not prove its exact Git commit and test project.");
}
static string[] DiscoveredTests(string discovery)
{
var names = discovery.Split('\n', StringSplitOptions.TrimEntries).Where(line => line.StartsWith("MeetingAssistant.Tests.", StringComparison.Ordinal)).ToArray();
if (names.Length == 0 || names.Distinct(StringComparer.Ordinal).Count() != names.Length || RequiredNativeTests.Concat(WindowsOnlyTests).Append(CompositionGuard).Any(name => !names.Contains(name, StringComparer.Ordinal)))
throw new InvalidOperationException("Native discovery must uniquely list the platform guard, five native facts and two Windows-only facts.");
return names;
}
static TestSummary ValidateTrx(string xml, string discovery, DateTimeOffset testStarted) => ValidateTrx(Encoding.UTF8.GetBytes(xml), discovery, testStarted);
static TestSummary ValidateTrx(byte[] xml, string discovery, DateTimeOffset testStarted)
{
var discovered = DiscoveredTests(discovery);
var expected = discovered.Length;
var document = ParseXml(xml);
var root = document.Root ?? throw new InvalidOperationException("Empty TRX.");
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
if (root.Name != ns + "TestRun") throw new InvalidOperationException("Unexpected TRX namespace or root.");
var times = root.Element(ns + "Times") ?? throw new InvalidOperationException("TRX lacks execution timestamps.");
if (!DateTimeOffset.TryParse((string?)times.Attribute("start"), out var start) || !DateTimeOffset.TryParse((string?)times.Attribute("finish"), out var finish) || start < testStarted.AddSeconds(-2) || finish < start || finish > DateTimeOffset.UtcNow.AddSeconds(30))
throw new InvalidOperationException("TRX belongs to a stale or invalid test execution.");
var resultSummary = root.Element(ns + "ResultSummary") ?? throw new InvalidOperationException("TRX lacks a final result summary.");
if ((string?)resultSummary.Attribute("outcome") != "Completed") throw new InvalidOperationException("TRX test run did not complete.");
var counters = resultSummary.Element(ns + "Counters") ?? throw new InvalidOperationException("TRX lacks final counters.");
int Count(string name) => int.TryParse((string?)counters.Attribute(name), out var value) && value >= 0 ? value : throw new InvalidOperationException("TRX lacks a valid counter: " + name);
var total = Count("total");
var executed = Count("executed");
var passed = Count("passed");
var failed = Count("failed");
var notExecuted = Count("notExecuted");
foreach (var name in new[] { "error", "timeout", "aborted", "inconclusive", "passedButRunAborted", "notRunnable", "disconnected", "inProgress", "pending" })
if (counters.Attribute(name) is not null && Count(name) != 0) throw new InvalidOperationException("TRX contains an incomplete or unsuccessful execution: " + name);
if (total != expected || executed != expected - WindowsOnlyTests.Length || passed != executed || failed != 0 || notExecuted is not (0 or 2))
throw new InvalidOperationException($"Native TRX must match {expected} discovered tests with exactly two Windows-only skips and no failures; got {total}/{executed}/{passed}/{failed}/{notExecuted}.");
var definitions = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var unit in root.Element(ns + "TestDefinitions")?.Elements(ns + "UnitTest") ?? [])
{
var method = unit.Element(ns + "TestMethod") ?? throw new InvalidOperationException("TRX test definition lacks its method identity.");
var className = ((string?)method.Attribute("className") ?? "").Split(',')[0].Trim();
var methodName = (string?)method.Attribute("name") ?? "";
var id = (string?)unit.Attribute("id") ?? "";
if (id.Length == 0 || className.Length == 0 || methodName.Length == 0 || !definitions.TryAdd(id, methodName.StartsWith(className + ".", StringComparison.Ordinal) ? methodName : className + "." + methodName))
throw new InvalidOperationException("TRX contains an ambiguous test definition.");
}
var results = root.Element(ns + "Results")?.Elements(ns + "UnitTestResult").ToArray() ?? [];
var executionIds = new HashSet<string>(StringComparer.Ordinal);
var testIds = new HashSet<string>(StringComparer.Ordinal);
var resultNames = new HashSet<string>(StringComparer.Ordinal);
var native = new List<string>();
foreach (var result in results)
{
var id = (string?)result.Attribute("testId") ?? "";
var executionId = (string?)result.Attribute("executionId") ?? "";
var name = (string?)result.Attribute("testName") ?? "";
if (executionId.Length == 0 || !executionIds.Add(executionId) || !testIds.Add(id) || !definitions.TryGetValue(id, out var identity) || !resultNames.Add(name) || !discovered.Contains(name, StringComparer.Ordinal) || name != identity && !name.StartsWith(identity + "(", StringComparison.Ordinal))
throw new InvalidOperationException("TRX has a missing, duplicate or undiscovered execution.");
var windowsOnly = WindowsOnlyTests.Contains(name, StringComparer.Ordinal);
if ((string?)result.Attribute("outcome") != (windowsOnly ? "NotExecuted" : "Passed") || windowsOnly && !result.Descendants().Any(element => element.Name.LocalName is "Message" or "StdOut" && element.Value.Contains("Windows", StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException("Only the two named Windows-only facts may skip, with a Windows capability reason; every other case must pass.");
if (name == CompositionGuard && !result.Descendants().Any(element => element.Name.LocalName == "StdOut" && element.Value.Split('\n', StringSplitOptions.TrimEntries).Contains("platform-assurance: target=portable; host=macos")))
throw new InvalidOperationException("Native composition guard must report the portable target on macOS.");
if (RequiredNativeTests.Contains(identity, StringComparer.Ordinal)) native.Add(identity);
}
if (definitions.Count != expected || results.Length != expected || native.Count != RequiredNativeTests.Length || !native.Order().SequenceEqual(RequiredNativeTests.Order()))
throw new InvalidOperationException("TRX does not prove every expected test definition exactly once and the five explicit native macOS tests.");
return new(total, executed, passed, failed, WindowsOnlyTests.Length, native.ToArray());
}
static void ValidateResult(FullResult result, Payload payload)
{
if (result.Token != payload.RunToken || !result.Success || result.SourceCommit != payload.SourceCommit || result.ArchiveSha256 != payload.ArchiveSha256 || !Version.TryParse(result.OsVersion, out var version) || version.Major < 13 || result.Architecture != "x86_64" || result.SdkVersion != SdkVersion || result.ExpectedTests <= RequiredNativeTests.Length + WindowsOnlyTests.Length || result.Total != result.ExpectedTests || result.Executed != result.Total - WindowsOnlyTests.Length || result.Passed != result.Executed || result.Failed != 0 || result.NotExecuted != WindowsOnlyTests.Length || !result.NativeTests.Order().SequenceEqual(RequiredNativeTests.Order()) || result.AudioCodeSignExit != 0 || result.DesktopCodeSignExit != 0 || !Hex(result.TrxSha256, 64) || !Hex(result.DiscoverySha256, 64) || result.NativeArtifacts.Length != NativeNames.Length || !result.NativeArtifacts.Select(artifact => artifact.Name).Order().SequenceEqual(NativeNames.Order()) || result.NativeArtifacts.Any(artifact => artifact.Architecture != "x86_64" || !Hex(artifact.Sha256, 64)) || result.CompletedUtc < result.StartedUtc || result.CompletedUtc - result.StartedUtc > TimeSpan.FromMinutes(25).Add(TimeSpan.FromSeconds(15)) || result.CompletedUtc > DateTimeOffset.UtcNow.AddSeconds(30) || result.CompletedUtc < DateTimeOffset.UtcNow.AddMinutes(-1) || result.Reason.Length != 0)
throw new InvalidOperationException("Native guest receipt does not prove this source, toolchain, signed artifacts and all expected tests.");
}
static void RequirePlatform(string version, string architecture, string uid)
{
if (!Version.TryParse(version, out var parsed) || parsed.Major < 13 || architecture != "x86_64" || uid != "0")
throw new InvalidOperationException("Native build requires installed macOS 13+/x86_64 running as root.");
}
static void RequireApfs(string xml)
{
var elements = ParseXml(xml).Root?.Element("dict")?.Elements().ToArray() ?? [];
var type = elements.Select((element, index) => (element, index)).FirstOrDefault(pair => pair.element.Name == "key" && pair.element.Value == "FilesystemType");
if (type.element is null || type.index + 1 >= elements.Length || elements[type.index + 1].Name != "string" || elements[type.index + 1].Value != "apfs")
throw new InvalidOperationException("Native build must run from the installed APFS system and owned APFS work volume.");
}
static void RequireNativeArtifact(string fileOutput, string architecture)
{
if (!fileOutput.Contains("Mach-O", StringComparison.Ordinal) || !fileOutput.Contains("x86_64", StringComparison.Ordinal) || architecture != "x86_64")
throw new InvalidOperationException("Native helper is not a Mach-O executable containing exactly x86_64.");
}
static void RequireAbsent(string path)
{
if (Path.Exists(path) || GetAttributesSafe(path)?.HasFlag(FileAttributes.ReparsePoint) == true)
throw new InvalidOperationException("Fresh guest execution refuses pre-existing output: " + path);
}
static void RequireFreshFile(string path, DateTimeOffset started, long maximumBytes = long.MaxValue)
{
RequireNoLinks(path);
var file = new FileInfo(path);
if (!file.Exists || file.Length == 0 || file.Length > maximumBytes || file.LastWriteTimeUtc < started.UtcDateTime.AddSeconds(-2))
throw new InvalidOperationException("Expected a fresh, nonempty, bounded output from this execution: " + path);
}
static void RequireNoLinks(string path)
{
for (var current = Path.GetFullPath(path); current is not null; current = Path.GetDirectoryName(current))
if (GetAttributesSafe(current)?.HasFlag(FileAttributes.ReparsePoint) == true)
throw new InvalidOperationException("Guest ownership/extraction refuses a symbolic link: " + current);
}
static FileAttributes? GetAttributesSafe(string path)
{
try { return File.GetAttributes(path); }
catch (FileNotFoundException) { return null; }
catch (DirectoryNotFoundException) { return null; }
}
static XDocument ParseXml(string xml) => ParseXml(Encoding.UTF8.GetBytes(xml));
static XDocument ParseXml(byte[] xml)
{
using var stream = new MemoryStream(xml, writable: false);
using var reader = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, XmlResolver = null, MaxCharactersInDocument = 16 * 1024 * 1024 });
return XDocument.Load(reader);
}
static bool Hex(string? value, int length) => value is not null && Regex.IsMatch(value, "^[0-9a-f]{" + length + "}$");
static async Task<string> HashFile(string path, bool sha512, CancellationToken cancellation)
{
using var stream = File.OpenRead(path);
var hash = sha512 ? await SHA512.HashDataAsync(stream, cancellation) : await SHA256.HashDataAsync(stream, cancellation);
return Convert.ToHexStringLower(hash);
}
static void Save(string path, object value)
{
RequireNoLinks(path);
var temporary = path + ".tmp";
RequireNoLinks(temporary);
File.WriteAllText(temporary, JsonSerializer.Serialize(value, JsonOptions), new UTF8Encoding(false));
File.Move(temporary, path, overwrite: true);
}
static async Task<CommandResult> Command(string executable, string[] arguments, string source, string work, string logs, string label, CancellationToken cancellation, bool requireSuccess)
{
Console.WriteLine("[native-guest] " + label);
using var commandCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
var token = commandCancellation.Token;
var start = new ProcessStartInfo(executable) { WorkingDirectory = Directory.Exists(source) ? source : work, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
foreach (var argument in arguments) start.ArgumentList.Add(argument);
start.Environment.Clear();
foreach (var pair in new Dictionary<string, string>
{
["PATH"] = Path.Combine(work, "dotnet") + ":/usr/bin:/bin:/usr/sbin:/sbin",
["TMPDIR"] = Path.Combine(work, "tmp"),
["DOTNET_ROOT"] = Path.Combine(work, "dotnet"), ["DOTNET_CLI_HOME"] = Path.Combine(work, "home"),
["NUGET_PACKAGES"] = Path.Combine(work, "packages"), ["TZ"] = "Europe/Berlin",
["LANG"] = "en_US.UTF-8", ["LC_ALL"] = "en_US.UTF-8", ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1",
["DOTNET_NOLOGO"] = "1", ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1", ["MSBUILDDISABLENODEREUSE"] = "1"
}) start.Environment[pair.Key] = pair.Value;
using var process = Process.Start(start) ?? throw new InvalidOperationException("Cannot start guest command: " + label);
long capturedBytes = 0;
async Task<string> Read(StreamReader reader, string stream)
{
var captured = new StringBuilder();
var buffer = new char[8192];
using var log = new StreamWriter(Path.Combine(logs, label + "." + stream + ".log"), false, new UTF8Encoding(false));
while (true)
{
var count = await reader.ReadAsync(buffer.AsMemory(), token);
if (count == 0) break;
if (Interlocked.Add(ref capturedBytes, Encoding.UTF8.GetByteCount(buffer.AsSpan(0, count))) > MaximumLogBytes)
{
commandCancellation.Cancel();
throw new InvalidOperationException(label + " exceeded its combined 8-MiB output budget.");
}
captured.Append(buffer, 0, count);
await log.WriteAsync(buffer.AsMemory(0, count), token);
await log.FlushAsync(token);
}
return captured.ToString();
}
var stdout = Read(process.StandardOutput, "stdout");
var stderr = Read(process.StandardError, "stderr");
try
{
await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(token));
var result = new CommandResult(process.ExitCode, await stdout, await stderr);
if (requireSuccess && result.ExitCode != 0)
throw new InvalidOperationException($"{label} exited {result.ExitCode}: {result.Error[..Math.Min(result.Error.Length, 1500)]}");
return result;
}
catch
{
try { if (!process.HasExited) process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { }
using var killDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try { await process.WaitForExitAsync(killDeadline.Token); } catch (OperationCanceledException) { }
throw;
}
}
static void ValidateContracts()
{
var payload = new Payload(new string('a', 32), new string('b', 40), new string('c', 64), SdkVersion, SdkSha512);
var json = JsonSerializer.Serialize(payload, JsonOptions);
ReadPayload(json);
Reject(() => ReadPayload(json.Replace(SdkVersion, "10.0.100", StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(SdkSha512, new string('d', 128), StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(payload.RunToken, "stale", StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(payload.ArchiveSha256, "invalid", StringComparison.Ordinal)));
RequirePlatform("13.6.1", "x86_64", "0");
Reject(() => RequirePlatform("12.6.1", "x86_64", "0"));
Reject(() => RequirePlatform("14.6.1", "arm64", "0"));
Reject(() => RequirePlatform("14.6.1", "x86_64", "501"));
RequireApfs("<plist><dict><key>FilesystemType</key><string>apfs</string></dict></plist>");
Reject(() => RequireApfs("<plist><dict><key>FilesystemType</key><string>hfs</string></dict></plist>"));
RequireNativeArtifact("Mach-O 64-bit executable x86_64", "x86_64");
Reject(() => RequireNativeArtifact("Mach-O universal binary x86_64 arm64", "x86_64 arm64"));
Reject(() => RequireNativeArtifact("ELF 64-bit executable x86_64", "x86_64"));
using (var archive = FixtureArchive(payload.SourceCommit)) ValidateArchive(archive, payload.SourceCommit);
foreach (var path in new[] { "../escape", "/absolute", "safe/../escape", "safe\\escape", "MeetingAssistant/bin/stale" })
Reject(() => { using var archive = FixtureArchive(payload.SourceCommit, path); ValidateArchive(archive, payload.SourceCommit); });
Reject(() => { using var archive = FixtureArchive(payload.SourceCommit, "link", true); ValidateArchive(archive, payload.SourceCommit); });
Reject(() => { using var archive = FixtureArchive(new string('d', 40)); ValidateArchive(archive, payload.SourceCommit); });
var started = DateTimeOffset.UtcNow.AddMinutes(-1);
var fixture = FixtureTrx(started);
XNamespace ns = fixture.Root!.Name.Namespace;
var discovery = string.Join('\n', fixture.Descendants(ns + "UnitTestResult").Select(result => (string)result.Attribute("testName")!));
var summary = ValidateTrx(fixture.ToString(), discovery, started);
var plainTrx = Encoding.UTF8.GetBytes(fixture.ToString());
var bomTrx = new byte[] { 0xef, 0xbb, 0xbf }.Concat(plainTrx).ToArray();
ValidateTrx(bomTrx, discovery, started);
if (SHA256.HashData(plainTrx).SequenceEqual(SHA256.HashData(bomTrx))) throw new InvalidOperationException("TRX raw-byte hashing discarded its BOM.");
Reject(() => ValidateTrx(fixture.ToString(), discovery, started.AddMinutes(2)));
var skipped = new XDocument(fixture);
skipped.Descendants(ns + "UnitTestResult").First().SetAttributeValue("outcome", "NotExecuted");
Reject(() => ValidateTrx(skipped.ToString(), discovery, started));
var missingNative = new XDocument(fixture);
missingNative.Descendants(ns + "TestMethod").First().SetAttributeValue("name", "ManagedReplacement");
Reject(() => ValidateTrx(missingNative.ToString(), discovery, started));
var duplicate = new XDocument(fixture);
duplicate.Descendants(ns + "UnitTestResult").Last().SetAttributeValue("executionId", "execution-0");
Reject(() => ValidateTrx(duplicate.ToString(), discovery, started));
var repeatedManaged = new XDocument(fixture);
repeatedManaged.Descendants(ns + "UnitTestResult").Last().SetAttributeValue("testId", "test-5");
Reject(() => ValidateTrx(repeatedManaged.ToString(), discovery, started));
var unexecutedDefinition = new XDocument(fixture);
var extraDefinition = new XElement(unexecutedDefinition.Descendants(ns + "UnitTest").Last());
extraDefinition.SetAttributeValue("id", "unexecuted-test");
unexecutedDefinition.Root!.Element(ns + "TestDefinitions")!.Add(extraDefinition);
Reject(() => ValidateTrx(unexecutedDefinition.ToString(), discovery, started));
var missingDefinition = new XDocument(fixture);
missingDefinition.Descendants(ns + "UnitTest").Last().Remove();
Reject(() => ValidateTrx(missingDefinition.ToString(), discovery, started));
var theoryRows = new XDocument(fixture);
foreach (var method in theoryRows.Descendants(ns + "TestMethod").Skip(RequiredNativeTests.Length + WindowsOnlyTests.Length + 1).Take(2))
method.SetAttributeValue("name", "TheoryWithDistinctRowIds");
var theoryRecords = theoryRows.Descendants(ns + "UnitTestResult").Skip(RequiredNativeTests.Length + WindowsOnlyTests.Length + 1).Take(2).ToArray();
for (var index = 0; index < theoryRecords.Length; index++) theoryRecords[index].SetAttributeValue("testName", $"MeetingAssistant.Tests.ManagedTests.TheoryWithDistinctRowIds(value: {index})");
var theoryDiscovery = string.Join('\n', theoryRows.Descendants(ns + "UnitTestResult").Select(result => (string)result.Attribute("testName")!));
ValidateTrx(theoryRows.ToString(), theoryDiscovery, started);
var counters = new XDocument(fixture);
counters.Descendants(ns + "Counters").Single().SetAttributeValue("passed", "572");
Reject(() => ValidateTrx(counters.ToString(), discovery, started));
var aborted = new XDocument(fixture);
aborted.Descendants(ns + "ResultSummary").Single().SetAttributeValue("outcome", "Aborted");
Reject(() => ValidateTrx(aborted.ToString(), discovery, started));
Reject(() => ValidateTrx(fixture.ToString(), discovery + "\nMeetingAssistant.Tests.ManagedTests.Omitted", started));
Reject(() => ValidateTrx(fixture.ToString(), discovery + "\n" + RequiredNativeTests[0], started));
var unexplained = new XDocument(fixture);
unexplained.Descendants(ns + "UnitTestResult").First(result => (string?)result.Attribute("outcome") == "NotExecuted").Element(ns + "Output")!.Remove();
Reject(() => ValidateTrx(unexplained.ToString(), discovery, started));
var wrongHost = new XDocument(fixture);
wrongHost.Descendants(ns + "StdOut").Single(item => item.Value.Contains("platform-assurance:")).Value = "platform-assurance: target=portable; host=linux";
Reject(() => ValidateTrx(wrongHost.ToString(), discovery, started));
var artifacts = NativeNames.Select(name => new NativeArtifact(name, new string('d', 64), "x86_64")).ToArray();
var result = new FullResult(payload.RunToken, true, payload.SourceCommit, payload.ArchiveSha256, "13.6.1", "x86_64", SdkVersion, FixtureTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, artifacts, 0, 0, new string('e', 64), new string('f', 64), "", started, DateTimeOffset.UtcNow);
ValidateResult(result, payload);
foreach (var invalid in new[] { result with { Token = new string('f', 32) }, result with { Success = false }, result with { SourceCommit = new string('f', 40) }, result with { ArchiveSha256 = new string('f', 64) }, result with { NotExecuted = 5 }, result with { AudioCodeSignExit = 1 }, result with { DesktopCodeSignExit = 1 }, result with { NativeTests = RequiredNativeTests[..4] }, result with { NativeArtifacts = artifacts[..3] }, result with { NativeArtifacts = [artifacts[0] with { Architecture = "arm64" }, .. artifacts[1..]] }, result with { TrxSha256 = "" }, result with { DiscoverySha256 = "" }, result with { SdkVersion = "10.0.100" }, result with { OsVersion = "12.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
Reject(() => ValidateResult(invalid, payload));
var temporary = Path.Combine(OperatingSystem.IsMacOS() ? "/private/tmp" : Path.GetTempPath(), "meeting-assistant-guest-validation-" + Guid.NewGuid().ToString("N"));
try
{
RequireAbsent(temporary);
Directory.CreateDirectory(temporary);
Reject(() => RequireAbsent(temporary));
var file = Path.Combine(temporary, "fresh.trx");
File.WriteAllText(file, "fixture");
RequireFreshFile(file, started);
Reject(() => RequireFreshFile(file, started, 1));
File.SetLastWriteTimeUtc(file, started.UtcDateTime.AddMinutes(-5));
Reject(() => RequireFreshFile(file, started));
File.Delete(file);
}
finally { if (Directory.Exists(temporary)) Directory.Delete(temporary, recursive: true); }
}
static MemoryStream FixtureArchive(string commit, string? extra = null, bool link = false)
{
var stream = new MemoryStream();
using (var writer = new TarWriter(stream, TarEntryFormat.Pax, leaveOpen: true))
{
writer.WriteEntry(new PaxGlobalExtendedAttributesTarEntry(new Dictionary<string, string> { ["comment"] = commit }));
writer.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, "MeetingAssistant.Tests/MeetingAssistant.Tests.csproj") { DataStream = new MemoryStream(Encoding.UTF8.GetBytes("<Project />")) });
if (extra is not null)
{
var entry = new PaxTarEntry(link ? TarEntryType.SymbolicLink : TarEntryType.RegularFile, extra);
if (link) entry.LinkName = "../outside";
else entry.DataStream = new MemoryStream([1]);
writer.WriteEntry(entry);
}
}
stream.Position = 0;
return stream;
}
static XDocument FixtureTrx(DateTimeOffset started)
{
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
var definitions = new XElement(ns + "TestDefinitions");
var results = new XElement(ns + "Results");
for (var index = 0; index < FixtureTests; index++)
{
var identity = index < RequiredNativeTests.Length ? RequiredNativeTests[index] : index < RequiredNativeTests.Length + WindowsOnlyTests.Length ? WindowsOnlyTests[index - RequiredNativeTests.Length] : index == RequiredNativeTests.Length + WindowsOnlyTests.Length ? CompositionGuard : "MeetingAssistant.Tests.ManagedTests.Test" + index;
var separator = identity.LastIndexOf('.');
definitions.Add(new XElement(ns + "UnitTest", new XAttribute("id", "test-" + index), new XElement(ns + "TestMethod", new XAttribute("className", identity[..separator] + ", MeetingAssistant.Tests"), new XAttribute("name", identity[(separator + 1)..]))));
var skipped = WindowsOnlyTests.Contains(identity);
results.Add(new XElement(ns + "UnitTestResult", new XAttribute("testId", "test-" + index), new XAttribute("testName", identity), new XAttribute("executionId", "execution-" + index), new XAttribute("outcome", skipped ? "NotExecuted" : "Passed"), skipped || identity == CompositionGuard ? new XElement(ns + "Output", new XElement(ns + "StdOut", skipped ? "Requires Windows capabilities; run on Windows." : "platform-assurance: target=portable; host=macos")) : null));
}
return new XDocument(new XElement(ns + "TestRun", new XElement(ns + "Times", new XAttribute("start", started.ToString("O")), new XAttribute("finish", started.AddSeconds(1).ToString("O"))), definitions, results, new XElement(ns + "ResultSummary", new XAttribute("outcome", "Completed"), new XElement(ns + "Counters", new XAttribute("total", FixtureTests), new XAttribute("executed", FixtureTests - WindowsOnlyTests.Length), new XAttribute("passed", FixtureTests - WindowsOnlyTests.Length), new XAttribute("failed", 0), new XAttribute("notExecuted", 0)))));
}
static void Reject(Action action)
{
try { action(); }
catch (InvalidOperationException) { return; }
throw new InvalidOperationException("Contract validation accepted an invalid or stale fixture.");
}
sealed record Payload(string RunToken, string SourceCommit, string ArchiveSha256, string SdkVersion, string SdkSha512);
sealed record NativeArtifact(string Name, string Sha256, string Architecture);
sealed record TestSummary(int Total, int Executed, int Passed, int Failed, int NotExecuted, string[] NativeTests);
sealed record FullResult(string Token, bool Success, string SourceCommit, string ArchiveSha256, string OsVersion, string Architecture, string SdkVersion, int ExpectedTests, int Total, int Executed, int Passed, int Failed, int NotExecuted, string[] NativeTests, NativeArtifact[] NativeArtifacts, int AudioCodeSignExit, int DesktopCodeSignExit, string TrxSha256, string DiscoverySha256, string Reason, DateTimeOffset StartedUtc, DateTimeOffset CompletedUtc);
sealed record CommandResult(int ExitCode, string Output, string Error);
}
-33
View File
@@ -1,33 +0,0 @@
#!/bin/bash
# Apple pre-.NET boot seam, sourced in Recovery and on installed first boot.
# A match requires the exact run-owned emulated serial, one whole writable 64-GiB disk.
verify_owned_disk() {
local disk="$1" state="$2" token="$3" info serial matches bytes
[[ "$disk" =~ ^/dev/disk[0-9]+$ && "$token" =~ ^[0-9a-f]{32}$ ]] || return 1
[ "$(cat "$state/run.owner" 2>/dev/null)" = "$token" ] || return 1
serial="${token:0:20}"
/usr/sbin/diskutil list physical > "$state/disk-list-current.log" 2>&1 || return 1
/usr/bin/grep -Eq "^$disk[[:space:]].*physical" "$state/disk-list-current.log" || return 1
/usr/sbin/diskutil info "$disk" > "$state/disk-info-current.log" 2>&1 || return 1
info=$(cat "$state/disk-info-current.log")
printf '%s\n' "$info" | /usr/bin/grep -Eq '^[[:space:]]*Whole:[[:space:]]*Yes' || return 1
printf '%s\n' "$info" | /usr/bin/grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*Yes' && return 1
printf '%s\n' "$info" | /usr/bin/grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*No' || return 1
bytes=$(printf '%s\n' "$info" | /usr/bin/sed -nE 's/^[[:space:]]*Disk Size:.*\(([0-9]+) Bytes\).*/\1/p')
[ "$bytes" = 68719476736 ] || return 1
/usr/sbin/ioreg -r -c IOBlockStorageDevice -l -w 0 > "$state/disk-ownership-ioreg.log" 2>&1 || return 1
matches=$(/usr/bin/awk -v expected="$serial" -v disk="${disk#/dev/}" '
function finish_root() { if (serialCount == 1 && serial == expected && ownedDisk) found++ }
/^\+-o/ { finish_root(); serial=""; serialCount=0; ownedDisk=0 }
/"Serial Number"[[:space:]]*=[[:space:]]*"/ {
serialCount++; serial=$0; sub(/^.*"Serial Number"[[:space:]]*=[[:space:]]*"/, "", serial); sub(/".*$/, "", serial); sub(/[[:space:]]+$/, "", serial)
}
/"BSD Name"[[:space:]]*=[[:space:]]*"/ {
name=$0; sub(/^.*"BSD Name"[[:space:]]*=[[:space:]]*"/, "", name); sub(/".*$/, "", name)
if (name == disk) ownedDisk=1
}
END { finish_root(); print found+0 }
' "$state/disk-ownership-ioreg.log")
[ "$matches" = 1 ] || return 1
printf '[owned-disk] %s serial=%s bytes=%s\n' "$disk" "$serial" "$bytes"
}
-93
View File
@@ -1,93 +0,0 @@
#!/bin/bash
# Apple LaunchDaemon bootstrap before the guest .NET SDK exists.
# Runs only inside the isolated owned VM; installs CLT and expands the pinned SDK.
set -u
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
export PATH
PROOF_TOKEN="${1:-}"
STATE_DIR="/Volumes/installstate"
WORK="/private/var/tmp/meeting-assistant-native-$PROOF_TOKEN"
[[ "$PROOF_TOKEN" =~ ^[0-9a-f]{32}$ ]] || exit 1
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || exit 1
exec >> "$STATE_DIR/firstboot.log" 2>&1
phase() {
printf '{"token":"%s","phase":"%s","updatedUtc":"%s"}\n' "$PROOF_TOKEN" "$1" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATE_DIR/guest-phase.json.tmp"
mv -f "$STATE_DIR/guest-phase.json.tmp" "$STATE_DIR/guest-phase.json"
}
fail() { printf '[firstboot] ERROR: %s\n' "$1"; phase bootstrap-failed; exit 1; }
phase toolchain-installing
echo '[firstboot] verifying installed OS, APFS and owned physical store'
sw_vers; uname -a; id
[ "$(id -u)" = 0 ] && [ "$(uname -m)" = x86_64 ] || fail wrong_guest_platform
/usr/sbin/diskutil info -plist / > "$STATE_DIR/installed-root.plist" || fail root_diskutil_failed
[ "$(/usr/libexec/PlistBuddy -c 'Print :FilesystemType' "$STATE_DIR/installed-root.plist")" = apfs ] || fail root_is_not_installed_apfs
container=$(/usr/libexec/PlistBuddy -c 'Print :APFSContainerReference' "$STATE_DIR/installed-root.plist") || fail root_container_missing
/usr/sbin/diskutil apfs list -plist > "$STATE_DIR/apfs-containers.plist" || fail apfs_list_failed
index=0
physical=""
while reference=$(/usr/libexec/PlistBuddy -c "Print :Containers:$index:ContainerReference" "$STATE_DIR/apfs-containers.plist" 2>/dev/null); do
if [ "$reference" = "$container" ]; then
[ -z "$physical" ] || fail ambiguous_root_containers
physical=$(/usr/libexec/PlistBuddy -c "Print :Containers:$index:PhysicalStores:0:DeviceIdentifier" "$STATE_DIR/apfs-containers.plist") || fail physical_store_missing
/usr/libexec/PlistBuddy -c "Print :Containers:$index:PhysicalStores:1" "$STATE_DIR/apfs-containers.plist" >/dev/null 2>&1 && fail multiple_physical_stores
fi
index=$((index + 1))
done
[[ "$physical" =~ ^disk[0-9]+s[0-9]+$ ]] || fail invalid_physical_store
/usr/sbin/diskutil info -plist "/dev/$physical" > "$STATE_DIR/physical-store.plist" || fail physical_store_info_failed
whole=$(/usr/libexec/PlistBuddy -c 'Print :ParentWholeDisk' "$STATE_DIR/physical-store.plist") || fail physical_parent_missing
. "$STATE_DIR/macos-native-disk-guard.sh"
verify_owned_disk "/dev/$whole" "$STATE_DIR" "$PROOF_TOKEN" || fail installed_root_is_not_the_owned_64g_disk
# The phase has an independent 30-minute watchdog; host outer deadline is 180 minutes.
parent=$$
(
trap 'kill "$sleeper" 2>/dev/null || :; exit 0' TERM INT
# Toolchain watchdog: 30 minutes, also bounded by the host's 172-minute total.
sleep 1800 & sleeper=$!; wait "$sleeper"; kill -TERM "$parent" 2>/dev/null || :
) & watchdog=$!
clt_marker="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
trap 'rm -f "$clt_marker"; kill -TERM "$watchdog" 2>/dev/null || :; wait "$watchdog" 2>/dev/null || :' EXIT
trap 'fail toolchain_deadline_or_cancellation' TERM INT
[ ! -e "$WORK" ] || fail guest_work_directory_already_exists
mkdir -p "$WORK" || fail guest_work_directory_failed
printf '%s\n' "$PROOF_TOKEN" > "$WORK/run.owner" || fail guest_work_owner_failed
free_kib=$(df -Pk "$WORK" | awk 'NR==2 {print $4}')
[[ "$free_kib" =~ ^[0-9]+$ ]] && (( free_kib >= 8 * 1024 * 1024 )) || fail insufficient_existing_guest_free_space
echo '[firstboot] installing a compatible Apple CLT catalog entry without a GUI'
touch "$clt_marker" || fail clt_marker_failed
/usr/sbin/softwareupdate -l > "$STATE_DIR/clt-catalog.log" 2>&1 || fail clt_catalog_failed
label=$(grep -B 1 -E 'Command Line Tools' "$STATE_DIR/clt-catalog.log" | awk -F'*' '/^ *\*/ {print $2}' | sed -e 's/^ *Label: //' -e 's/^ *//' | sort -V | tail -n 1)
[ -n "$label" ] || fail no_compatible_headless_clt_label
printf '[firstboot] selected CLT: %s\n' "$label"
/usr/sbin/softwareupdate -i "$label" > "$STATE_DIR/clt-install.log" 2>&1 || fail clt_install_failed
/usr/bin/xcode-select --switch /Library/Developer/CommandLineTools || fail clt_switch_failed
/usr/sbin/pkgutil --pkg-info=com.apple.pkg.CLTools_Executables || fail clt_receipt_failed
/usr/bin/xcrun --find swiftc || fail swiftc_missing
/usr/bin/xcrun swiftc --version || fail swiftc_version_failed
{
/usr/bin/xcrun --sdk macosx --show-sdk-version &&
/usr/bin/xcrun --sdk macosx --show-sdk-path
} > "$STATE_DIR/clt-sdk.log" 2>&1 || fail clt_sdk_identity_failed
printf 'import AppKit\nimport AVFoundation\nimport ScreenCaptureKit\nimport EventKit\nimport WebKit\nprint("native SDK ready")\n' > "$WORK/toolchain-smoke.swift"
/usr/bin/xcrun swiftc -target x86_64-apple-macos13.0 "$WORK/toolchain-smoke.swift" -o "$WORK/toolchain-smoke" || fail native_framework_compile_failed
"$WORK/toolchain-smoke" || fail native_framework_execution_failed
rm -f "$clt_marker"
echo '[firstboot] validating and expanding the pinned .NET SDK on owned APFS'
expected_sdk=33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0
actual_sdk=$(shasum -a 512 "$STATE_DIR/sdk.tar.gz" | awk '{print $1}')
[ "$actual_sdk" = "$expected_sdk" ] || fail sdk_hash_mismatch
mkdir "$WORK/dotnet" || fail sdk_directory_failed
tar -xzf "$STATE_DIR/sdk.tar.gz" -C "$WORK/dotnet" || fail sdk_extract_failed
[ "$("$WORK/dotnet/dotnet" --version)" = 10.0.401 ] || fail sdk_version_mismatch
# Guest C# owns build/test deadlines from this point; stop the CLT-only watchdog.
kill -TERM "$watchdog" 2>/dev/null || :
wait "$watchdog" 2>/dev/null || :
trap - TERM INT
"$WORK/dotnet/dotnet" run --file "$STATE_DIR/MacOsNativeGuest.cs" -- --run --state "$STATE_DIR" --work "$WORK"
result=$?
(( result == 0 )) || fail native_tests_failed
exit 0
-72
View File
@@ -1,72 +0,0 @@
#!/bin/bash
# Full-only pre-.NET seam. Claim one persistent owned probe before forking;
# launchd then execs the original Apple daemon, including on a real failure.
set -u
PROOF_TOKEN="@@PROOF_TOKEN@@"
STATE_DIR="/Volumes/installstate"
MARKER="$STATE_DIR/probe.started"
bootstrap_failed() {
printf '[full-bootstrap] ERROR: %s\n' "$1" >&2
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
printf '{"token":"%s","phase":"bootstrap-failed","reason":"%s"}\n' "$PROOF_TOKEN" "$1" > "$STATE_DIR/guest-phase.bootstrap.$$.tmp" &&
/bin/mv -f "$STATE_DIR/guest-phase.bootstrap.$$.tmp" "$STATE_DIR/guest-phase.json"
}
wait_for_owned_state() {
local count=0
while :; do
if [ -e "$STATE_DIR/run.owner" ] || [ -L "$STATE_DIR/run.owner" ]; then
[ -f "$STATE_DIR/run.owner" ] && [ ! -L "$STATE_DIR/run.owner" ] &&
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || {
bootstrap_failed foreign_state_owner
return 1
}
return 0
fi
if (( count >= 120 )); then
bootstrap_failed state_share_mount_timeout
return 1
fi
/sbin/mount_9p installstate >/dev/null 2>&1 || :
count=$((count + 1))
sleep 1
done
}
owns_probe_marker() {
local record extra
[ -f "$MARKER" ] && [ ! -L "$MARKER" ] || return 1
{
IFS= read -r record || return 1
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
} < "$MARKER"
[ "$record" = "$PROOF_TOKEN:$source_commit" ]
}
claim_probe() {
[[ "$PROOF_TOKEN" =~ ^[0-9a-f]{32}$ ]] || { bootstrap_failed invalid_probe_token; return 1; }
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || { bootstrap_failed foreign_state_owner; return 1; }
source_commit=$(cat "$STATE_DIR/source.commit" 2>/dev/null) || { bootstrap_failed source_commit_missing; return 1; }
[[ "$source_commit" =~ ^[0-9a-f]{40}$ ]] || { bootstrap_failed source_commit_invalid; return 1; }
if [ -e "$MARKER" ] || [ -L "$MARKER" ]; then
owns_probe_marker || { bootstrap_failed foreign_or_invalid_probe_marker; return 1; }
return 2
fi
# Keep partial/failed markers: an incomplete first start is an error, no retry.
if ! ( set -o noclobber; printf '%s:%s\n' "$PROOF_TOKEN" "$source_commit" > "$MARKER" ); then
bootstrap_failed probe_marker_write_failed
return 1
fi
owns_probe_marker || { bootstrap_failed probe_marker_incomplete; return 1; }
return 0
}
if wait_for_owned_state && claim_probe; then
(
/bin/bash "$STATE_DIR/readiness.sh"
child_exit=$?
(( child_exit == 0 )) || bootstrap_failed first_probe_child_failed
) &
fi
exec /usr/libexec/recoveryosd
+115 -6
View File
@@ -14,6 +14,9 @@ TIMER_FIFO="/tmp/native-diagnostic-$PROOF_TOKEN-$$.fifo"
PENDING_OUTPUTS=() PENDING_OUTPUTS=()
ACTIVE_COMMAND="" ACTIVE_COMMAND=""
ACTIVE_TIMER="" ACTIVE_TIMER=""
# BEGIN disk IPC diagnostic
ACTIVE_OBSERVER=""
# END disk IPC diagnostic
os_version="" os_version=""
architecture="" architecture=""
uid=-1 uid=-1
@@ -96,8 +99,92 @@ read_scalar() {
SCALAR="$value" SCALAR="$value"
} }
# BEGIN successful sw_vers version parser
read_product_version() {
local value status line version="" fields=0
# Read the entire successful native output. EOF is mandatory; a NUL delimiter
# or reaching the 1025-byte sentinel must never hide a suffix.
LC_ALL=C IFS= read -r -n 1025 -d '' value < "$LAST_OUTPUT"; status=$?
(( status == 1 && ${#value} <= 1024 )) || return 1
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^[[:blank:]]*ProductVersion: ]]; then
fields=$((fields + 1))
(( fields == 1 )) || return 1
[[ "$line" =~ ^[[:blank:]]*ProductVersion:[[:blank:]]*([0-9]+\.[0-9]+(\.[0-9]+)?)[[:blank:]]*$ ]] || return 1
version="${BASH_REMATCH[1]}"
fi
done <<< "$value"
(( fields == 1 )) || return 1
SCALAR="$version"
}
# END successful sw_vers version parser
# BEGIN disk IPC diagnostic
# Optional observations have their own child/timer ownership. Thread state/time
# targets only this probe's diskutil and does not request stack symbolication.
observe_disk_query() {
local disk_process="$1" output="$2" observation_child="" observation_timer=""
cancel_observation() {
trap '' TERM INT
if [ -n "$observation_child" ]; then
kill -TERM "$observation_child" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
kill -KILL "$observation_child" 2>/dev/null || :
wait "$observation_child" 2>/dev/null || :
fi
[ -z "$observation_timer" ] || { kill -TERM "$observation_timer" 2>/dev/null || :; wait "$observation_timer" 2>/dev/null || :; }
printf '[disk-observation] stopped after the owned disk query\n' >> "$output"
exit 143
}
observe_command() {
local name="$1" status started=$SECONDS
shift
printf '\n[disk-observation-command] %s:' "$name" >> "$output"
printf ' %s' "$@" >> "$output"
printf '\n' >> "$output"
"$@" >> "$output" 2>&1 &
observation_child=$!
(
trap 'exit 0' TERM INT
IFS= read -r -t 60 -u 9 unused || :
printf '[disk-observation-timeout] %s child=%s limit=60s\n' "$name" "$observation_child" >> "$output"
kill -TERM "$observation_child" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
kill -KILL "$observation_child" 2>/dev/null || :
) &
observation_timer=$!
wait "$observation_child"; status=$?
kill -TERM "$observation_timer" 2>/dev/null || :
wait "$observation_timer" 2>/dev/null || :
printf '[disk-observation-exit] %s status=%s elapsed=%ss\n' "$name" "$status" "$((SECONDS - started))" >> "$output"
observation_child=""; observation_timer=""
}
trap cancel_observation TERM INT
printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output"
if [ -x /bin/ps ]; then
if kill -0 "$disk_process" 2>/dev/null; then
observe_command diskutil-threads /bin/ps -M -p "$disk_process"
else
printf '[disk-observation-unavailable] diskutil already exited before thread observation\n' >> "$output"
fi
else
printf '[disk-observation-unavailable] /bin/ps is unavailable\n' >> "$output"
fi
}
stop_disk_observation() {
[ -n "$ACTIVE_OBSERVER" ] || return 0
kill -TERM "$ACTIVE_OBSERVER" 2>/dev/null || :
wait "$ACTIVE_OBSERVER" 2>/dev/null || :
ACTIVE_OBSERVER=""
}
# END disk IPC diagnostic
cancel_probe() { cancel_probe() {
trap '' TERM INT trap '' TERM INT
# BEGIN disk IPC diagnostic
stop_disk_observation
# END disk IPC diagnostic
if [ -n "$ACTIVE_COMMAND" ]; then if [ -n "$ACTIVE_COMMAND" ]; then
kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || : kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || : IFS= read -r -t 2 -u 9 unused || :
@@ -116,6 +203,9 @@ run_command() {
# Run 4161: even native uname/ps startup took 34-42s under TCG. # Run 4161: even native uname/ps startup took 34-42s under TCG.
# Isolate only the failed UID gate; every other watchdog remains unchanged. # Isolate only the failed UID gate; every other watchdog remains unchanged.
[[ "$name" != uid ]] || command_limit=180 [[ "$name" != uid ]] || command_limit=180
# BEGIN disk IPC diagnostic
if [[ "$name" == disks && "${attempt:-0}" == 1 ]]; then command_limit=120; fi
# END disk IPC diagnostic
LAST_OUTPUT="/tmp/native-diagnostic-$name.out" LAST_OUTPUT="/tmp/native-diagnostic-$name.out"
printf '\n[proof-command] %s:' "$name" >&3 printf '\n[proof-command] %s:' "$name" >&3
printf ' %s' "$@" >&3 printf ' %s' "$@" >&3
@@ -136,6 +226,16 @@ run_command() {
) & ) &
timer=$! timer=$!
ACTIVE_TIMER="$timer" ACTIVE_TIMER="$timer"
# BEGIN disk IPC diagnostic
if [[ "$name" == disks && "$attempt" == 1 ]]; then
local observation_output="/tmp/native-diagnostic-disk-observation.out"
: > "$observation_output"
observe_disk_query "$process" "$observation_output" &
ACTIVE_OBSERVER=$!
printf '[disk-observation-start] observer=%s owned-diskutil-child=%s\n' "$ACTIVE_OBSERVER" "$process" >&3
PENDING_OUTPUTS+=("$observation_output")
fi
# END disk IPC diagnostic
wait "$process" wait "$process"
exit_code=$? exit_code=$?
waited=$SECONDS waited=$SECONDS
@@ -143,6 +243,9 @@ run_command() {
printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3 printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3
kill -TERM "$timer" 2>/dev/null || : kill -TERM "$timer" 2>/dev/null || :
wait "$timer" 2>/dev/null || : wait "$timer" 2>/dev/null || :
# BEGIN disk IPC diagnostic
stop_disk_observation
# END disk IPC diagnostic
ACTIVE_COMMAND=""; ACTIVE_TIMER="" ACTIVE_COMMAND=""; ACTIVE_TIMER=""
printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3 printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3
printf '[proof-exit] %s\n' "$exit_code" >&3 printf '[proof-exit] %s\n' "$exit_code" >&3
@@ -157,7 +260,6 @@ diagnose_failure() {
run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features
run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm= run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm=
run_command processes /bin/ps -axo pid,ppid,comm run_command processes /bin/ps -axo pid,ppid,comm
run_command loaded_kexts /usr/bin/kmutil showloaded --list-only
} }
fail_probe() { fail_probe() {
@@ -196,18 +298,25 @@ if (( platform_exit != 0 )); then
platform_exit="$LAST_EXIT" platform_exit="$LAST_EXIT"
fi fi
(( platform_exit == 0 )) || fail_probe sw_vers_failed (( platform_exit == 0 )) || fail_probe sw_vers_failed
run_command version /usr/bin/sw_vers -productVersion # BEGIN successful sw_vers version extraction
(( LAST_EXIT == 0 )) || fail_probe product_version_failed read_product_version || fail_probe product_version_invalid
read_scalar || fail_probe product_version_invalid # END successful sw_vers version extraction
os_version="$SCALAR" os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid [[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version (( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
flush_outputs || finish false diagnostic_log_budget_exceeded flush_outputs || finish false diagnostic_log_budget_exceeded
# BEGIN disk IPC diagnostic
run_command arbitration_before /bin/launchctl print system/com.apple.diskarbitrationd
run_command management_before /bin/launchctl print system/com.apple.diskmanagementd
run_command media_before /usr/sbin/ioreg -r -c IOMedia -l -w 0
flush_outputs || finish false diagnostic_log_budget_exceeded
# END disk IPC diagnostic
# Bound readiness independently of the host's 40-minute overall deadline. # Bound readiness independently of the host's 40-minute overall deadline.
readiness_start=$SECONDS readiness_start=$SECONDS
attempt=0 attempt=0
while (( SECONDS - readiness_start < 600 )); do while (( attempt < 1 && SECONDS - readiness_start < 600 )); do
attempt=$((attempt + 1)) attempt=$((attempt + 1))
printf '\n[readiness-attempt] %s\n' "$attempt" >&3 printf '\n[readiness-attempt] %s\n' "$attempt" >&3
run_command disks /usr/sbin/diskutil list physical run_command disks /usr/sbin/diskutil list physical
+79
View File
@@ -0,0 +1,79 @@
; Bare 64-KiB BIOS for the existing Linux QEMU binary, not macOS firmware.
; Assemble: nasm -f bin -o ci-cpu-preflight.bin macos-tcg-cpu-preflight.asm
; No disks/network. isa-debug-exit returns 33 only after AVX + AVX2 execute
; and the upper 128-bit lane contains the expected arithmetic result.
; Unsupported instructions/triple faults cannot produce the success code.
BITS 16
ORG 0
start:
cli
cld
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x8000
; QEMU zeroes fresh RAM. Identity-map the first 2 MiB through three tables.
mov dword [0x1000], 0x2003
mov dword [0x2000], 0x3003
mov dword [0x3000], 0x0083
lgdt [cs:gdt_descriptor]
mov eax, 0x40620 ; PAE, OSFXSR, OSXMMEXCPT, OSXSAVE
mov cr4, eax
mov eax, 0x1000
mov cr3, eax
mov ecx, 0xc0000080 ; EFER.LME
rdmsr
or eax, 0x100
wrmsr
mov eax, cr0
and eax, ~0x0c ; clear EM and TS before vector instructions
or eax, 0x80000003 ; paging, protected mode, monitor coprocessor
mov cr0, eax
jmp dword 0x08:(0xf0000 + long_mode)
ALIGN 8
gdt:
dq 0
dq 0x00af9a000000ffff ; ring-0 long-mode code, base 0
dq 0x00cf92000000ffff ; ring-0 data, base 0
gdt_descriptor:
dw gdt_descriptor - gdt - 1
dd 0xf0000 + gdt
BITS 64
long_mode:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
mov rsp, 0x8000
xor ecx, ecx
mov eax, 7 ; XCR0 enables x87, SSE and AVX state
xor edx, edx
xsetbv
vxorps ymm0, ymm0, ymm0 ; AVX, including the upper YMM lane
vpcmpeqd ymm1, ymm1, ymm1 ; AVX2: all eight int32 lanes become -1
vpsrld ymm1, ymm1, 31 ; AVX2: all lanes become 1
vpaddd ymm2, ymm1, ymm1 ; AVX2: all lanes become 2
vextracti128 xmm3, ymm2, 1 ; AVX2: inspect the upper half, not only SSE
vmovd eax, xmm3
cmp eax, 2
jne fail
vzeroupper
mov eax, 0x10 ; QEMU debugexit computes (value << 1) | 1
jmp exit_qemu
fail:
mov eax, 0x11
exit_qemu:
mov dx, 0xf4
out dx, eax
hlt
jmp $
; CPU reset starts at the last 16 bytes; reload the real-mode CS base.
BITS 16
TIMES 0xfff0 - ($ - $$) db 0xff
jmp 0xf000:start
TIMES 0x10000 - ($ - $$) db 0xff