Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: CI

on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
dotnet:
name: .NET build and tests (Windows)
runs-on: windows-2022
timeout-minutes: 20
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false

- name: Set up .NET SDK
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9
with:
global-json-file: global.json

- name: Restore
shell: pwsh
run: dotnet restore AIBar.sln --nologo

- name: Build
shell: pwsh
run: dotnet build AIBar.sln --no-restore --configuration Debug --nologo

- name: Test
shell: pwsh
run: >-
dotnet test AIBar.sln --no-build --no-restore --configuration Debug
--logger "console;verbosity=normal"
--logger "trx;LogFileName=AIBar.Tests.trx"
--results-directory "$env:RUNNER_TEMP\aibar-test-results"

- name: Upload .NET test results
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: dotnet-test-results
path: ${{ runner.temp }}\aibar-test-results\*.trx
if-no-files-found: ignore
retention-days: 7

yasb-pester:
name: YASB Pester 5 (Windows)
runs-on: windows-2022
timeout-minutes: 20
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false

- name: Run YASB tests with pinned Pester
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$packageUri = 'https://www.powershellgallery.com/api/v2/package/Pester/5.7.1'
$packagePath = Join-Path $env:RUNNER_TEMP 'Pester.5.7.1.nupkg'
$extractPath = Join-Path $env:RUNNER_TEMP 'Pester.5.7.1'
$expectedHash = '4a27904c6814a5fbe4758f8e49861f6a1994aee77b71165a5c43c0371ba6c580'

Invoke-WebRequest -Uri $packageUri -OutFile $packagePath
$actualHash = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualHash -cne $expectedHash) {
throw "Pester package hash mismatch. Expected $expectedHash but received $actualHash."
}

Add-Type -AssemblyName System.IO.Compression.FileSystem
[IO.Compression.ZipFile]::ExtractToDirectory($packagePath, $extractPath)
$manifestPath = Join-Path $extractPath 'Pester.psd1'
Import-Module -Name $manifestPath -Force -ErrorAction Stop
$expectedModuleBase = (Resolve-Path -LiteralPath $extractPath).ProviderPath
$importedPester = Get-Module -Name Pester |
Where-Object { $_.ModuleBase -ceq $expectedModuleBase }
if ($null -eq $importedPester -or $importedPester.Version.ToString() -cne '5.7.1') {
throw "Expected imported Pester version 5.7.1 from module base $expectedModuleBase."
}

$resultDirectory = Join-Path $env:RUNNER_TEMP 'aibar-pester-results'
$resultPath = Join-Path $resultDirectory 'yasb-pester-results.xml'
New-Item -ItemType Directory -Path $resultDirectory -Force | Out-Null

$configuration = New-PesterConfiguration
$configuration.Run.Path = Join-Path $env:GITHUB_WORKSPACE 'yasb/tests/read-aibar-quota.Tests.ps1'
$configuration.Run.PassThru = $true
$configuration.Output.Verbosity = 'Detailed'
$configuration.TestResult.Enabled = $true
$configuration.TestResult.OutputPath = $resultPath
$configuration.TestResult.OutputFormat = 'NUnitXml'

$result = Invoke-Pester -Configuration $configuration
if ($result.FailedCount -ne 0 -or $result.SkippedCount -ne 0 -or $result.TotalCount -ne 11) {
Write-Error "Expected 11 passing YASB tests, but total=$($result.TotalCount) failed=$($result.FailedCount) skipped=$($result.SkippedCount)."
exit 1
}

