ci: validate portable tests on macos

This commit is contained in:
dh
2026-09-16 08:30:56 +02:00
parent 0e2aa3830f
commit d1d4b53ee8
7 changed files with 99 additions and 23 deletions
@@ -90,3 +90,61 @@ jobs:
run: | run: |
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-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
@@ -110,6 +110,11 @@ public sealed class AzureSpeechStreamingTranscriptionProviderTests
[Fact] [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
{ {
@@ -206,7 +206,7 @@ public sealed class MeetingNoteStoreTests
{ {
var link = MeetingNoteActionLinks.CreateSummaryRetryLink( var link = MeetingNoteActionLinks.CreateSummaryRetryLink(
"http://localhost:5090/", "http://localhost:5090/",
"C:\\Vault\\Meetings\\Summaries\\summary with spaces.md"); Path.Combine(Path.GetTempPath(), "summary with spaces.md"));
Assert.Equal( Assert.Equal(
"[Retry summary generation](http://localhost:5090/meetings/summary/retry?summaryPath=summary%20with%20spaces.md)", "[Retry summary generation](http://localhost:5090/meetings/summary/retry?summaryPath=summary%20with%20spaces.md)",
@@ -448,9 +448,14 @@ public sealed class RecordingCoordinatorTests
[Fact] [Fact]
public async Task StartCreatesMeetingNoteLinkedToTranscriptAndOpensIt() 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 audioSource = new ControlledAudioSource();
var transcriptStore = new InMemoryTranscriptStore("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md"); var transcriptStore = new InMemoryTranscriptStore(transcriptPath);
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md"); var noteStore = new InMemoryMeetingNoteStore(meetingNotePath);
var clock = new ManualMeetingInactivityClock(DateTimeOffset.Parse("2026-05-19T10:03:42+02:00")); var clock = new ManualMeetingInactivityClock(DateTimeOffset.Parse("2026-05-19T10:03:42+02:00"));
var noteOpener = new CapturingMeetingNoteOpener(); var noteOpener = new CapturingMeetingNoteOpener();
var artifactStore = new InMemoryMeetingArtifactStore(); var artifactStore = new InMemoryMeetingArtifactStore();
@@ -468,25 +473,27 @@ public sealed class RecordingCoordinatorTests
{ {
Vault = new VaultOptions Vault = new VaultOptions
{ {
AssistantContextFolder = "C:\\Vault\\Meetings\\Assistant Context", AssistantContextFolder = assistantContextFolder,
SummariesFolder = "C:\\Vault\\Meetings\\Summaries" SummariesFolder = summariesFolder
} }
}), }),
NullLogger<MeetingRecordingCoordinator>.Instance, NullLogger<MeetingRecordingCoordinator>.Instance,
inactivityClock: clock); inactivityClock: clock);
var status = await coordinator.StartAsync(CancellationToken.None); 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.True(status.IsRecording);
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", status.MeetingNotePath); Assert.Equal(meetingNotePath, status.MeetingNotePath);
Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", noteStore.SavedNote?.Frontmatter.Transcript); Assert.Equal(transcriptPath, noteStore.SavedNote?.Frontmatter.Transcript);
Assert.Equal("C:\\Vault\\Meetings\\Assistant Context\\20260519-1003-context.md", noteStore.SavedNote?.Frontmatter.AssistantContext); Assert.Equal(assistantContextPath, noteStore.SavedNote?.Frontmatter.AssistantContext);
Assert.Equal("C:\\Vault\\Meetings\\Summaries\\20260519-1003-summary.md", noteStore.SavedNote?.Frontmatter.Summary); Assert.Equal(summaryPath, noteStore.SavedNote?.Frontmatter.Summary);
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", noteOpener.OpenedPath); Assert.Equal(meetingNotePath, noteOpener.OpenedPath);
Assert.Equal("C:\\Vault\\Meetings\\Notes\\20260519-meeting.md", artifactStore.CreatedArtifacts?.MeetingNotePath); Assert.Equal(meetingNotePath, artifactStore.CreatedArtifacts?.MeetingNotePath);
Assert.Equal("C:\\Vault\\Meetings\\Transcripts\\20260519-transcript.md", artifactStore.CreatedArtifacts?.TranscriptPath); Assert.Equal(transcriptPath, artifactStore.CreatedArtifacts?.TranscriptPath);
Assert.Equal("C:\\Vault\\Meetings\\Assistant Context\\20260519-1003-context.md", artifactStore.CreatedArtifacts?.AssistantContextPath); Assert.Equal(assistantContextPath, artifactStore.CreatedArtifacts?.AssistantContextPath);
Assert.Equal("C:\\Vault\\Meetings\\Summaries\\20260519-1003-summary.md", artifactStore.CreatedArtifacts?.SummaryPath); Assert.Equal(summaryPath, artifactStore.CreatedArtifacts?.SummaryPath);
Assert.Equal(noteStore.SavedNote?.Frontmatter.Title, artifactStore.ContextMeetingNote?.Frontmatter.Title); Assert.Equal(noteStore.SavedNote?.Frontmatter.Title, artifactStore.ContextMeetingNote?.Frontmatter.Title);
Assert.Equal(noteStore.SavedNote?.Frontmatter.StartTime, artifactStore.ContextMeetingNote?.Frontmatter.StartTime); Assert.Equal(noteStore.SavedNote?.Frontmatter.StartTime, artifactStore.ContextMeetingNote?.Frontmatter.StartTime);
@@ -181,6 +181,11 @@ public sealed class TaskbarIconTests
[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,
+9 -8
View File
@@ -7,16 +7,16 @@ public sealed class VaultPathTests
[Fact] [Fact]
public void ResolveUsesVaultBaseFolderForRelativePaths() public void ResolveUsesVaultBaseFolderForRelativePaths()
{ {
var baseFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-vault");
var vault = new VaultOptions 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( Assert.Equal(Path.Combine(baseFolder, relativePath), resolved);
@"C:\Users\masc3\OpenCloud\Persönlich\Vault\Exxeta\Meetings\Transcripts",
resolved);
} }
[Fact] [Fact]
@@ -24,11 +24,12 @@ public sealed class VaultPathTests
{ {
var vault = new VaultOptions 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);
} }
} }
+1 -1
View File
@@ -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 <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 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 ## Operations And Limitations