From b8e734eb5a3537d1ba06e312d9b4131b433967d6 Mon Sep 17 00:00:00 2001 From: Seyyed Soroosh Hosseinalipour Date: Sun, 8 Nov 2026 00:42:28 +0330 Subject: [PATCH 1/2] Fix Linux recordings: script(1) invocation rejected by util-linux 2.41 - Drop the redundant positional typescript file from the util-linux script(1) invocation: util-linux 2.41 rejects combining --log-out with a positional file ("unexpected number of arguments"), so every recording on Ubuntu 26.04 exited immediately with 0 events. - Add a CI-runnable Linux/macOS system test that records a -Command session through the script(1) engine and asserts markers/zero U+FFFD - the script(1) engine previously had no end-to-end CI coverage because every Unix system test skipped under CI redirected stdio. - Bsn.Host 0.2.4; AGENTS.md documents the util-linux 2.41 constraint. Verified on WSL2 Ubuntu 26.04.1 (util-linux 2.41.3, pwsh 7.6.5): the failure is reproduced before the fix and recordings capture markers, kernel release and clean playback after it. Windows gate: analyzer clean, 96 tests green. --- AGENTS.md | 5 +++ CHANGELOG.md | 24 ++++++++++++++ src/Bsn.Host/Bsn.Host.psd1 | 4 +-- .../Private/Start-BsnUnixPtyRecording.ps1 | 7 +++- .../InteractiveTranscript.System.Tests.ps1 | 33 +++++++++++++++++++ 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5dc8373..ab8053e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,6 +167,11 @@ before: sync with ConvertTo-BsnAsciicast. `ConvertTo-BsnAsciicast` and `Get-BsnCastTimeline` are the pure, Pester-tested conversion/parsing logic - extend tests there rather than smoke-testing the native engine. + Unix script(1) invocation: do NOT pass a trailing positional typescript + file alongside `--log-out` - util-linux 2.41 (Ubuntu 26.04) rejects the + combination ('unexpected number of arguments') and recordings come back + empty; the options-only form works on current and older util-linux. + The `-Command` Unix system test keeps this pinned in CI. - `src/Bsn.SystemManagement/scripts/Suspend-Action.ps1` must remain fully self-contained (it is shipped alone to remote machines via `Invoke-Command -FilePath`, which cannot carry the module's DLL): its diff --git a/CHANGELOG.md b/CHANGELOG.md index 30684fb..de67c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to the modules in this repository are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the modules adhere to [Semantic Versioning](https://semver.org/). +## [Bsn.Host 0.2.4] - 2026-11-08 + +### Fixed + +- **Every recording on Ubuntu 26.04 (util-linux 2.41) came back empty**: + the script(1) engine combined `--log-out ` with a trailing + positional typescript file (`/dev/null`), which util-linux 2.41 + rejects with "unexpected number of arguments" - the recording process + exited immediately and produced a 0-event cast. The invocation now + uses the options-only form (`--log-timing`/`--log-out` without a + positional), which works on current and older util-linux releases. + Found while verifying WSL2/Ubuntu 26.04 end to end: `echo`-level + recordings now capture markers, kernel release and zero U+FFFD. + +### Added + +- A CI-runnable Linux/macOS system test for `-Command` recordings via + the script(1) engine (the engine had zero end-to-end CI coverage: + every other Unix system test skips under CI redirected stdio). It + asserts markers, kernel name and zero U+FFFD in the cast, and pins + the util-linux 2.41 invocation regression. +- Show-InteractiveTranscript follows the same restore discipline for + playback writes (from 0.2.3) - unchanged here. + ## [Bsn.Host 0.2.3] - 2026-11-07 ### Fixed diff --git a/src/Bsn.Host/Bsn.Host.psd1 b/src/Bsn.Host/Bsn.Host.psd1 index b374d4e..9b6effc 100644 --- a/src/Bsn.Host/Bsn.Host.psd1 +++ b/src/Bsn.Host/Bsn.Host.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'Bsn.Host.psm1' - ModuleVersion = '0.2.3' + ModuleVersion = '0.2.4' GUID = '9d84a7b8-1f2b-4d60-a4bd-3d7ef1d9f337' Author = 'BSVN' CompanyName = 'BSVN' @@ -21,7 +21,7 @@ Tags = @('BSVN', 'Lmt', 'Host', 'Transcript', 'Recording', 'Terminal', 'Session', 'Asciicast', 'Asciinema', 'TUI', 'Audit', 'Screencast') LicenseUri = 'https://github.com/BSVN/Lmt.Commons/blob/main/LICENSE' ProjectUri = 'https://github.com/BSVN/Lmt.Commons' - ReleaseNotes = 'Bug fix release: the LIVE recording view (what you watch while recording) no longer turns box-drawing text into CP437 mojibake on OEM-codepage consoles (cmd, Windows PowerShell 5.1 hosts) - the engine switches the console to UTF-8 for the recording and restores it afterwards; the cast file was always correct. Also includes the 0.2.2 fixes: recordings of byte-writing TUI applications (OpenCode and other Go/Node TUIs) bootstrap the console codepage to UTF-8; interop assembly loading is loader-managed (Import-Module -Force reuses the loaded assembly; cross-version imports get clear guidance). Fixes issues #5 and #15.' + ReleaseNotes = 'Bug fix release: recordings on Linux distros shipping util-linux 2.41 (Ubuntu 26.04) came back empty - the script(1) invocation combined --log-out with a trailing positional file, rejected since util-linux 2.41; it now uses the options-only form, and a CI-runnable Linux/macOS script(1) engine system test pins the regression. Also includes the 0.2.3 fixes: the LIVE recording view and Show-InteractiveTranscript playback no longer corrupt box-drawing characters on OEM-codepage consoles - the console output codepage/encoding is switched to UTF-8 for the operation and restored afterwards. Fixes issue #5.' } } } diff --git a/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 b/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 index 8857e42..daa45e8 100644 --- a/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 +++ b/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 @@ -83,7 +83,12 @@ function Start-BsnUnixPtyRecording { $outputPath = [System.IO.Path]::GetTempFileName() $temporaryPaths = @($timingPath, $outputPath) - $processInfo.Arguments = ('-q -f -e -c "{0}" --log-timing "{1}" --log-out "{2}" /dev/null' -f $recordedCommand, $timingPath, $outputPath) + # No trailing positional typescript file: --log-out already captures + # the stream, and util-linux 2.41 (Ubuntu 26.04) rejects the + # combination of --log-out and a positional file with 'unexpected + # number of arguments', which made every recording come back empty. + # Older util-linux versions accept the options-only form too. + $processInfo.Arguments = ('-q -f -e -c "{0}" --log-timing "{1}" --log-out "{2}"' -f $recordedCommand, $timingPath, $outputPath) } else { $rawPath = [System.IO.Path]::GetTempFileName() diff --git a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 index cb88995..00692e9 100644 --- a/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 +++ b/src/Bsn.Host/tests/system/InteractiveTranscript.System.Tests.ps1 @@ -1084,6 +1084,39 @@ Write-Output 'METACHAR:a|b&ce^f' } } + Context 'Unix script(1) engine: -Command recording (util-linux 2.41 regression)' { + It 'records a -Command session and captures the markers on Linux/macOS' -Tag 'SystemTest' { + # Regression for Ubuntu 26.04 / util-linux 2.41: util-linux rejects + # combining --log-out with a trailing positional typescript file + # ('unexpected number of arguments'), which made every -Command + # recording come back empty (0 events). The invocation must use the + # options-only form. Runs under CI redirected stdio on purpose: + # script(1) allocates its own PTY, and -Command children like + # 'echo' do not need stdin, so this is deterministic in CI. + if (-not $script:CanRunSystemTests) { + Set-ItResult -Skipped -Because 'pwsh is required to run recorded scenarios and was not found.' + } + if ($PSVersionTable.PSVersion.Major -lt 7) { + Set-ItResult -Skipped -Because 'system tests require PowerShell 7 console infrastructure.' + } + if ($script:IsWindowsPlatform) { + Set-ItResult -Skipped -Because 'Windows uses the ConPTY engine; this regression targets the Unix script(1) engine.' + } + + $marker = 'UNIXMARKER-{0}' -f (Get-Random) + $castPath = Join-Path -Path $TestDrive -ChildPath 'unix-command.cast' + $result = Start-InteractiveTranscript -Command ("echo $marker; uname -s") -Path $castPath + + $result.ExitCode | Should -Be 0 -Because 'the recorded echo command must succeed' + $stream = Get-BsnCastRawStream -CastPath $castPath + $stream.Length | Should -BeGreaterThan 0 -Because 'the recording must not be empty (util-linux 2.41 invocation regression)' + $stream.Contains($marker) | Should -BeTrue -Because 'the recorded marker must be captured' + $stream.Contains('Linux') -or $stream.Contains('Darwin') | Should -BeTrue -Because 'the uname -s output must be captured' + $replacementCount = ([regex]::Matches($stream, [regex]::Escape([string][char]0xFFFD))).Count + $replacementCount | Should -Be 0 -Because 'the recording must contain zero U+FFFD replacement characters' + } + } + Context 'Stop-InteractiveTranscript finalizes a running session' { It 'stops early via the stop channel and captures everything before the stop' { if (-not $script:CanRunSystemTests) { From fdf2f7bf0183c0adb87e1003d8fbe5cd8e59c80c Mon Sep 17 00:00:00 2001 From: Seyyed Soroosh Hosseinalipour Date: Sun, 8 Nov 2026 01:05:46 +0330 Subject: [PATCH 2/2] Version-agnostic script(1) invocation probe + unit tests + ubuntu-24.04 CI image The first fix hardcoded the options-only invocation, which is unverified on older distros and would silently change behavior there. Docker verification of the real script(1) binaries refined the matrix: - util-linux 2.34 (Ubuntu 20.04): no --log-timing at all - the engine uses the approximated-timing (BSD-style) branch, never the buggy form. - util-linux 2.37.2 (Ubuntu 22.04): BOTH shapes work. - util-linux 2.39.3 (Ubuntu 24.04): positional form REJECTED - 24.04 was broken by the same bug as 26.04 (2.41.3), not just 26.04. - util-linux 2.41.3 (Ubuntu 26.04): positional form REJECTED. Instead of parsing version strings, the engine now probes the installed script(1) binary once with a no-op child: where the classic positional form is accepted (22.04 and older) it is kept unchanged; where it is rejected (24.04/26.04) the options-only form is used. New private Resolve-BsnScriptInvocationArguments-free helper Resolve-BsnScriptInvocation implements the probe, deterministic on any future util-linux release. Tests: - Unit: Resolve-BsnScriptInvocation with fake script(1) shims - a shim that exits 0 must keep the positional form, one that exits 1 must get the options-only form, and both shapes must carry the recorded command and both log paths (runs on Linux/macOS CI; Windows skips - no Unix exec-bit shims). - System: the Unix -Command recording test (from the previous commit) runs on ubuntu CI images and pins the end-to-end behavior. - CI matrix: add a pinned ubuntu-24.04 image alongside ubuntu-latest so two util-linux generations (2.39 and 2.41) exercise the script(1) engine on every pull request. --- .github/workflows/CI.yml | 8 +- AGENTS.md | 15 ++-- CHANGELOG.md | 31 ++++--- src/Bsn.Host/Bsn.Host.psd1 | 2 +- .../Private/Resolve-BsnScriptInvocation.ps1 | 70 ++++++++++++++++ .../Private/Start-BsnUnixPtyRecording.ps1 | 10 +-- .../Resolve-BsnScriptInvocation.Tests.ps1 | 81 +++++++++++++++++++ 7 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 src/Bsn.Host/Private/Resolve-BsnScriptInvocation.ps1 create mode 100644 src/Bsn.Host/tests/unit/Resolve-BsnScriptInvocation.Tests.ps1 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b4d0441..0c7d51a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,7 +12,13 @@ jobs: strategy: fail-fast: false matrix: - os: [ windows-latest, ubuntu-latest, macos-latest ] + os: [ windows-latest, ubuntu-latest, ubuntu-24.04, macos-latest ] + # ubuntu-24.04 is pinned on purpose: ubuntu-latest follows the newest LTS + # (26.04, util-linux 2.41), while 24.04 keeps an older script(1) and + # util-linux generation in the matrix - the Unix script(1) engine has + # version-sensitive invocation compatibility (see Start-BsnUnixPtyRecording + # and Get-BsnScriptInvocationArguments), and the Unix -Command system test + # pins that behavior on both generations. runs-on: ${{ matrix.os }} permissions: id-token: write # Codecov OIDC token exchange (tokenless default-branch uploads) diff --git a/AGENTS.md b/AGENTS.md index ab8053e..8e89313 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,11 +167,16 @@ before: sync with ConvertTo-BsnAsciicast. `ConvertTo-BsnAsciicast` and `Get-BsnCastTimeline` are the pure, Pester-tested conversion/parsing logic - extend tests there rather than smoke-testing the native engine. - Unix script(1) invocation: do NOT pass a trailing positional typescript - file alongside `--log-out` - util-linux 2.41 (Ubuntu 26.04) rejects the - combination ('unexpected number of arguments') and recordings come back - empty; the options-only form works on current and older util-linux. - The `-Command` Unix system test keeps this pinned in CI. + Unix script(1) invocation: the engine PROBES the installed script(1) + binary (Get-BsnScriptInvocationArguments) because util-linux 2.39+ + (Ubuntu 24.04/26.04) rejects combining `--log-out` with a trailing + positional typescript file ('unexpected number of arguments') - which + made recordings come back empty - while 2.37 and older accept both + shapes; do not hardcode either invocation shape or parse util-linux + version strings. The `-Command` Unix system test plus the + Get-BsnScriptInvocationArguments unit tests keep this pinned, and the + CI matrix runs a pinned ubuntu-24.04 image (util-linux 2.39) next to + ubuntu-latest so two util-linux generations are covered. - `src/Bsn.SystemManagement/scripts/Suspend-Action.ps1` must remain fully self-contained (it is shipped alone to remote machines via `Invoke-Command -FilePath`, which cannot carry the module's DLL): its diff --git a/CHANGELOG.md b/CHANGELOG.md index de67c68..5ac1d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,21 @@ and the modules adhere to [Semantic Versioning](https://semver.org/). ### Fixed -- **Every recording on Ubuntu 26.04 (util-linux 2.41) came back empty**: - the script(1) engine combined `--log-out ` with a trailing - positional typescript file (`/dev/null`), which util-linux 2.41 - rejects with "unexpected number of arguments" - the recording process - exited immediately and produced a 0-event cast. The invocation now - uses the options-only form (`--log-timing`/`--log-out` without a - positional), which works on current and older util-linux releases. - Found while verifying WSL2/Ubuntu 26.04 end to end: `echo`-level - recordings now capture markers, kernel release and zero U+FFFD. +- **Every recording on Ubuntu 24.04 and 26.04 (util-linux 2.39 / 2.41) + came back empty**: the script(1) engine combined `--log-out ` + with a trailing positional typescript file (`/dev/null`), which + util-linux 2.39 rejects with "unexpected number of arguments" - the + recording process exited immediately and produced a 0-event cast. + (Ubuntu 22.04 / util-linux 2.37 accepts both shapes; Ubuntu 20.04 / + 2.34 has no `--log-timing` at all and uses the approximated-timing + branch, so it was never affected.) The engine now **probes the + installed script(1) binary** once with a no-op child and picks the + invocation shape it accepts - the historically tested positional form + where supported, the options-only form where not - so no util-linux + version string parsing is involved and future releases are handled by + the same probe. Found while verifying WSL2/Ubuntu 26.04 end to end: + `echo`-level recordings now capture markers, kernel release and zero + U+FFFD. ### Added @@ -24,7 +30,12 @@ and the modules adhere to [Semantic Versioning](https://semver.org/). the script(1) engine (the engine had zero end-to-end CI coverage: every other Unix system test skips under CI redirected stdio). It asserts markers, kernel name and zero U+FFFD in the cast, and pins - the util-linux 2.41 invocation regression. + the invocation-compatibility regression. +- Unit tests for the invocation probe with fake script(1) binaries + (accepting/rejecting), covering both invocation shapes. +- CI matrix: adds a pinned `ubuntu-24.04` image alongside + `ubuntu-latest` (26.04) so two util-linux generations exercise the + script(1) engine on every pull request. - Show-InteractiveTranscript follows the same restore discipline for playback writes (from 0.2.3) - unchanged here. diff --git a/src/Bsn.Host/Bsn.Host.psd1 b/src/Bsn.Host/Bsn.Host.psd1 index 9b6effc..756ec24 100644 --- a/src/Bsn.Host/Bsn.Host.psd1 +++ b/src/Bsn.Host/Bsn.Host.psd1 @@ -21,7 +21,7 @@ Tags = @('BSVN', 'Lmt', 'Host', 'Transcript', 'Recording', 'Terminal', 'Session', 'Asciicast', 'Asciinema', 'TUI', 'Audit', 'Screencast') LicenseUri = 'https://github.com/BSVN/Lmt.Commons/blob/main/LICENSE' ProjectUri = 'https://github.com/BSVN/Lmt.Commons' - ReleaseNotes = 'Bug fix release: recordings on Linux distros shipping util-linux 2.41 (Ubuntu 26.04) came back empty - the script(1) invocation combined --log-out with a trailing positional file, rejected since util-linux 2.41; it now uses the options-only form, and a CI-runnable Linux/macOS script(1) engine system test pins the regression. Also includes the 0.2.3 fixes: the LIVE recording view and Show-InteractiveTranscript playback no longer corrupt box-drawing characters on OEM-codepage consoles - the console output codepage/encoding is switched to UTF-8 for the operation and restored afterwards. Fixes issue #5.' + ReleaseNotes = 'Bug fix release: recordings on Linux distros shipping util-linux 2.39 or 2.41 (Ubuntu 24.04 and 26.04) came back empty - the script(1) invocation combined --log-out with a trailing positional file, rejected since util-linux 2.39; the engine now probes the installed script(1) binary and picks the invocation shape it accepts, and a CI-runnable Linux/macOS script(1) engine system test plus a pinned ubuntu-24.04 CI image pin the regression. Also includes the 0.2.3 fixes: the LIVE recording view and Show-InteractiveTranscript playback no longer corrupt box-drawing characters on OEM-codepage consoles - the console output codepage/encoding is switched to UTF-8 for the operation and restored afterwards. Fixes issue #5.' } } } diff --git a/src/Bsn.Host/Private/Resolve-BsnScriptInvocation.ps1 b/src/Bsn.Host/Private/Resolve-BsnScriptInvocation.ps1 new file mode 100644 index 0000000..62446c1 --- /dev/null +++ b/src/Bsn.Host/Private/Resolve-BsnScriptInvocation.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + Builds the script(1) arguments for a timing-based recording, probing the + installed binary for invocation compatibility. + +.DESCRIPTION + util-linux changed script(1) invocation compatibility across releases: + + - util-linux 2.39+ (Ubuntu 24.04, 26.04) REJECTS combining --log-out + with a trailing positional typescript file ('unexpected number of + arguments') - the recording process exits immediately and every cast + comes back empty. + - util-linux 2.37 (Ubuntu 22.04) and older releases with --log-timing + accept both shapes. + + This helper probes the installed binary once with a no-op child using + the classic positional form: where it is accepted, the historically + tested invocation is kept; where it is rejected, the options-only form + is used. No version string parsing is involved, so any future + util-linux release is handled by the same probe. + + Kept private (invoked by the Start-InteractiveTranscript engine). +#> +function Resolve-BsnScriptInvocation { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$ScriptPath, + + [Parameter(Mandatory = $true)] + [string]$Command, + + [Parameter(Mandatory = $true)] + [string]$TimingPath, + + [Parameter(Mandatory = $true)] + [string]$OutputPath + ) + + $probeTimingPath = [System.IO.Path]::GetTempFileName() + $probeOutputPath = [System.IO.Path]::GetTempFileName() + try { + $probeInfo = [System.Diagnostics.ProcessStartInfo]::new() + $probeInfo.FileName = $ScriptPath + $probeInfo.UseShellExecute = $false + $probeInfo.RedirectStandardError = $true + $probeInfo.Arguments = ('-q -f -e -c "true" --log-timing "{0}" --log-out "{1}" /dev/null' -f $probeTimingPath, $probeOutputPath) + $probeProcess = [System.Diagnostics.Process]::Start($probeInfo) + if (-not $probeProcess.WaitForExit(10000)) { + $probeProcess.Kill() + Write-Verbose -Message 'script(1) invocation probe timed out; using the options-only form.' + return ('-q -f -e -c "{0}" --log-timing "{1}" --log-out "{2}"' -f $Command, $TimingPath, $OutputPath) + } + + # Drain stderr so the probe's rejection message never surfaces to the user. + $null = $probeProcess.StandardError.ReadToEnd() + + if ($probeProcess.ExitCode -eq 0) { + Write-Verbose -Message 'script(1) accepts the positional typescript file; using the classic invocation.' + return ('-q -f -e -c "{0}" --log-timing "{1}" --log-out "{2}" /dev/null' -f $Command, $TimingPath, $OutputPath) + } + + Write-Verbose -Message ('script(1) rejected the positional typescript file (exit {0}); using the options-only form.' -f $probeProcess.ExitCode) + return ('-q -f -e -c "{0}" --log-timing "{1}" --log-out "{2}"' -f $Command, $TimingPath, $OutputPath) + } + finally { + Remove-Item -LiteralPath $probeTimingPath, $probeOutputPath -Force -ErrorAction 'SilentlyContinue' + } +} diff --git a/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 b/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 index daa45e8..cf5084a 100644 --- a/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 +++ b/src/Bsn.Host/Private/Start-BsnUnixPtyRecording.ps1 @@ -83,12 +83,10 @@ function Start-BsnUnixPtyRecording { $outputPath = [System.IO.Path]::GetTempFileName() $temporaryPaths = @($timingPath, $outputPath) - # No trailing positional typescript file: --log-out already captures - # the stream, and util-linux 2.41 (Ubuntu 26.04) rejects the - # combination of --log-out and a positional file with 'unexpected - # number of arguments', which made every recording come back empty. - # Older util-linux versions accept the options-only form too. - $processInfo.Arguments = ('-q -f -e -c "{0}" --log-timing "{1}" --log-out "{2}"' -f $recordedCommand, $timingPath, $outputPath) + # The invocation shape depends on the installed script(1): probe it + # (util-linux 2.39+/2.41 reject the positional typescript file that + # older releases accept - see Resolve-BsnScriptInvocation). + $processInfo.Arguments = Resolve-BsnScriptInvocation -ScriptPath $scriptTool.Source -Command $recordedCommand -TimingPath $timingPath -OutputPath $outputPath } else { $rawPath = [System.IO.Path]::GetTempFileName() diff --git a/src/Bsn.Host/tests/unit/Resolve-BsnScriptInvocation.Tests.ps1 b/src/Bsn.Host/tests/unit/Resolve-BsnScriptInvocation.Tests.ps1 new file mode 100644 index 0000000..1218352 --- /dev/null +++ b/src/Bsn.Host/tests/unit/Resolve-BsnScriptInvocation.Tests.ps1 @@ -0,0 +1,81 @@ +BeforeAll { + $moduleRoot = Split-Path -Parent -Path (Split-Path -Parent -Path $PSScriptRoot) + $manifestPath = Join-Path -Path $moduleRoot -ChildPath 'Bsn.Host.psd1' + # The module's DLL may already be loaded in this process (e.g., by PSScriptAnalyzer + # analyzing the manifest), which makes Import-Module fail with FileLoadException. + # Fall back to importing the psm1 directly - the assembly is already in memory. + try { + Import-Module -Name $manifestPath -Force -ErrorAction 'Stop' + } + 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; pull the enum value from the defining module's own scope + # (see AGENTS.md and the Start-InteractiveTranscript tests). + $script:BsnWindows = & (Get-Module -Name 'Bsn.Common') { [BsnOperatingSystem]::Windows } + + # Fake script(1) binaries are shell shims: they need a Unix host (chmod/exec). + if ((Get-BsnOperatingSystem) -ne $script:BsnWindows) { + $script:AcceptingShimPath = Join-Path -Path $TestDrive -ChildPath 'accepting-script' + Set-Content -LiteralPath $script:AcceptingShimPath -Value "#!/bin/sh`nexit 0" -NoNewline + & chmod +x $script:AcceptingShimPath + + $script:RejectingShimPath = Join-Path -Path $TestDrive -ChildPath 'rejecting-script' + Set-Content -LiteralPath $script:RejectingShimPath -Value "#!/bin/sh`nexit 1" -NoNewline + & chmod +x $script:RejectingShimPath + } +} + +Describe 'Resolve-BsnScriptInvocation' { + # https://github.com/BSVN/Lmt.Commons/issues/5 (Linux verification): + # util-linux 2.39+ rejects combining --log-out with a trailing positional + # typescript file, so the engine probes the installed binary and picks the + # invocation shape it accepts. The probe runs a no-op child ('-c "true"') + # through the shim, so the shim's exit code decides the branch. + It 'keeps the classic positional form when script(1) accepts it (util-linux <= 2.37)' { + if ((Get-BsnOperatingSystem) -eq $script:BsnWindows) { + Set-ItResult -Skipped -Because 'the fake script(1) shims are Unix shell executables; the probe is exercised end to end by the Unix system tests on Linux CI.' + } + + # Resolve-BsnScriptInvocation is private: invoke it through the + # defining module's scope (the same way the engine calls it). + $arguments = & (Get-Module -Name 'Bsn.Host') { + param($p, $c, $t, $o) + Resolve-BsnScriptInvocation -ScriptPath $p -Command $c -TimingPath $t -OutputPath $o + } $script:AcceptingShimPath 'echo MARKER' '/tmp/t.json' '/tmp/o.log' + + $arguments | Should -BeLike '*-c "echo MARKER" --log-timing "/tmp/t.json" --log-out "/tmp/o.log" /dev/null*' -Because 'a compatible binary must keep the historically tested positional form' + } + + It 'falls back to the options-only form when script(1) rejects the positional file (util-linux >= 2.39)' { + if ((Get-BsnOperatingSystem) -eq $script:BsnWindows) { + Set-ItResult -Skipped -Because 'the fake script(1) shims are Unix shell executables; the probe is exercised end to end by the Unix system tests on Linux CI.' + } + + $arguments = & (Get-Module -Name 'Bsn.Host') { + param($p, $c, $t, $o) + Resolve-BsnScriptInvocation -ScriptPath $p -Command $c -TimingPath $t -OutputPath $o + } $script:RejectingShimPath 'echo MARKER' '/tmp/t.json' '/tmp/o.log' + + $arguments | Should -BeLike '*-c "echo MARKER" --log-timing "/tmp/t.json" --log-out "/tmp/o.log"*' -Because 'a rejecting binary must get the options-only invocation' + $arguments | Should -Not -BeLike '*/dev/null*' -Because 'the positional typescript file is what util-linux >= 2.39 rejects' + } + + It 'includes the recorded command and both log paths in every invocation shape' { + if ((Get-BsnOperatingSystem) -eq $script:BsnWindows) { + Set-ItResult -Skipped -Because 'the fake script(1) shims are Unix shell executables; the probe is exercised end to end by the Unix system tests on Linux CI.' + } + + foreach ($shim in @($script:AcceptingShimPath, $script:RejectingShimPath)) { + $arguments = & (Get-Module -Name 'Bsn.Host') { + param($p, $c, $t, $o) + Resolve-BsnScriptInvocation -ScriptPath $p -Command $c -TimingPath $t -OutputPath $o + } $shim 'uname -s' '/tmp/t.json' '/tmp/o.log' + $arguments.Contains('uname -s') | Should -BeTrue -Because 'the recorded command must be part of the -c argument' + $arguments.Contains('/tmp/t.json') | Should -BeTrue -Because 'the timing log path must be passed to --log-timing' + $arguments.Contains('/tmp/o.log') | Should -BeTrue -Because 'the output log path must be passed to --log-out' + } + } +}