Skip to content

Commit 7250c9b

Browse files
committed
test: app exit code in ci
1 parent df6f0c6 commit 7250c9b

2 files changed

Lines changed: 67 additions & 10 deletions

File tree

.github/workflows/windows-ci.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,17 @@ jobs:
213213
# must open its first-run XAML-Islands window — the exact path a validation VM takes,
214214
# and the one a developer box with a configured key never exercises. If the window
215215
# never appears, the island failed to come up: that is a failure, not a shrug.
216-
- name: Launch/exit smoke test (signed, uiAccess-stamped exe)
216+
# Graceful path: tray-menu Exit runs main()'s full teardown and must return 0.
217+
- name: Smoke test — graceful exit (signed, uiAccess-stamped exe)
217218
if: steps.cansign.outputs.ok == 'true'
218-
run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow
219+
run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow -CloseMode TrayExit
220+
221+
# Harness path: close the main window like Process.CloseMainWindow, then verify the
222+
# app stays resident instead of self-terminating. A 0xC000027B here IS the winget
223+
# validation failure reproduced — this is the leg that can actually catch it.
224+
- name: Smoke test — winget harness sequence
225+
if: steps.cansign.outputs.ok == 'true'
226+
run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow -CloseMode Harness
219227

220228
- name: Collect crash diagnostics
221229
if: failure() && steps.cansign.outputs.ok == 'true'

scripts/exitcode_smoke.ps1

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
# Sequence, mirroring the harness: launch the exe with NO arguments; wait for the main XAML-
1515
# Islands window (on a clean profile there is no group key, so Clipp opens it by itself —
1616
# the same first-run path a validation VM takes, and the riskiest surface in the app); close
17-
# it with WM_CLOSE; exit through the tray window (WM_COMMAND/ID_TRAY_EXIT, the full teardown
18-
# path); assert exit code 0.
17+
# it with WM_CLOSE (which HIDES it — Clipp is a tray app and keeps the HWND for reuse, so
18+
# this waits for the window to stop SHOWING, not to be destroyed); then exit through the
19+
# tray window (WM_COMMAND/ID_TRAY_EXIT, the full teardown path); assert exit code 0.
1920
#
2021
# NOTE on the first-run window: it appears only when no group key is configured. On a
2122
# developer box that HAS one, Clipp starts to the tray with no window at all and this script
@@ -37,7 +38,20 @@ param(
3738
[string] $DumpDir = $env:WER_DUMP_DIR,
3839
[int] $WindowTimeoutSec = 90,
3940
[int] $ExitTimeoutSec = 60,
40-
[switch] $RequireWindow
41+
[switch] $RequireWindow,
42+
43+
# How to end the run:
44+
# TrayExit — the graceful path a user takes: tray menu Exit (WM_COMMAND/ID_TRAY_EXIT)
45+
# -> WM_CLOSE -> WM_DESTROY -> PostQuitMessage -> main()'s full teardown
46+
# (network stop, register-persistence flush, WSACleanup) -> exit 0.
47+
# Asserts a clean exit code.
48+
# Harness — what winget's validation actually does: close the main window the way
49+
# .NET's Process.CloseMainWindow() does, observe that the app correctly
50+
# stays resident (it is a tray app), then force-kill it. The assertion
51+
# here is INVERTED: the app must NOT die on its own. A self-termination
52+
# with 0xC000027B is the winget failure, reproduced.
53+
[ValidateSet('TrayExit', 'Harness')]
54+
[string] $CloseMode = 'TrayExit'
4155
)
4256

4357
$ErrorActionPreference = 'Stop'
@@ -66,6 +80,16 @@ public static class ClippSmoke
6680
[DllImport("user32.dll")]
6781
public static extern bool IsWindow(IntPtr hWnd);
6882
83+
[DllImport("user32.dll", EntryPoint = "IsWindowVisible")]
84+
static extern bool IsWindowVisibleNative(IntPtr hWnd);
85+
86+
// Clipp HIDES its main window on close and keeps the HWND for reuse (it is a tray
87+
// app), so "closed" means gone-or-hidden, never destroyed.
88+
public static bool IsWindowShowing(IntPtr hWnd)
89+
{
90+
return IsWindow(hWnd) && IsWindowVisibleNative(hWnd);
91+
}
92+
6993
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
7094
[DllImport("user32.dll")]
7195
static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
@@ -209,21 +233,46 @@ Start-Sleep -Seconds 5
209233

