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
210234Write-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).'
220248if ($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+
227276Write-Host ' Exiting via the tray window (WM_COMMAND / ID_TRAY_EXIT)...'
228277$tray = [ClippSmoke ]::FindWindowByClass($TRAY_CLASS )
229278if ($tray -eq [IntPtr ]::Zero) {
0 commit comments