diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dbf7d8c..b4d0441 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -22,7 +22,10 @@ jobs: - name: Install asciinema (Linux cross-check tool) if: runner.os == 'Linux' - run: pip install --only-binary :all: asciinema==2.4.0 + # ':all:' must stay quoted: unquoted, the colon+space after :all: turns + # this scalar into a nested YAML mapping and invalidates the whole + # workflow file (runs then fail in 0s with no jobs - see PR history). + run: pip install --only-binary ':all:' asciinema==2.4.0 - name: Build console-logger (Windows cross-check tool) if: runner.os == 'Windows' @@ -50,13 +53,55 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - winget install JanDeDobbeleer.OhMyPosh --accept-source-agreements --accept-package-agreements --silent - $poshPath = "$env:LOCALAPPDATA\Programs\oh-my-posh\bin" - if (Test-Path $poshPath) { - Add-Content -LiteralPath $env:GITHUB_PATH -Value $poshPath - Write-Host "oh-my-posh installed" - } else { - Write-Warning "oh-my-posh installation failed; stress test will skip" + # winget is the idiomatic installer (review decision). The version is + # pinned with --version/--exact, and winget verifies the downloaded + # installer against the winget-pkgs manifest InstallerSha256 during + # install - no manual hash step needed + # (https://learn.microsoft.com/en-us/windows/package-manager/winget/hash). + # The issue #5 stress test depends on this binary: if the install + # fails the job fails - never a silent skip. + winget install --id JanDeDobbeleer.OhMyPosh --version 31.9.1 --exact --silent --disable-interactivity --accept-source-agreements --accept-package-agreements + + # The winget package is an MSIX (ohmyposh.cli). Its public entry + # point is the app execution alias in WindowsApps - a reparse point + # that fails to execute from pwsh under redirected stdio on hosted + # runners ("StandardOutputEncoding is only supported when standard + # output is redirected"). Resolve the REAL package binary via + # Get-AppxPackage and use it for verification and PATH. + $appx = Get-AppxPackage -Name 'ohmyposh.cli' -ErrorAction Stop + if (-not $appx) { + throw 'ohmyposh.cli package not found after winget install.' + } + if ($appx.Version -ne '31.9.1.0') { + throw "oh-my-posh package version $($appx.Version) installed; expected 31.9.1.0." + } + + $packageExe = Join-Path $appx.InstallLocation 'oh-my-posh.exe' + if (-not (Test-Path -LiteralPath $packageExe)) { + throw "oh-my-posh.exe not found inside the package at $packageExe." + } + + # Hard verification: the binary must execute and report the pinned version. + $reported = ("{0}" -f (& $packageExe version)).Trim() + if ($LASTEXITCODE -ne 0 -or $reported -ne '31.9.1') { + throw "oh-my-posh reported '$reported' (exit $LASTEXITCODE); expected 31.9.1." + } + Write-Host "oh-my-posh $reported installed via winget ($packageExe)" + + # GITHUB_PATH is prepended, so 'oh-my-posh' resolves to the package + # binary, not the WindowsApps alias reparse point. + Add-Content -LiteralPath $env:GITHUB_PATH -Value $appx.InstallLocation + + # The MSIX ships the themes inside the package directory; point the + # stress test at the glyph-rich atomic theme when present (it falls + # back to the default theme, which still renders nerd-font glyphs). + $themesDir = Join-Path $appx.InstallLocation 'themes' + if (Test-Path -LiteralPath (Join-Path $themesDir 'atomic.omp.json')) { + Add-Content -LiteralPath $env:GITHUB_ENV -Value "POSH_THEMES_PATH=$themesDir" + Write-Host 'POSH_THEMES_PATH set; the stress test will use atomic.omp.json' + } + else { + Write-Host 'No themes directory found; the stress test will render the default theme.' } - name: Bootstrap development dependencies diff --git a/src/Bsn.Host/Private/Get-BsnDefaultShell.ps1 b/src/Bsn.Host/Private/Get-BsnDefaultShell.ps1 index 51e0e67..aca54ff 100644 --- a/src/Bsn.Host/Private/Get-BsnDefaultShell.ps1 +++ b/src/Bsn.Host/Private/Get-BsnDefaultShell.ps1 @@ -1,3 +1,5 @@ +using module Bsn.Common + <# .SYNOPSIS Resolves the default interactive shell for recording. @@ -12,7 +14,7 @@ function Get-BsnDefaultShell { [OutputType([string])] param() - if ((Get-BsnOperatingSystem) -eq 'Windows') { + if ((Get-BsnOperatingSystem) -eq [BsnOperatingSystem]::Windows) { foreach ($shellName in @('pwsh', 'powershell')) { $shellCommand = Get-Command -Name $shellName -ErrorAction 'SilentlyContinue' if ($shellCommand) { diff --git a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 index 81c49c5..6c3785b 100644 --- a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 +++ b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 @@ -18,7 +18,16 @@ BeforeAll { $script:RepoRoot = Split-Path -Parent -Path (Split-Path -Parent -Path $moduleRoot) $script:HostModule = Get-Module -Name 'Bsn.Host' - $script:IsWindowsPlatform = ($PSVersionTable.PSVersion.Major -ge 6) -and ($PSVersionTable.ContainsKey('Platform') -and ($PSVersionTable['Platform'] -eq 'Win32NT') -or $env:OS -eq 'Windows_NT') + # Bsn.Common (loaded above via Bsn.Host's RequiredModules) owns platform + # detection - reuse it instead of duplicating $PSVersionTable/$env:OS logic. + # The enum value comes from the defining module's scope: a bare type + # literal does not resolve from test scope, and a 'using module' header + # here would execute before the PSModulePath fix above (see AGENTS.md). + # IsWindowsPlatform is pure platform semantics (engine dispatch); the PS 7 + # console requirement is asserted per recording test with an explicit + # "system tests require PowerShell 7 console infrastructure" skip. + $script:BsnWindows = & (Get-Module -Name 'Bsn.Common') { [BsnOperatingSystem]::Windows } + $script:IsWindowsPlatform = (Get-BsnOperatingSystem) -eq $script:BsnWindows $script:CanRunSystemTests = [bool](Get-Command -Name 'pwsh' -ErrorAction 'SilentlyContinue') $script:HasAsciinema = [bool](Get-Command -Name 'asciinema' -ErrorAction 'SilentlyContinue') $script:HasTmux = [bool](Get-Command -Name 'tmux' -ErrorAction 'SilentlyContinue') diff --git a/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 b/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 index bd64927..a7d4476 100644 --- a/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 +++ b/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 @@ -13,6 +13,14 @@ BeforeAll { catch [System.IO.FileLoadException] { Import-Module -Name (Join-Path $moduleRoot 'Bsn.Host.psm1') -Force -ErrorAction 'Stop' } + + # Native keyword enums cannot be referenced by a bare type literal from + # test scope: the literal resolves only in files whose 'using module' + # header bound it at compile time (module files; see AGENTS.md), and a + # using header in this test file would execute before BeforeAll can make + # Bsn.Common resolvable. Pull the enum value from the defining module's + # own scope instead, where the literal always resolves. + $script:BsnWindows = & (Get-Module -Name 'Bsn.Common') { [BsnOperatingSystem]::Windows } } Describe 'Start-InteractiveTranscript' { @@ -219,6 +227,14 @@ Describe 'Start-InteractiveTranscript' { It 'throws when both stdin and stdout are redirected on Windows (issue #8)' { # https://github.com/BSVN/Lmt.Commons/issues/8 + # Requires a real Windows host: with -Platform Windows the version + # guard runs before the redirected-stdio check, and on Unix hosts + # [Environment]::OSVersion.Version is the kernel version (major + # below 10), so the version error fires instead of the stdio one. + # Compare against the enum value pulled from Bsn.Common (BeforeAll). + if ((Get-BsnOperatingSystem) -ne $script:BsnWindows) { + Set-ItResult -Skipped -Because 'requires a Windows host; on Unix the Windows-version guard throws first.' + } # In CI/test context, both are typically redirected if ([Console]::IsInputRedirected -and [Console]::IsOutputRedirected) { { & (Get-Module -Name 'Bsn.Host') { Test-BsnRecordingEnvironment -Platform Windows } } | Should -Throw '*cannot capture output*' @@ -230,6 +246,9 @@ Describe 'Start-InteractiveTranscript' { It 'warns (not throws) when only one handle is redirected (issue #8)' { # https://github.com/BSVN/Lmt.Commons/issues/8 + if ((Get-BsnOperatingSystem) -ne $script:BsnWindows) { + Set-ItResult -Skipped -Because 'requires a Windows host; on Unix the Windows-version guard throws first.' + } if ([Console]::IsInputRedirected -xor [Console]::IsOutputRedirected) { { & (Get-Module -Name 'Bsn.Host') { Test-BsnRecordingEnvironment -Platform Windows } } | Should -Not -Throw }