From c8a00207fdf917b65a4b496de6bef07c60c7bda9 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 01:34:39 +0330 Subject: [PATCH 01/11] fix(ci): quote pip ':all:' flag that invalidated the workflow YAML Commit 9b44e76 (SonarCloud S8541/S8544 fix) changed the asciinema install line to: run: pip install --only-binary :all: asciinema==2.4.0 Unquoted, the colon+space inside ':all: asciinema' makes YAML parse the scalar as a nested mapping (reproduced with PyYAML: yaml.scanner.ScannerError: mapping values are not allowed here line 25, column 44) which invalidated the entire workflow file. Since that commit: - push runs are created but die in 0s with no jobs and no check runs - pull_request runs are not created at all, so PRs since then showed only CodeQL/SonarCloud checks while CI silently never ran - workflow_dispatch returns 422 and the workflow is registered under its file path instead of its name (symptoms of the parse failure) Fix: quote the flag - the security intent (no setup.py execution from sdists, pinned version) is preserved: pip install --only-binary ':all:' asciinema==2.4.0 Also mark the oh-my-posh winget install step continue-on-error (the stress test skips gracefully without the tool; this is the first CI run that will execute that step). Verified locally: yaml.safe_load now parses the file; name, all three triggers, and the build job resolve correctly. --- .github/workflows/CI.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dbf7d8c..9d27fbb 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' @@ -48,6 +51,7 @@ jobs: - name: Install oh-my-posh (Windows multi-byte stress test) if: runner.os == 'Windows' + continue-on-error: true # best-effort: the stress test skips gracefully without it shell: pwsh run: | winget install JanDeDobbeleer.OhMyPosh --accept-source-agreements --accept-package-agreements --silent From 3e3e14be49c2559e1eac1bc99b5c0c4471a3b656 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 01:42:09 +0330 Subject: [PATCH 02/11] fix(ci): guarantee oh-my-posh install instead of best-effort winget Review feedback: the stress test must never silently skip on CI - a failed install has to fail the job, exactly like the silent-CI-gap this PR fixes. Replace continue-on-error winget with a deterministic, pinned direct download: - version pinned to 31.9.1 (manual bumps; no Dependabot ecosystem) - SHA256 verified against the pinned hash (supply-chain posture matches the SHA-pinned actions and pinned asciinema) - hard verification: the binary must execute and report its version - POSH_THEMES_PATH exported with atomic.omp.json from the same release so the test uses the glyph-rich theme --- .github/workflows/CI.yml | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9d27fbb..5cdc10f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -51,18 +51,38 @@ jobs: - name: Install oh-my-posh (Windows multi-byte stress test) if: runner.os == 'Windows' - continue-on-error: true # best-effort: the stress test skips gracefully without it 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" + # Deterministic pinned install (direct download; winget is not + # version-pinnable). The issue #5 stress test depends on this binary: + # if it cannot be installed the job fails - never a silent skip. + $version = '31.9.1' + $expectedHash = 'c2b34d2e8d9db243172170ce4adb70b0bd1476d29d355bc783cfa886724eb27e' + $installDir = Join-Path $env:RUNNER_TEMP 'oh-my-posh' + $themesDir = Join-Path $installDir 'themes' + New-Item -ItemType Directory -Path $themesDir -Force | Out-Null + + $exePath = Join-Path $installDir 'oh-my-posh.exe' + Invoke-WebRequest -Uri "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v$version/posh-windows-amd64.exe" -OutFile $exePath + + $actualHash = (certutil -hashfile $exePath SHA256 | Select-Object -Skip 1 -First 1).Trim() + if ($actualHash -ne $expectedHash) { + throw "oh-my-posh v$version SHA256 mismatch: expected $expectedHash, got $actualHash." } + $reported = (& $exePath version) + if ($LASTEXITCODE -ne 0 -or -not $reported) { + throw "oh-my-posh v$version failed to execute (exit code $LASTEXITCODE)." + } + Write-Host "oh-my-posh $reported installed (SHA256 verified)" + + # The stress test prefers $env:POSH_THEMES_PATH\atomic.omp.json (a + # nerd-font glyph-rich theme); provide it from the same release. + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/v$version/themes/atomic.omp.json" -OutFile (Join-Path $themesDir 'atomic.omp.json') + + Add-Content -LiteralPath $env:GITHUB_PATH -Value $installDir + Add-Content -LiteralPath $env:GITHUB_ENV -Value "POSH_THEMES_PATH=$themesDir" + - name: Bootstrap development dependencies shell: pwsh run: ./build.ps1 -Bootstrap From 2f64bd88861f7f8dfbb4a131adaa21657e6b04d1 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 01:42:09 +0330 Subject: [PATCH 03/11] fix(tests): skip issue #8 stdio tests on non-Windows hosts First real Linux CI run (the workflow had been invalid YAML since 9b44e76) exposed a platform dependency: Test-BsnRecordingEnvironment -Platform Windows checks [Environment]::OSVersion.Version before the redirected-stdio check, and on Linux that is the kernel version (major below 10), so the version error fired instead of the stdio error and the test failed. macOS passed only by luck (Darwin 23+). Skip both issue #8 tests unless running on a Windows host (5.1-safe detection: PSVersionTable.Platform guard with \Windows_NT fallback). --- .../tests/unit/Start-InteractiveTranscript.Tests.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 b/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 index bd64927..f0b683e 100644 --- a/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 +++ b/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 @@ -219,6 +219,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. + $isWindowsHost = ($PSVersionTable.ContainsKey('Platform') -and $PSVersionTable.Platform -eq 'Win32NT') -or ($env:OS -eq 'Windows_NT') + if (-not $isWindowsHost) { + 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 +238,10 @@ Describe 'Start-InteractiveTranscript' { It 'warns (not throws) when only one handle is redirected (issue #8)' { # https://github.com/BSVN/Lmt.Commons/issues/8 + $isWindowsHost = ($PSVersionTable.ContainsKey('Platform') -and $PSVersionTable.Platform -eq 'Win32NT') -or ($env:OS -eq 'Windows_NT') + if (-not $isWindowsHost) { + 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 } From 306e95cdbd2dc546f1e6e2537fb210fc0d7c81e5 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 01:48:41 +0330 Subject: [PATCH 04/11] refactor(tests): use Get-BsnOperatingSystem instead of duplicating platform detection Review feedback: the Windows-host guards inlined \System.Collections.Hashtable/ \Windows_NT detection, duplicating exactly the logic Bsn.Common owns (AGENTS.md: do not duplicate it in consumer modules). Both places now call the shared helper: - Start-InteractiveTranscript.Tests.ps1 (issue #8 tests): guard via (Get-BsnOperatingSystem) -ne 'Windows' - InteractiveTranscript.System.Tests.ps1: IsWindowsPlatform keeps its PS7+ requirement but delegates platform detection to the helper The string -eq comparison pattern already exists in production code (Get-BsnDefaultShell.ps1) and avoids enum type-literal resolution concerns in test scope. --- .../tests/system/InteractiveTranscript.System.Tests.ps1 | 4 +++- .../tests/unit/Start-InteractiveTranscript.Tests.ps1 | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 index 81c49c5..cdb8da9 100644 --- a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 +++ b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 @@ -18,7 +18,9 @@ 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. + $script:IsWindowsPlatform = ($PSVersionTable.PSVersion.Major -ge 6) -and ((Get-BsnOperatingSystem) -eq 'Windows') $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 f0b683e..18497bc 100644 --- a/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 +++ b/src/Bsn.Host/tests/unit/Start-InteractiveTranscript.Tests.ps1 @@ -223,8 +223,7 @@ Describe 'Start-InteractiveTranscript' { # 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. - $isWindowsHost = ($PSVersionTable.ContainsKey('Platform') -and $PSVersionTable.Platform -eq 'Win32NT') -or ($env:OS -eq 'Windows_NT') - if (-not $isWindowsHost) { + if ((Get-BsnOperatingSystem) -ne 'Windows') { Set-ItResult -Skipped -Because 'requires a Windows host; on Unix the Windows-version guard throws first.' } # In CI/test context, both are typically redirected @@ -238,8 +237,7 @@ Describe 'Start-InteractiveTranscript' { It 'warns (not throws) when only one handle is redirected (issue #8)' { # https://github.com/BSVN/Lmt.Commons/issues/8 - $isWindowsHost = ($PSVersionTable.ContainsKey('Platform') -and $PSVersionTable.Platform -eq 'Win32NT') -or ($env:OS -eq 'Windows_NT') - if (-not $isWindowsHost) { + if ((Get-BsnOperatingSystem) -ne 'Windows') { Set-ItResult -Skipped -Because 'requires a Windows host; on Unix the Windows-version guard throws first.' } if ([Console]::IsInputRedirected -xor [Console]::IsOutputRedirected) { From 37490f8b58f019bb597730bce01a09a4108b743e Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 01:57:46 +0330 Subject: [PATCH 05/11] refactor: compare platforms with the BsnOperatingSystem enum, not strings Review feedback. Two lessons learned empirically: 1. Get-BsnDefaultShell.ps1: -eq 'Windows' relied on string-to-enum coercion. Now uses a bare [BsnOperatingSystem]::Windows literal, bound at compile time by a 'using module Bsn.Common' header (same shape as Test-BsnRecordingEnvironment.ps1; the psm1 loader self-heals PSModulePath before dot-sourcing). 2. Test files CANNOT use bare enum literals: 'Unable to find type [BsnOperatingSystem]' at It-body runtime (verified by a failing local run). The AGENTS.md hazards are not limited to attributes and Mock proxies - test scope never binds the type. A 'using module' header in a test file would execute before BeforeAll can make Bsn.Common resolvable, so it is fragile too. Instead, BeforeAll pulls the enum value from the defining module's own scope (where the literal always resolves): \ = & (Get-Module -Name 'Bsn.Common') { [BsnOperatingSystem]::Windows } and the guards compare enum to enum. --- src/Bsn.Host/Private/Get-BsnDefaultShell.ps1 | 4 +++- .../system/InteractiveTranscript.System.Tests.ps1 | 6 +++++- .../unit/Start-InteractiveTranscript.Tests.ps1 | 13 +++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) 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 cdb8da9..ca16a65 100644 --- a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 +++ b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 @@ -20,7 +20,11 @@ BeforeAll { $script:HostModule = Get-Module -Name 'Bsn.Host' # Bsn.Common (loaded above via Bsn.Host's RequiredModules) owns platform # detection - reuse it instead of duplicating $PSVersionTable/$env:OS logic. - $script:IsWindowsPlatform = ($PSVersionTable.PSVersion.Major -ge 6) -and ((Get-BsnOperatingSystem) -eq 'Windows') + # 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). + $script:BsnWindows = & (Get-Module -Name 'Bsn.Common') { [BsnOperatingSystem]::Windows } + $script:IsWindowsPlatform = ($PSVersionTable.PSVersion.Major -ge 6) -and ((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 18497bc..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' { @@ -223,7 +231,8 @@ Describe 'Start-InteractiveTranscript' { # 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. - if ((Get-BsnOperatingSystem) -ne 'Windows') { + # 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 @@ -237,7 +246,7 @@ 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 'Windows') { + 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) { From 405ea313d584e13b869e583ba5c731ba53ceca63 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 01:57:46 +0330 Subject: [PATCH 06/11] docs(ci): correct the winget justification The previous commit message claimed winget is 'not version-pinnable' - that is wrong: winget install supports --version for exact versions, and winget pin locks future upgrades (winget.tech/blog/winget-pin-lock-versions, microsoft/winget-cli#4002). The actual reasons for the direct download are: the repo can pin the artifact's SHA256 (same supply-chain posture as the SHA-pinned actions), and it avoids winget/msstore source flakiness on hosted runners. --- .github/workflows/CI.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5cdc10f..4bbadf8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -53,9 +53,12 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - # Deterministic pinned install (direct download; winget is not - # version-pinnable). The issue #5 stress test depends on this binary: - # if it cannot be installed the job fails - never a silent skip. + # Deterministic pinned install. winget also supports exact versions + # (winget install --version plus winget pin), but a direct download + # lets the repo pin the artifact's SHA256 (same supply-chain posture + # as the SHA-pinned actions) and avoids winget/msstore source flake + # on hosted runners. If it cannot be installed the job fails - + # never a silent skip. $version = '31.9.1' $expectedHash = 'c2b34d2e8d9db243172170ce4adb70b0bd1476d29d355bc783cfa886724eb27e' $installDir = Join-Path $env:RUNNER_TEMP 'oh-my-posh' From 87f3fc48c92c09b919724ace85ca41fb17271004 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 02:05:19 +0330 Subject: [PATCH 07/11] ci: install oh-my-posh via pinned winget instead of direct download Review decision: winget is the cleaner, idiomatic approach, and it supports exact version pinning: winget install --id JanDeDobbeleer.OhMyPosh --version 31.9.1 --exact winget verifies the downloaded installer against the winget-pkgs manifest InstallerSha256 during install, so no manual hash step is needed (winget hash exists for manual verification when required: https://learn.microsoft.com/en-us/windows/package-manager/winget/hash). The step still fails loudly (no continue-on-error): after install it verifies the binary exists, executes, and reports exactly 31.9.1, and that the themes directory (POSH_THEMES_PATH for the stress test) contains atomic.omp.json. --- .github/workflows/CI.yml | 49 +++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4bbadf8..ffcf1d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -53,37 +53,34 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - # Deterministic pinned install. winget also supports exact versions - # (winget install --version plus winget pin), but a direct download - # lets the repo pin the artifact's SHA256 (same supply-chain posture - # as the SHA-pinned actions) and avoids winget/msstore source flake - # on hosted runners. If it cannot be installed the job fails - - # never a silent skip. - $version = '31.9.1' - $expectedHash = 'c2b34d2e8d9db243172170ce4adb70b0bd1476d29d355bc783cfa886724eb27e' - $installDir = Join-Path $env:RUNNER_TEMP 'oh-my-posh' - $themesDir = Join-Path $installDir 'themes' - New-Item -ItemType Directory -Path $themesDir -Force | Out-Null + # 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 - $exePath = Join-Path $installDir 'oh-my-posh.exe' - Invoke-WebRequest -Uri "https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/v$version/posh-windows-amd64.exe" -OutFile $exePath - - $actualHash = (certutil -hashfile $exePath SHA256 | Select-Object -Skip 1 -First 1).Trim() - if ($actualHash -ne $expectedHash) { - throw "oh-my-posh v$version SHA256 mismatch: expected $expectedHash, got $actualHash." + # Hard verification: the pinned version must be on disk and execute. + $poshBin = "$env:LOCALAPPDATA\Programs\oh-my-posh\bin" + $exePath = Join-Path $poshBin 'oh-my-posh.exe' + if (-not (Test-Path -LiteralPath $exePath)) { + throw "oh-my-posh.exe not found at $exePath after winget install." } - - $reported = (& $exePath version) - if ($LASTEXITCODE -ne 0 -or -not $reported) { - throw "oh-my-posh v$version failed to execute (exit code $LASTEXITCODE)." + $reported = ("{0}" -f (& $exePath 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 (SHA256 verified)" + Write-Host "oh-my-posh $reported installed via winget" + Add-Content -LiteralPath $env:GITHUB_PATH -Value $poshBin # The stress test prefers $env:POSH_THEMES_PATH\atomic.omp.json (a - # nerd-font glyph-rich theme); provide it from the same release. - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/v$version/themes/atomic.omp.json" -OutFile (Join-Path $themesDir 'atomic.omp.json') - - Add-Content -LiteralPath $env:GITHUB_PATH -Value $installDir + # nerd-font glyph-rich theme); the winget package ships themes. + $themesDir = "$env:LOCALAPPDATA\Programs\oh-my-posh\themes" + if (-not (Test-Path -LiteralPath (Join-Path $themesDir 'atomic.omp.json'))) { + throw "atomic.omp.json not found under $themesDir; the stress test expects the winget themes directory." + } Add-Content -LiteralPath $env:GITHUB_ENV -Value "POSH_THEMES_PATH=$themesDir" - name: Bootstrap development dependencies From 7f42ea039d157bfd381bcbdad177e124b925e465 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 02:05:19 +0330 Subject: [PATCH 08/11] refactor(tests): IsWindowsPlatform is pure platform semantics Review feedback: Get-BsnOperatingSystem alone is enough - the PSVersion Major -ge 6 term made a variable named IsWindowsPlatform mean 'Windows AND PowerShell 6+', which is misleading: - every recording test already asserts the PS 7 requirement explicitly with its own 'system tests require PowerShell 7 console infrastructure' skip (9 occurrences) - Invoke-BsnSystemRecording uses the flag for engine dispatch (Windows hidden-console runner vs Unix script(1)), where platform - not PowerShell edition - is the correct discriminator --- .../tests/system/InteractiveTranscript.System.Tests.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 index ca16a65..6c3785b 100644 --- a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 +++ b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 @@ -23,8 +23,11 @@ BeforeAll { # 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 = ($PSVersionTable.PSVersion.Major -ge 6) -and ((Get-BsnOperatingSystem) -eq $script:BsnWindows) + $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') From fe21e177e1dd436b8ecbd0e25dc02aea4ce2eb1f Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 02:11:22 +0330 Subject: [PATCH 09/11] fix(ci): locate the winget portable install of oh-my-posh First real run on windows-latest: winget install --version 31.9.1 succeeded, but the exe was not at the classic installer layout (Programs\oh-my-posh\bin) - the winget community package is a PORTABLE install: the exe lands under WinGet\Packages with a symlink in WinGet\Links (added to the user PATH registry, not to the step's process). The step now resolves the binary from known candidates (Links first, classic layouts as fallback) before verifying it, adds its directory to GITHUB_PATH, and treats themes opportunistically because portable packages do not ship them (the stress test falls back to the default theme, which still renders nerd-font glyphs). --- .github/workflows/CI.yml | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ffcf1d7..44f8f64 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -62,26 +62,40 @@ jobs: # 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 - # Hard verification: the pinned version must be on disk and execute. - $poshBin = "$env:LOCALAPPDATA\Programs\oh-my-posh\bin" - $exePath = Join-Path $poshBin 'oh-my-posh.exe' - if (-not (Test-Path -LiteralPath $exePath)) { - throw "oh-my-posh.exe not found at $exePath after winget install." + # The winget community package is a portable install: the exe lands + # under WinGet\Packages with a symlink in WinGet\Links (winget adds + # Links to the user PATH registry, but not to this process). + # Resolve from the known candidates instead of hardcoding one. + $candidates = @( + (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links\oh-my-posh.exe'), + (Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\bin\oh-my-posh.exe'), + (Join-Path $env:ProgramFiles 'oh-my-posh\bin\oh-my-posh.exe') + ) + $exePath = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if (-not $exePath) { + throw "oh-my-posh.exe not found in any expected location after winget install: $($candidates -join ', ')" } + + # Hard verification: the pinned version must execute. $reported = ("{0}" -f (& $exePath 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" - Add-Content -LiteralPath $env:GITHUB_PATH -Value $poshBin + Write-Host "oh-my-posh $reported installed via winget ($exePath)" + Add-Content -LiteralPath $env:GITHUB_PATH -Value (Split-Path -Parent $exePath) - # The stress test prefers $env:POSH_THEMES_PATH\atomic.omp.json (a - # nerd-font glyph-rich theme); the winget package ships themes. - $themesDir = "$env:LOCALAPPDATA\Programs\oh-my-posh\themes" - if (-not (Test-Path -LiteralPath (Join-Path $themesDir 'atomic.omp.json'))) { - throw "atomic.omp.json not found under $themesDir; the stress test expects the winget themes directory." + # Themes: the portable winget package does not ship them. When a + # classic installer layout exists, point the stress test at the + # glyph-rich atomic theme; otherwise the test renders the default + # theme (which still emits nerd-font multi-byte glyphs). + $themesDir = Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\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.' } - Add-Content -LiteralPath $env:GITHUB_ENV -Value "POSH_THEMES_PATH=$themesDir" - name: Bootstrap development dependencies shell: pwsh From f844ad57ba3be549d6efaff1b35815dbe80f33f1 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 02:16:23 +0330 Subject: [PATCH 10/11] fix(ci): resolve the MSIX layout of the oh-my-posh winget package Second windows-latest failure with empty WinGet\Links: the winget manifest for JanDeDobbeleer.OhMyPosh is InstallerType: appx - an MSIX (ohmyposh.cli), not a portable package. The executable is exposed via the app execution alias: %LOCALAPPDATA%\Microsoft\WindowsApps\oh-my-posh.exe (verified locally by installing the same pinned version). Candidate order now: MSIX alias, WinGet Links (if the manifest ever moves to portable), classic installer layout. Themes: the MSIX ships them inside the package directory, resolved via Get-AppxPackage (InstallLocation\\themes\\atomic.omp.json); classic Programs\\oh-my-posh\\themes remains a fallback and the test ultimately falls back to the default theme. --- .github/workflows/CI.yml | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 44f8f64..d585610 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -62,14 +62,13 @@ jobs: # 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 community package is a portable install: the exe lands - # under WinGet\Packages with a symlink in WinGet\Links (winget adds - # Links to the user PATH registry, but not to this process). - # Resolve from the known candidates instead of hardcoding one. + # The winget package is an MSIX (ohmyposh.cli): the executable is + # exposed through the app execution alias in WindowsApps. Keep the + # other layouts as candidates in case the manifest changes shape. $candidates = @( + (Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\oh-my-posh.exe'), (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links\oh-my-posh.exe'), - (Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\bin\oh-my-posh.exe'), - (Join-Path $env:ProgramFiles 'oh-my-posh\bin\oh-my-posh.exe') + (Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\bin\oh-my-posh.exe') ) $exePath = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $exePath) { @@ -84,12 +83,20 @@ jobs: Write-Host "oh-my-posh $reported installed via winget ($exePath)" Add-Content -LiteralPath $env:GITHUB_PATH -Value (Split-Path -Parent $exePath) - # Themes: the portable winget package does not ship them. When a - # classic installer layout exists, point the stress test at the - # glyph-rich atomic theme; otherwise the test renders the default - # theme (which still emits nerd-font multi-byte glyphs). - $themesDir = Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\themes' - if (Test-Path -LiteralPath (Join-Path $themesDir 'atomic.omp.json')) { + # The MSIX ships the themes inside the package directory; point the + # stress test at the glyph-rich atomic theme when resolvable. Any + # miss falls back to the default theme (still nerd-font multi-byte). + $themeCandidates = @() + try { + $appx = Get-AppxPackage -Name 'ohmyposh.cli' -ErrorAction Stop + if ($appx) { $themeCandidates += (Join-Path $appx.InstallLocation 'themes') } + } + catch { + Write-Verbose "Get-AppxPackage unavailable: $($_.Exception.Message)" + } + $themeCandidates += (Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\themes') + $themesDir = $themeCandidates | Where-Object { Test-Path -LiteralPath (Join-Path $_ 'atomic.omp.json') } | Select-Object -First 1 + if ($themesDir) { 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" } From 2e1db14df51d9bb890f44c3f84cb00c73a783fe9 Mon Sep 17 00:00:00 2001 From: soroshsabz Date: Sat, 7 Nov 2026 02:21:29 +0330 Subject: [PATCH 11/11] fix(ci): execute the real MSIX package binary, not the WindowsApps alias Third windows-latest failure: the alias WAS found, but executing the WindowsApps app-execution alias (a reparse point) from pwsh fails on hosted runners under redirected stdio: Program 'oh-my-posh.exe' failed to run: StandardOutputEncoding is only supported when standard output is redirected. Resolve everything from Get-AppxPackage instead: - verify the installed package version equals 31.9.1.0 - execute \oh-my-posh.exe (the real PE - verified working locally) for the version check - GITHUB_PATH gets InstallLocation (prepended), so the recorded scenario resolves 'oh-my-posh' to the real binary, never the alias - POSH_THEMES_PATH points at the package themes directory --- .github/workflows/CI.yml | 58 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d585610..b4d0441 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -62,43 +62,43 @@ jobs: # 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): the executable is - # exposed through the app execution alias in WindowsApps. Keep the - # other layouts as candidates in case the manifest changes shape. - $candidates = @( - (Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\oh-my-posh.exe'), - (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links\oh-my-posh.exe'), - (Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\bin\oh-my-posh.exe') - ) - $exePath = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 - if (-not $exePath) { - throw "oh-my-posh.exe not found in any expected location after winget install: $($candidates -join ', ')" + # 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 pinned version must execute. - $reported = ("{0}" -f (& $exePath version)).Trim() + # 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 ($exePath)" - Add-Content -LiteralPath $env:GITHUB_PATH -Value (Split-Path -Parent $exePath) + 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 resolvable. Any - # miss falls back to the default theme (still nerd-font multi-byte). - $themeCandidates = @() - try { - $appx = Get-AppxPackage -Name 'ohmyposh.cli' -ErrorAction Stop - if ($appx) { $themeCandidates += (Join-Path $appx.InstallLocation 'themes') } - } - catch { - Write-Verbose "Get-AppxPackage unavailable: $($_.Exception.Message)" - } - $themeCandidates += (Join-Path $env:LOCALAPPDATA 'Programs\oh-my-posh\themes') - $themesDir = $themeCandidates | Where-Object { Test-Path -LiteralPath (Join-Path $_ 'atomic.omp.json') } | Select-Object -First 1 - if ($themesDir) { + # 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" + 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.'