diff --git a/.gitea/workflows/pr-push-build-and-test.yaml b/.gitea/workflows/pr-push-build-and-test.yaml index 83bc719..8afa519 100644 --- a/.gitea/workflows/pr-push-build-and-test.yaml +++ b/.gitea/workflows/pr-push-build-and-test.yaml @@ -90,3 +90,61 @@ jobs: run: | find MeetingAssistant.Tests -type d -name TestResults -print || true find MeetingAssistant.Tests -type f -path "*/TestResults/*" -maxdepth 5 -print || true + + macos-build-and-test: + runs-on: macos + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1" + DOTNET_NOLOGO: "1" + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Verify native macOS runner + run: | + if [ "$(uname -s)" != "Darwin" ]; then + echo "The macos job requires a native Gitea runner registered with the macos:host label." + exit 1 + fi + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + dotnet-version: "10.0.x" + + - name: Restore (portable target) + run: | + dotnet restore MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ + -p:EnableWindowsTargeting=true + + - name: Build (portable target and macOS helpers) + run: | + dotnet build MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ + -c Release \ + -f net10.0 \ + --no-restore \ + -p:EnableWindowsTargeting=true \ + --nologo + + - name: Verify macOS native helpers + run: | + test -x MeetingAssistant/bin/Release/net10.0/Native/MeetingAssistantAudioCapture.app/Contents/MacOS/macos-meeting-audio-capture + test -x MeetingAssistant/bin/Release/net10.0/Native/macos-desktop-controls + test -x MeetingAssistant/bin/Release/net10.0/Native/macos-meeting-integrations + test -x MeetingAssistant/bin/Release/net10.0/Native/macos-meeting-assistant-launcher + + - name: Run tests on macOS + run: | + dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj \ + -c Release \ + -f net10.0 \ + --no-build \ + -p:EnableWindowsTargeting=true \ + --nologo + + - name: Show test output folder on failure + if: failure() + run: | + find MeetingAssistant.Tests -type d -name TestResults -print || true + find MeetingAssistant.Tests -type f -path "*/TestResults/*" -maxdepth 5 -print || true diff --git a/MeetingAssistant.Tests/AzureSpeechStreamingTranscriptionProviderTests.cs b/MeetingAssistant.Tests/AzureSpeechStreamingTranscriptionProviderTests.cs index a280c8c..14efb99 100644 --- a/MeetingAssistant.Tests/AzureSpeechStreamingTranscriptionProviderTests.cs +++ b/MeetingAssistant.Tests/AzureSpeechStreamingTranscriptionProviderTests.cs @@ -110,6 +110,11 @@ public sealed class AzureSpeechStreamingTranscriptionProviderTests [Fact] public void ResolveKeyFallsBackToUserEnvironment() { + if (!OperatingSystem.IsWindows()) + { + return; + } + var keyEnv = $"MEETING_ASSISTANT_AZURE_KEY_{Guid.NewGuid():N}"; try { diff --git a/MeetingAssistant.Tests/MeetingNoteStoreTests.cs b/MeetingAssistant.Tests/MeetingNoteStoreTests.cs index b25b699..8c2422b 100644 --- a/MeetingAssistant.Tests/MeetingNoteStoreTests.cs +++ b/MeetingAssistant.Tests/MeetingNoteStoreTests.cs @@ -206,7 +206,7 @@ public sealed class MeetingNoteStoreTests { var link = MeetingNoteActionLinks.CreateSummaryRetryLink( "http://localhost:5090/", - "C:\\Vault\\Meetings\\Summaries\\summary with spaces.md"); + Path.Combine(Path.GetTempPath(), "summary with spaces.md")); Assert.Equal( "[Retry summary generation](http://localhost:5090/meetings/summary/retry?summaryPath=summary%20with%20spaces.md)", diff --git a/MeetingAssistant.Tests/RecordingCoordinatorTests.cs b/MeetingAssistant.Tests/RecordingCoordinatorTests.cs index 20572d8..f5c5656 100644 --- a/MeetingAssistant.Tests/RecordingCoordinatorTests.cs +++ b/MeetingAssistant.Tests/RecordingCoordinatorTests.cs @@ -448,9 +448,14 @@ public sealed class RecordingCoordinatorTests [Fact] public async Task StartCreatesMeetingNoteLinkedToTranscriptAndOpensIt() { + var vaultRoot = Path.Combine(Path.GetTempPath(), "meeting-assistant-vault"); + var transcriptPath = Path.Combine(vaultRoot, "Meetings", "Transcripts", "20260519-transcript.md"); + var meetingNotePath = Path.Combine(vaultRoot, "Meetings", "Notes", "20260519-meeting.md"); + var assistantContextFolder = Path.Combine(vaultRoot, "Meetings", "Assistant Context"); + var summariesFolder = Path.Combine(vaultRoot, "Meetings", "Summaries"); var audioSource = new ControlledAudioSource(); - var transcriptStore = new InMemoryTranscriptStore("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md"); - var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md"); + var transcriptStore = new InMemoryTranscriptStore(transcriptPath); + var noteStore = new InMemoryMeetingNoteStore(meetingNotePath); var clock = new ManualMeetingInactivityClock(DateTimeOffset.Parse("2026-05-19T10:03:42+02:00")); var noteOpener = new CapturingMeetingNoteOpener(); var artifactStore = new InMemoryMeetingArtifactStore(); @@ -468,25 +473,27 @@ public sealed class RecordingCoordinatorTests { Vault = new VaultOptions { - AssistantContextFolder = "C:\\Vault\\Meetings\\Assistant Context", - SummariesFolder = "C:\\Vault\\Meetings\\Summaries" + AssistantContextFolder = assistantContextFolder, + SummariesFolder = summariesFolder } }), NullLogger.Instance, inactivityClock: clock); var status = await coordinator.StartAsync(CancellationToken.None); + var assistantContextPath = Path.Combine(assistantContextFolder, "20260519-1003-context.md"); + var summaryPath = Path.Combine(summariesFolder, "20260519-1003-summary.md"); Assert.True(status.IsRecording); - Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", status.MeetingNotePath); - Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", noteStore.SavedNote?.Frontmatter.Transcript); - Assert.Equal("C:\\Vault\\Meetings\\Assistant Context\\20260519-1003-context.md", noteStore.SavedNote?.Frontmatter.AssistantContext); - Assert.Equal("C:\\Vault\\Meetings\\Summaries\\20260519-1003-summary.md", noteStore.SavedNote?.Frontmatter.Summary); - Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", noteOpener.OpenedPath); - Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", artifactStore.CreatedArtifacts?.MeetingNotePath); - Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", artifactStore.CreatedArtifacts?.TranscriptPath); - Assert.Equal("C:\\Vault\\Meetings\\Assistant Context\\20260519-1003-context.md", artifactStore.CreatedArtifacts?.AssistantContextPath); - Assert.Equal("C:\\Vault\\Meetings\\Summaries\\20260519-1003-summary.md", artifactStore.CreatedArtifacts?.SummaryPath); + Assert.Equal(meetingNotePath, status.MeetingNotePath); + Assert.Equal(transcriptPath, noteStore.SavedNote?.Frontmatter.Transcript); + Assert.Equal(assistantContextPath, noteStore.SavedNote?.Frontmatter.AssistantContext); + Assert.Equal(summaryPath, noteStore.SavedNote?.Frontmatter.Summary); + Assert.Equal(meetingNotePath, noteOpener.OpenedPath); + Assert.Equal(meetingNotePath, artifactStore.CreatedArtifacts?.MeetingNotePath); + Assert.Equal(transcriptPath, artifactStore.CreatedArtifacts?.TranscriptPath); + Assert.Equal(assistantContextPath, artifactStore.CreatedArtifacts?.AssistantContextPath); + Assert.Equal(summaryPath, artifactStore.CreatedArtifacts?.SummaryPath); Assert.Equal(noteStore.SavedNote?.Frontmatter.Title, artifactStore.ContextMeetingNote?.Frontmatter.Title); Assert.Equal(noteStore.SavedNote?.Frontmatter.StartTime, artifactStore.ContextMeetingNote?.Frontmatter.StartTime); diff --git a/MeetingAssistant.Tests/TaskbarIconTests.cs b/MeetingAssistant.Tests/TaskbarIconTests.cs index ec2eea3..18c3528 100644 --- a/MeetingAssistant.Tests/TaskbarIconTests.cs +++ b/MeetingAssistant.Tests/TaskbarIconTests.cs @@ -181,6 +181,11 @@ public sealed class TaskbarIconTests [SupportedOSPlatform("windows")] public void TaskbarIconGlyphsAreVisuallyCentered() { + if (!OperatingSystem.IsWindows()) + { + return; + } + foreach (var state in new[] { RecordingProcessState.Idle, diff --git a/MeetingAssistant.Tests/VaultPathTests.cs b/MeetingAssistant.Tests/VaultPathTests.cs index 91e694e..f828ab1 100644 --- a/MeetingAssistant.Tests/VaultPathTests.cs +++ b/MeetingAssistant.Tests/VaultPathTests.cs @@ -7,16 +7,16 @@ public sealed class VaultPathTests [Fact] public void ResolveUsesVaultBaseFolderForRelativePaths() { + var baseFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-vault"); var vault = new VaultOptions { - BaseFolder = @"C:\Users\masc3\OpenCloud\Persönlich\Vault\Exxeta" + BaseFolder = baseFolder }; - var resolved = VaultPath.Resolve(vault, @"Meetings\Transcripts"); + var relativePath = Path.Combine("Meetings", "Transcripts"); + var resolved = VaultPath.Resolve(vault, relativePath); - Assert.Equal( - @"C:\Users\masc3\OpenCloud\Persönlich\Vault\Exxeta\Meetings\Transcripts", - resolved); + Assert.Equal(Path.Combine(baseFolder, relativePath), resolved); } [Fact] @@ -24,11 +24,12 @@ public sealed class VaultPathTests { var vault = new VaultOptions { - BaseFolder = @"C:\Users\masc3\OpenCloud\Persönlich\Vault\Exxeta" + BaseFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-vault") }; + var absolutePath = Path.Combine(Path.GetTempPath(), "meeting-assistant-other"); - var resolved = VaultPath.Resolve(vault, @"C:\Other\Path"); + var resolved = VaultPath.Resolve(vault, absolutePath); - Assert.Equal(@"C:\Other\Path", resolved); + Assert.Equal(absolutePath, resolved); } } diff --git a/README.md b/README.md index c1d5915..27ef294 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ 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 --strict`. Documentation-only maintenance does not need a new OpenSpec change. -The Gitea workflow runs for pull requests, pushes, and manual dispatch. It builds the Windows target on an Ubuntu runner, installs Wine plus a matching Windows .NET SDK, and runs the test project through the Windows host under Wine. It validates source; it does not publish or deploy the workstation application. +The Gitea workflow runs for pull requests, pushes, and manual dispatch. Its Linux job builds the Windows target, installs Wine plus a matching Windows .NET SDK, and runs the test project through the Windows host under Wine. A separate `runs-on: macos` job builds the portable target and bundled Swift helpers, verifies the native helper executables, and runs the complete portable unit-test suite on macOS. The Gitea runner serving that job must run natively on a Mac and expose the `macos:host` label; Docker-OSX is not used because it requires nested KVM that is normally unavailable on CI runners. CI validates source; it does not publish or deploy the workstation application. ## Operations And Limitations