Skip to content

Commit 99c0150

Browse files
authored
Restructure Windows CLI integration into scenario matrix with missing shell/env/path/chaos coverage (#38526)
1 parent 52f3a07 commit 99c0150

1 file changed

Lines changed: 197 additions & 71 deletions

File tree

.github/workflows/windows-cli-integration.yml

Lines changed: 197 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -70,39 +70,210 @@ jobs:
7070
$size = (Get-Item $env:BINARY).Length
7171
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Binary found ($size bytes)"
7272
73-
# pwsh (PowerShell Core) - explicit per-command timeouts via WaitForExit
74-
- name: "[pwsh] --help with timeout"
73+
- name: Windows CLI scenario matrix (shell × launch-mode × env-shape × path-style)
7574
shell: pwsh
7675
run: |
77-
$proc = Start-Process -FilePath $env:BINARY -ArgumentList "--help" -PassThru -NoNewWindow
78-
if (-Not $proc.WaitForExit(30000)) {
79-
$proc.Kill()
80-
throw "TIMEOUT: --help hung for 30s in pwsh"
76+
Set-StrictMode -Version Latest
77+
$ErrorActionPreference = "Stop"
78+
79+
function Invoke-CliProcess {
80+
param(
81+
[Parameter(Mandatory)] [string] $FilePath,
82+
[string[]] $ArgumentList = @(),
83+
[int] $TimeoutMs = 30000,
84+
[string] $WorkingDirectory = (Get-Location).Path,
85+
[hashtable] $EnvironmentOverrides = @{},
86+
[string[]] $UnsetEnvironment = @(),
87+
[switch] $MinimalEnvironment,
88+
[switch] $ExpectFailure,
89+
[string] $ExpectedErrorPattern = "",
90+
[switch] $AssertNoAnsi
91+
)
92+
93+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
94+
$psi.FileName = $FilePath
95+
$psi.WorkingDirectory = $WorkingDirectory
96+
$psi.UseShellExecute = $false
97+
$psi.RedirectStandardOutput = $true
98+
$psi.RedirectStandardError = $true
99+
100+
foreach ($arg in $ArgumentList) {
101+
[void]$psi.ArgumentList.Add($arg)
102+
}
103+
104+
if ($MinimalEnvironment) {
105+
$psi.Environment.Clear()
106+
foreach ($key in @("PATH", "TEMP", "TMP", "SystemRoot")) {
107+
$value = [System.Environment]::GetEnvironmentVariable($key)
108+
if ($null -ne $value -and $value -ne "") {
109+
$psi.Environment[$key] = $value
110+
}
111+
}
112+
}
113+
114+
foreach ($key in $UnsetEnvironment) {
115+
[void]$psi.Environment.Remove($key)
116+
}
117+
foreach ($key in $EnvironmentOverrides.Keys) {
118+
$value = $EnvironmentOverrides[$key]
119+
if ($null -eq $value) {
120+
[void]$psi.Environment.Remove($key)
121+
} else {
122+
$psi.Environment[$key] = [string]$value
123+
}
124+
}
125+
126+
$proc = [System.Diagnostics.Process]::Start($psi)
127+
if (-not $proc.WaitForExit($TimeoutMs)) {
128+
try { $proc.Kill($true) } catch {}
129+
throw "TIMEOUT: '$FilePath $($ArgumentList -join ' ')' exceeded ${TimeoutMs}ms"
130+
}
131+
132+
$stdout = $proc.StandardOutput.ReadToEnd()
133+
$stderr = $proc.StandardError.ReadToEnd()
134+
$output = "$stdout`n$stderr"
135+
136+
if ($AssertNoAnsi -and $output -match "\x1B\[[0-9;]*[A-Za-z]") {
137+
throw "Unexpected ANSI escape sequences in output"
138+
}
139+
140+
if ($ExpectFailure) {
141+
if ($proc.ExitCode -eq 0) {
142+
throw "Expected failure but command exited 0"
143+
}
144+
if ($ExpectedErrorPattern -and $output -notmatch $ExpectedErrorPattern) {
145+
throw "Expected error pattern '$ExpectedErrorPattern' not found"
146+
}
147+
} elseif ($proc.ExitCode -ne 0) {
148+
throw "Exit code $($proc.ExitCode) for '$FilePath $($ArgumentList -join ' ')'`n$output"
149+
}
150+
151+
return @{
152+
ExitCode = $proc.ExitCode
153+
Output = $output
154+
}
81155
}
82-
if ($proc.ExitCode -ne 0) { throw "Exit code $($proc.ExitCode)" }
83-
Add-Content $env:GITHUB_STEP_SUMMARY "✅ [pwsh] --help OK"
84156
85-
- name: "[pwsh] version with timeout"
86-
shell: pwsh
87-
run: |
88-
$proc = Start-Process -FilePath $env:BINARY -ArgumentList "version" -PassThru -NoNewWindow
89-
if (-Not $proc.WaitForExit(30000)) {
90-
$proc.Kill()
91-
throw "TIMEOUT: version hung for 30s in pwsh"
157+
$workspace = $env:GITHUB_WORKSPACE
158+
$binary = $env:BINARY
159+
$originalPath = $env:PATH
160+
$toolcacheDir = Join-Path $env:RUNNER_TEMP "toolcache-like"
161+
$spaceDir = Join-Path $env:RUNNER_TEMP "path with spaces"
162+
$parenDir = Join-Path $env:RUNNER_TEMP "Program Files (x86)"
163+
$unicodeDir = Join-Path $env:TEMP "tëst-dïr-αβγ"
164+
foreach ($dir in @($toolcacheDir, $spaceDir, $parenDir, $unicodeDir)) {
165+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
92166
}
93-
if ($proc.ExitCode -ne 0) { throw "Exit code $($proc.ExitCode)" }
94-
Add-Content $env:GITHUB_STEP_SUMMARY "✅ [pwsh] version OK"
167+
Copy-Item -Path $binary -Destination (Join-Path $spaceDir "gh-aw.exe") -Force
168+
Copy-Item -Path $binary -Destination (Join-Path $parenDir "gh-aw.exe") -Force
95169
96-
- name: "[pwsh] compile --help with timeout"
97-
shell: pwsh
98-
run: |
99-
$proc = Start-Process -FilePath $env:BINARY -ArgumentList "compile","--help" -PassThru -NoNewWindow
100-
if (-Not $proc.WaitForExit(30000)) {
101-
$proc.Kill()
102-
throw "TIMEOUT: compile --help hung for 30s in pwsh"
170+
$scenarioMatrix = @(
171+
@{ name = "pwsh-default/direct/default/default"; shell = "pwsh-default"; launch = "direct"; env = "default"; path = "default" },
172+
@{ name = "pwsh-noprofile/direct/default/default"; shell = "pwsh-noprofile"; launch = "direct"; env = "default"; path = "default" },
173+
@{ name = "powershell-default/direct/default/default"; shell = "powershell-default"; launch = "direct"; env = "default"; path = "default" },
174+
@{ name = "powershell-noprofile/direct/default/default"; shell = "powershell-noprofile"; launch = "direct"; env = "default"; path = "default" },
175+
@{ name = "pwsh-default/path/default/workspace-first"; shell = "pwsh-default"; launch = "path"; env = "default"; path = "workspace-first" },
176+
@{ name = "cmd/path/default/toolcache-first"; shell = "cmd"; launch = "path"; env = "default"; path = "toolcache-first" },
177+
@{ name = "pwsh-default/path/default/duplicate-workspace"; shell = "pwsh-default"; launch = "path"; env = "default"; path = "duplicate-workspace" },
178+
@{ name = "pwsh-default/path/no-color/workspace-first"; shell = "pwsh-default"; launch = "path"; env = "no-color"; path = "workspace-first" },
179+
@{ name = "pwsh-default/path/term-dumb/workspace-first"; shell = "pwsh-default"; launch = "path"; env = "term-dumb"; path = "workspace-first" },
180+
@{ name = "pwsh-default/path/term-unset/workspace-first"; shell = "pwsh-default"; launch = "path"; env = "term-unset"; path = "workspace-first" },
181+
@{ name = "pwsh-default/path/ci-true/workspace-first"; shell = "pwsh-default"; launch = "path"; env = "ci-true"; path = "workspace-first" },
182+
@{ name = "pwsh-default/path/ci-unset/workspace-first"; shell = "pwsh-default"; launch = "path"; env = "ci-unset"; path = "workspace-first" }
183+
)
184+
185+
foreach ($scenario in $scenarioMatrix) {
186+
$env:PATH = $originalPath
187+
switch ($scenario.path) {
188+
"workspace-first" { $env:PATH = "$workspace;$originalPath" }
189+
"toolcache-first" { $env:PATH = "$toolcacheDir;$workspace;$originalPath" }
190+
"duplicate-workspace" { $env:PATH = "$workspace;$workspace;$originalPath" }
191+
default {}
192+
}
193+
194+
$envOverrides = @{}
195+
$unsetVars = @()
196+
$assertNoAnsi = $false
197+
switch ($scenario.env) {
198+
"no-color" { $envOverrides["NO_COLOR"] = "1" }
199+
"term-dumb" { $envOverrides["TERM"] = "dumb"; $assertNoAnsi = $true }
200+
"term-unset" { $unsetVars += "TERM" }
201+
"ci-true" { $envOverrides["CI"] = "true" }
202+
"ci-unset" { $unsetVars += "CI" }
203+
default {}
204+
}
205+
206+
if ($scenario.launch -eq "path") {
207+
$whereResult = Invoke-CliProcess -FilePath "cmd" -ArgumentList @("/d", "/s", "/c", "where gh-aw") -EnvironmentOverrides $envOverrides -UnsetEnvironment $unsetVars
208+
$firstResolved = ($whereResult.Output -split "`r?`n" | Where-Object { $_ -match "gh-aw\.exe$" } | Select-Object -First 1)
209+
if (-not $firstResolved -or -not $firstResolved.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) {
210+
throw "PATH resolution mismatch for '$($scenario.name)': $firstResolved"
211+
}
212+
}
213+
214+
switch ($scenario.shell) {
215+
"pwsh-default" {
216+
$cmd = if ($scenario.launch -eq "path") { "& gh-aw --help" } else { "& '$binary' --help" }
217+
Invoke-CliProcess -FilePath "pwsh" -ArgumentList @("-Command", $cmd) -EnvironmentOverrides $envOverrides -UnsetEnvironment $unsetVars -AssertNoAnsi:$assertNoAnsi | Out-Null
218+
}
219+
"pwsh-noprofile" {
220+
$cmd = if ($scenario.launch -eq "path") { "& gh-aw --help" } else { "& '$binary' --help" }
221+
Invoke-CliProcess -FilePath "pwsh" -ArgumentList @("-NoProfile", "-Command", $cmd) -EnvironmentOverrides $envOverrides -UnsetEnvironment $unsetVars -AssertNoAnsi:$assertNoAnsi | Out-Null
222+
}
223+
"powershell-default" {
224+
$cmd = if ($scenario.launch -eq "path") { "& gh-aw --help" } else { "& '$binary' --help" }
225+
Invoke-CliProcess -FilePath "powershell" -ArgumentList @("-Command", $cmd) -EnvironmentOverrides $envOverrides -UnsetEnvironment $unsetVars -AssertNoAnsi:$assertNoAnsi | Out-Null
226+
}
227+
"powershell-noprofile" {
228+
$cmd = if ($scenario.launch -eq "path") { "& gh-aw --help" } else { "& '$binary' --help" }
229+
Invoke-CliProcess -FilePath "powershell" -ArgumentList @("-NoProfile", "-Command", $cmd) -EnvironmentOverrides $envOverrides -UnsetEnvironment $unsetVars -AssertNoAnsi:$assertNoAnsi | Out-Null
230+
}
231+
"cmd" {
232+
$cmd = if ($scenario.launch -eq "path") { "gh-aw --help" } else { """$binary"" --help" }
233+
Invoke-CliProcess -FilePath "cmd" -ArgumentList @("/d", "/s", "/c", $cmd) -EnvironmentOverrides $envOverrides -UnsetEnvironment $unsetVars -AssertNoAnsi:$assertNoAnsi | Out-Null
234+
}
235+
default {
236+
throw "Unknown shell scenario: $($scenario.shell)"
237+
}
238+
}
239+
240+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Matrix scenario passed: $($scenario.name)"
241+
}
242+
243+
if ($env:PATHEXT -notmatch "(?i)\.EXE") {
244+
throw "PATHEXT missing .EXE"
245+
}
246+
Invoke-CliProcess -FilePath "pwsh" -ArgumentList @("-NoProfile", "-Command", "& gh-aw --help") | Out-Null
247+
Invoke-CliProcess -FilePath "cmd" -ArgumentList @("/d", "/s", "/c", "gh-aw --help") | Out-Null
248+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ PATHEXT/extension resolution checks passed from pwsh and cmd"
249+
250+
$spaceBinary = Join-Path $spaceDir "gh-aw.exe"
251+
$spaceBinaryForwardSlashes = $spaceBinary -replace "\\", "/"
252+
$parenBinary = Join-Path $parenDir "gh-aw.exe"
253+
Invoke-CliProcess -FilePath $spaceBinary -ArgumentList @("--help") | Out-Null
254+
Invoke-CliProcess -FilePath $spaceBinaryForwardSlashes -ArgumentList @("--help") | Out-Null
255+
Invoke-CliProcess -FilePath $parenBinary -ArgumentList @("--help") | Out-Null
256+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Paths with spaces, mixed slashes, and parentheses passed"
257+
258+
Invoke-CliProcess -FilePath $binary -ArgumentList @("--help") -WorkingDirectory $unicodeDir | Out-Null
259+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Unicode working directory check passed ($unicodeDir)"
260+
261+
Invoke-CliProcess -FilePath $binary -ArgumentList @("--help") -MinimalEnvironment | Out-Null
262+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Minimal environment execution passed"
263+
264+
foreach ($command in @(
265+
@{ name = "--help"; args = @("--help") },
266+
@{ name = "version"; args = @("version") },
267+
@{ name = "compile --help"; args = @("compile", "--help") },
268+
@{ name = "help"; args = @("help") },
269+
@{ name = "run --help"; args = @("run", "--help") }
270+
)) {
271+
Invoke-CliProcess -FilePath $binary -ArgumentList $command.args | Out-Null
272+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Command variant passed: $($command.name)"
103273
}
104-
if ($proc.ExitCode -ne 0) { throw "Exit code $($proc.ExitCode)" }
105-
Add-Content $env:GITHUB_STEP_SUMMARY "✅ [pwsh] compile --help OK"
274+
275+
Invoke-CliProcess -FilePath $binary -ArgumentList @("totally-unknown-subcommand-xyz") -TimeoutMs 10000 -ExpectFailure -ExpectedErrorPattern "unknown|not found|invalid|unrecognized" | Out-Null
276+
Add-Content $env:GITHUB_STEP_SUMMARY "✅ Unknown subcommand fails fast with explicit error output"
106277
107278
# Explicit check for stdin-blocking hang (common bubbletea/TUI regression)
108279
- name: "[pwsh] stdin hang detection"
@@ -119,51 +290,6 @@ jobs:
119290
}
120291
Add-Content $env:GITHUB_STEP_SUMMARY "✅ [pwsh] No stdin hang detected"
121292
122-
# Windows PowerShell (legacy powershell.exe) - catches PS 5.x-specific regressions
123-
- name: "[powershell] --help with timeout"
124-
shell: powershell
125-
run: |
126-
$psi = New-Object System.Diagnostics.ProcessStartInfo
127-
$psi.FileName = $env:BINARY
128-
$psi.Arguments = "--help"
129-
$psi.UseShellExecute = $false
130-
$proc = [System.Diagnostics.Process]::Start($psi)
131-
if (-Not $proc.WaitForExit(30000)) {
132-
$proc.Kill()
133-
throw "TIMEOUT: --help hung for 30s in Windows PowerShell"
134-
}
135-
if ($proc.ExitCode -ne 0) { throw "Exit code $($proc.ExitCode)" }
136-
Write-Output "[powershell] --help OK"
137-
138-
- name: "[powershell] version with timeout"
139-
shell: powershell
140-
run: |
141-
$psi = New-Object System.Diagnostics.ProcessStartInfo
142-
$psi.FileName = $env:BINARY
143-
$psi.Arguments = "version"
144-
$psi.UseShellExecute = $false
145-
$proc = [System.Diagnostics.Process]::Start($psi)
146-
if (-Not $proc.WaitForExit(30000)) {
147-
$proc.Kill()
148-
throw "TIMEOUT: version hung for 30s in Windows PowerShell"
149-
}
150-
if ($proc.ExitCode -ne 0) { throw "Exit code $($proc.ExitCode)" }
151-
Write-Output "[powershell] version OK"
152-
153-
# cmd.exe - catches Win32 console and PATH resolution regressions
154-
# Per-command timeout not available in cmd; job-level timeout-minutes: 20 applies
155-
- name: "[cmd] --help"
156-
shell: cmd
157-
run: |
158-
"%BINARY%" --help
159-
if errorlevel 1 exit /b 1
160-
161-
- name: "[cmd] version"
162-
shell: cmd
163-
run: |
164-
"%BINARY%" version
165-
if errorlevel 1 exit /b 1
166-
167293
- name: Integration summary
168294
if: always()
169295
shell: pwsh

0 commit comments

Comments
 (0)