- name: Upload YASB Pester test results
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: yasb-pester-results
path: ${{ runner.temp }}\aibar-pester-results\*.xml
if-no-files-found: ignore
retention-days: 7
7 changes: 7 additions & 0 deletions AIBar.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9135F1C3
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIBar.Domain.Tests", "tests\AIBar.Domain.Tests\AIBar.Domain.Tests.csproj", "{2FBA6A95-9279-4296-BABD-41B6BCAEDD90}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIBar.Packaging.ProcessHarness", "tests\AIBar.Packaging.ProcessHarness\AIBar.Packaging.ProcessHarness.csproj", "{7E46B5C2-420A-4C29-AEA2-F466974B2C21}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{0CA92BF2-27DA-4C5E-8414-EB95A8B2CDA4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIBar.Packaging.Supervisor", "tools\AIBar.Packaging.Supervisor\AIBar.Packaging.Supervisor.csproj", "{356C2AC2-0DBF-42AB-B637-C0CE23F99EEF}"
Expand Down Expand Up @@ -50,6 +52,10 @@ Global
{2FBA6A95-9279-4296-BABD-41B6BCAEDD90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FBA6A95-9279-4296-BABD-41B6BCAEDD90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FBA6A95-9279-4296-BABD-41B6BCAEDD90}.Release|Any CPU.Build.0 = Release|Any CPU
{7E46B5C2-420A-4C29-AEA2-F466974B2C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E46B5C2-420A-4C29-AEA2-F466974B2C21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E46B5C2-420A-4C29-AEA2-F466974B2C21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E46B5C2-420A-4C29-AEA2-F466974B2C21}.Release|Any CPU.Build.0 = Release|Any CPU
{356C2AC2-0DBF-42AB-B637-C0CE23F99EEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{356C2AC2-0DBF-42AB-B637-C0CE23F99EEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{356C2AC2-0DBF-42AB-B637-C0CE23F99EEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -72,6 +78,7 @@ Global
{D1D5DCD9-AB48-4FDE-8555-298ECBB623A5} = {B73090FB-C935-460A-BC35-085182BB3375}
{DDA2B9FB-527A-4490-AFF6-A4C77D22D0EA} = {B73090FB-C935-460A-BC35-085182BB3375}
{2FBA6A95-9279-4296-BABD-41B6BCAEDD90} = {9135F1C3-DE4D-485B-B656-A6B3DC8D4AB8}
{7E46B5C2-420A-4C29-AEA2-F466974B2C21} = {9135F1C3-DE4D-485B-B656-A6B3DC8D4AB8}
{356C2AC2-0DBF-42AB-B637-C0CE23F99EEF} = {0CA92BF2-27DA-4C5E-8414-EB95A8B2CDA4}
{A1BFC4D4-C97B-4BD5-A34A-147B489A3E9E} = {0CA92BF2-27DA-4C5E-8414-EB95A8B2CDA4}
{CF5B8208-64E5-4E50-BAD3-6E687CE7D9C4} = {0CA92BF2-27DA-4C5E-8414-EB95A8B2CDA4}
Expand Down
7 changes: 7 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.408",
"rollForward": "disable",
"allowPrerelease": false
}
}
1 change: 1 addition & 0 deletions tests/AIBar.Domain.Tests/AIBar.Domain.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ProjectReference Include="..\..\src\AIBar.Domain\AIBar.Domain.csproj" />
<ProjectReference Include="..\..\src\AIBar.Application\AIBar.Application.csproj" />
<ProjectReference Include="..\..\src\AIBar.Desktop\AIBar.Desktop.csproj" />
<ProjectReference Include="..\AIBar.Packaging.ProcessHarness\AIBar.Packaging.ProcessHarness.csproj" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\AIBar.Packaging.Supervisor.Authority.Testing\AIBar.Packaging.Supervisor.Authority.Testing.csproj" />
</ItemGroup>

Expand Down
18 changes: 10 additions & 8 deletions tests/AIBar.Domain.Tests/AnalyticsLifecycleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@
Assert.True(harness.Entered.Wait(TimeSpan.FromSeconds(2)));

var exit = harness.ExitAsync();
Assert.True(harness.Cancelled.Wait(TimeSpan.FromSeconds(2)));
await harness.CancellationObserved.Task.WaitAsync(TimeSpan.FromSeconds(2));
harness.Release.Set();
await exit;

Assert.Equal(AnalyticsShutdownKind.Completed, harness.Outcome.Kind);
Assert.Equal(1, harness.Events.Count(item => item == "analytics_cancelled"));
Assert.Equal(1, harness.Events.Count(item => item == "analytics_awaited"));
Assert.Contains("analytics_view_disposed", harness.Events);
Assert.Contains("analytics_store_disposed", harness.Events);
Assert.Contains("quota_store_disposed", harness.Events);
Assert.Equal(1, harness.Events.Count(item => item == "analytics_view_disposed"));
Assert.Equal(1, harness.Events.Count(item => item == "analytics_store_disposed"));
Assert.Equal(1, harness.Events.Count(item => item == "quota_store_disposed"));
}

[Fact]
Expand Down Expand Up @@ -210,9 +210,9 @@
private readonly string _root;
private readonly TrayHostRuntime _host;

private ProductionHarness(string root, string sourcePath, TrayHostRuntime host, App.StartupComposition composition, List<string> events, ManualResetEventSlim entered, ManualResetEventSlim cancelled, ManualResetEventSlim release, TaskCompletionSource scanFinished)
private ProductionHarness(string root, string sourcePath, TrayHostRuntime host, App.StartupComposition composition, List<string> events, ManualResetEventSlim entered, ManualResetEventSlim cancelled, ManualResetEventSlim release, TaskCompletionSource scanFinished, TaskCompletionSource cancellationObserved)
{
_root = root; SourcePath = sourcePath; _host = host; Composition = composition; Events = events; Entered = entered; Cancelled = cancelled; Release = release; ScanFinished = scanFinished;
_root = root; SourcePath = sourcePath; _host = host; Composition = composition; Events = events; Entered = entered; Cancelled = cancelled; Release = release; ScanFinished = scanFinished; CancellationObserved = cancellationObserved;
}

public App.StartupComposition Composition { get; }
Expand All @@ -221,6 +221,7 @@
public ManualResetEventSlim Cancelled { get; }
public ManualResetEventSlim Release { get; }
public TaskCompletionSource ScanFinished { get; }
public TaskCompletionSource CancellationObserved { get; }
public string SourcePath { get; }
public string AnalyticsDatabasePath => Path.Combine(_root, "analytics.db");
public string AnalyticsWalPath => Path.Combine(_root, "analytics.db-wal");
Expand All @@ -237,6 +238,7 @@
await File.WriteAllTextAsync(sourcePath, "{\"timestamp\":\"2026-01-01T00:00:00Z\",\"model\":\"gpt-5\",\"usage\":{\"input_tokens\":1,\"cached_input_tokens\":0,\"output_tokens\":1}}\n" + (partial ? "{\n" : ""));
var events = new List<string>(); var entered = new ManualResetEventSlim(); var cancelled = new ManualResetEventSlim(); var release = new ManualResetEventSlim();
var finished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var cancellationObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var scanNumber = 0;
var composition = App.CreateComposition(new(root, codex, shutdownBound, _ =>
{
Expand All @@ -246,13 +248,13 @@
}, item =>
{
lock (events) events.Add(item);
if (item == "analytics_cancelled") cancelled.Set();
if (item == "analytics_cancelled") { cancelled.Set(); cancellationObserved.TrySetResult(); }
if (item == "analytics_scan_finished") finished.TrySetResult();
}, Path.Combine(root, "home")));
var instance = new SingleInstanceHost($"AIBar.lifecycle.{Guid.NewGuid():N}");
var host = new TrayHostRuntime(instance, new FakeTray(), new FakePopover(), new FakeTaskbar(), _ => Task.CompletedTask, composition.Resource, () => { });
instance.Dispose();
return new(root, sourcePath, host, composition, events, entered, cancelled, release, finished);
return new(root, sourcePath, host, composition, events, entered, cancelled, release, finished, cancellationObserved);
}