210234
Write-Host 'Closing the main window (WM_CLOSE)...'
211235
[void][ClippSmoke]::PostMessage($dialog, $WM_CLOSE, [UIntPtr]::Zero, [IntPtr]::Zero)
236+
# Success is the window no longer SHOWING. Clipp hides it and keeps the HWND alive for the
237+
# next open, so IsWindow() stays true forever and must not be the condition — the island's
238+
# teardown-on-hide is what we are exercising here, not window destruction.
212239
$deadline = (Get-Date).AddSeconds(15)
213-
while ((Get-Date) -lt $deadline -and [ClippSmoke]::IsWindow($dialog)) {
240+
while ((Get-Date) -lt $deadline -and [ClippSmoke]::IsWindowShowing($dialog)) {
214241
Start-Sleep -Milliseconds 250
215242
}
216-
if ([ClippSmoke]::IsWindow($dialog)) {
217-
Invoke-HangAutopsy $p 'main-window-wont-close'
218-
throw 'Main window did not close on WM_CLOSE.'
243+
if ([ClippSmoke]::IsWindowShowing($dialog)) {
244+
Invoke-HangAutopsy $p 'main-window-wont-hide'
245+
throw 'Main window did not hide on WM_CLOSE.'
219246
}
247+
Write-Host 'Main window closed (hidden; app stays resident, as designed).'
220248
if ($p.HasExited) {
221-
# Closing the window should leave the app alive in the tray; dying here is itself a bug.
249+
# Closing the window must leave the app alive in the tray; dying here is itself a bug.
222250
Write-Host 'App exited on WM_CLOSE of the main window (expected: stays resident in tray).'
223251
Assert-CleanExit $p.ExitCode
224252
throw 'FAIL: the app terminated when its main window closed; it should remain in the tray.'
225253
}
226254

255+
if ($CloseMode -eq 'Harness') {
256+
# Replay the validation harness: the main window is already closed (above, the same
257+
# WM_CLOSE that .NET's Process.CloseMainWindow posts). A tray app is SUPPOSED to keep
258+
# running. Give it room to die of its own accord — that self-termination, not our kill,
259+
# is what produced "App Clipp returned exit code: -1073741189".
260+
Write-Host "Harness mode: watching for ${ExitTimeoutSec}s to see if the app self-terminates..."
261+
if ($p.WaitForExit($ExitTimeoutSec * 1000)) {
262+
Write-Host ("App exited ON ITS OWN after its window was closed: {0} (0x{1:X8})" -f $p.ExitCode, $p.ExitCode)
263+
if ($p.ExitCode -eq -1073741189) {
264+
throw 'FAIL: exit code 0xC000027B (stowed exception) after window close — the winget validation failure, REPRODUCED.'
265+
}
266+
throw ("FAIL: app self-terminated ({0} / 0x{1:X8}); a tray app must stay resident after its window closes." -f $p.ExitCode, $p.ExitCode)
267+
}
268+
Write-Host 'Still resident, as designed. Force-killing the way the harness does...'
269+
Stop-Process -Id $p.Id -Force
270+
$p.WaitForExit(15000) | Out-Null
271+
Write-Host ("Exit code after force-kill: {0} (0x{1:X8}) — not meaningful; the kill sets it." -f $p.ExitCode, $p.ExitCode)
272+
Write-Host 'PASS: survived the harness sequence without self-terminating.'
273+
return
274+
}
275+
227276
Write-Host 'Exiting via the tray window (WM_COMMAND / ID_TRAY_EXIT)...'
228277
$tray = [ClippSmoke]::FindWindowByClass($TRAY_CLASS)
229278
if ($tray -eq [IntPtr]::Zero) {

0 commit comments

Comments
 (0)