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

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

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
# Native macOS CI on the existing Ubuntu runner
`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.
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.
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.
## Entrypoints
Run from a clean checkout of the commit to test:
```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 -- --cleanup --output artifacts/native-macos-full
```
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.
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:
```sh
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
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 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.
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.
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.
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.
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.
## Required proof and side effects
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.
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: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.
`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.
| 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.
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.
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.
| 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.
`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_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_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.
`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.
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.
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.
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.
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.
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.
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.
```json
{
+58
View File
@@ -0,0 +1,58 @@
# 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
@@ -0,0 +1,23 @@
# 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.
+9 -5
View File
@@ -9,18 +9,22 @@ 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.
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:
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:
```sh
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" artifacts/wine-sdk-trust/WineSdkTrust.dll --import \
"Z:${DOTNET_ROOT}/sdk/10.0.401/trustedroots/codesignctl.pem" \
"Z:${DOTNET_ROOT}/sdk/10.0.401/trustedroots/timestampctl.pem"
export WINEPREFIX="${RUNNER_TEMP}/meeting-assistant-wine"
SDK_VERSION="$(dotnet --version)"
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" "Z:${PWD}/artifacts/wine-sdk-trust/WineSdkTrust.dll" --import \
"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 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.
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).
[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.
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.
+26 -2
View File
@@ -1,6 +1,6 @@
# Windows desktop build under Wine
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.
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.
## Observed failures
@@ -38,4 +38,28 @@ 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.
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.
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.
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.