public Task ExitAsync() => _host.ExitAsync();
Expand All @@ -267,12 +269,12 @@

private sealed class FakeTray : ITrayRuntime
{
public event Action? Toggled; public event Action? ExitRequested; public event Action? RefreshRequested; public event Action? StartupToggleRequested; public event Action? ClearAiBarDataRequested; public event Action? PrivateIntegrationEnableRequested; public event Action? PrivateIntegrationDisableRequested; public event Action? OpenCodeLocalUsageToggleRequested { add { } remove { } } public event Action? PiLocalUsageToggleRequested { add { } remove { } }

Check warning on line 272 in tests/AIBar.Domain.Tests/AnalyticsLifecycleTests.cs

View workflow job for this annotation

GitHub Actions / .NET build and tests (Windows)

The event 'AnalyticsLifecycleTests.FakeTray.PrivateIntegrationDisableRequested' is never used

Check warning on line 272 in tests/AIBar.Domain.Tests/AnalyticsLifecycleTests.cs

View workflow job for this annotation

GitHub Actions / .NET build and tests (Windows)

The event 'AnalyticsLifecycleTests.FakeTray.ExitRequested' is never used

Check warning on line 272 in tests/AIBar.Domain.Tests/AnalyticsLifecycleTests.cs

View workflow job for this annotation

GitHub Actions / .NET build and tests (Windows)

The event 'AnalyticsLifecycleTests.FakeTray.Toggled' is never used

Check warning on line 272 in tests/AIBar.Domain.Tests/AnalyticsLifecycleTests.cs

View workflow job for this annotation

GitHub Actions / .NET build and tests (Windows)

The event 'AnalyticsLifecycleTests.FakeTray.StartupToggleRequested' is never used
public void SetRefreshAvailable(bool available) { } public void SetPresentation(BetaPresentationState state) { } public void SetSettingsAvailable(bool available) { } public void SetPrivateIntegrationEnabled(bool enabled) { } public void SetStartupEnabled(bool enabled) { } public void SetLocalUsageAvailable(bool available) { } public void SetLocalUsagePolicy(LocalUsagePolicy policy) { } public void Show() { } public void Hide() { }
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
private sealed class FakePopover : IPopoverRuntime { public event Action? Deactivated; public bool IsVisible => false; public bool IsOwnedDialogActive => false; public void Show() { } public void Hide() { } public void Activate() { } }
private sealed class FakeTaskbar : ITaskbarRecreationEvents { public event Action? Recreated; }

Check warning on line 277 in tests/AIBar.Domain.Tests/AnalyticsLifecycleTests.cs

View workflow job for this annotation

GitHub Actions / .NET build and tests (Windows)

The event 'AnalyticsLifecycleTests.FakeTaskbar.Recreated' is never used

private sealed class GenerationScanner : ILocalCodexAnalyticsScanner
{
Expand Down
2 changes: 1 addition & 1 deletion tests/AIBar.Domain.Tests/HostRuntimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ async ValueTask RevokeConsentAsync(CancellationToken cancellationToken)
await provider.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));

Assert.Equal(1, prompt.Calls); Assert.Equal(1, provider.Calls); Assert.Equal(1, files.Calls);
provider.Release.TrySetResult(); PumpUntil(() => tray.DisablePrivateVisible);
provider.Release.TrySetResult(); PumpUntil(() => tray.DisablePrivateVisible && beta.State.Failure?.SafeCode == "quota_credential_missing");
using var document = System.Text.Json.JsonDocument.Parse(await File.ReadAllTextAsync(settingsFile));
Assert.Equal(2, document.RootElement.EnumerateObject().Count()); Assert.True(document.RootElement.GetProperty("privateCodexConsent").GetBoolean());
Assert.Equal("quota_credential_missing", beta.State.Failure!.SafeCode);
Expand Down
Loading
Loading