fix(update): Windows progress server hands out its URL only once it is serving

`Start-UiServer` printed the -SelfTestUi URL (and opened the browser window)
as soon as the TcpListener was bound, but the runspace that answers /progress
starts asynchronously — BeginInvoke returns before the pipeline is open and
the script block is JIT'd, which is seconds on a loaded runner. The kernel
accepted connections into the backlog during that gap and nobody answered
them. The self-test hit it three times (#90371 and two follow-ups each
widened a timeout instead of removing the race) and it just failed an
unrelated hermes_state.py PR (run 33591547099, two 5s stale-backlog
timeouts = red).

- windows.ps1: readiness handshake after BeginInvoke — one /progress
  round-trip must succeed (≤15s) before the server is returned; on failure
  tear the listener down and continue without UI. The URL now means
  "serving", not "bound". Also fixes the browser opening to a page that never
  loads on a slow machine.
- test: 1s per-attempt probe timeout so a single dead backlog socket cannot
  consume half the readiness budget.
- CI: new `desktop_updater` classifier lane. tests/test_desktop_update_windows_*.py
  spawn the real PowerShell script; the Windows-only job now runs them only
  when scripts/desktop-update/**, the Electron updater launcher, conftest,
  pyproject, or those tests change (push/dispatch fail open). A PR that
  never touched that surface cannot be failed by its process timing.
This commit is contained in:
Teknium
2026-09-01 21:58:50 -07:00
parent 2b7132c8e5
commit bfbb34bbec
7 changed files with 132 additions and 5 deletions
@@ -48,6 +48,9 @@ outputs:
installer:
description: Run the PowerShell installer tests on a Windows runner.
value: ${{ steps.classify.outputs.installer }}
desktop_updater:
description: Run the Windows desktop-update hand-off (windows.ps1) integration tests.
value: ${{ steps.classify.outputs.desktop_updater }}
rust:
description: Run `cargo test` for the Tauri bootstrap installer.
value: ${{ steps.classify.outputs.rust }}
+6
View File
@@ -49,6 +49,7 @@ jobs:
uv_lock: ${{ steps.classify.outputs.uv_lock }}
npm_lock: ${{ steps.classify.outputs.npm_lock }}
installer: ${{ steps.classify.outputs.installer }}
desktop_updater: ${{ steps.classify.outputs.desktop_updater }}
rust: ${{ steps.classify.outputs.rust }}
docker_meta: ${{ steps.classify.outputs.docker_meta }}
mcp_catalog: ${{ steps.classify.outputs.mcp_catalog }}
@@ -84,6 +85,11 @@ jobs:
needs: detect
if: needs.detect.outputs.python == 'true'
uses: ./.github/workflows/tests-os.yml
with:
# The Windows lane spawns the real desktop-update hand-off script
# (tests/test_desktop_update_windows_*.py) only when that surface
# changed; unit-level windows_only tests always run.
desktop_updater: ${{ needs.detect.outputs.desktop_updater == 'true' }}
lint:
name: Python lints
+27
View File
@@ -27,6 +27,19 @@ name: OS-specific tests
on:
workflow_call:
inputs:
desktop_updater:
description: >-
Run the Windows desktop-update hand-off integration tests
(tests/test_desktop_update_windows_*.py). These spawn the real
scripts/desktop-update/windows.ps1 and poll its loopback server, so
they carry process-timing noise a shared runner amplifies; the
caller gates them on the classifier's desktop_updater lane so a PR
that never touched that surface cannot be failed by it. Push /
dispatch runs fail open (classifier sets every lane true).
type: boolean
required: false
default: true
permissions:
contents: read
@@ -134,9 +147,23 @@ jobs:
# would therefore abort the script on any non-zero exit and the
# exit-5 branch below would be unreachable dead code — the job
# would still fail red, but the diagnostic would never print.
# Desktop-update hand-off integration tests spawn the real
# windows.ps1; deselect them unless the PR touched that surface
# (see the workflow_call input). ``--ignore-glob`` keeps the file
# list above intact, so a renamed test file still trips the
# zero-tests guard rather than silently vanishing.
# (bash 3.2 on the macOS runner: an empty array under ``set -u`` is
# an unbound-variable error, hence the ``${arr[@]+...}`` idiom.)
EXTRA_ARGS=()
if [ "${{ inputs.desktop_updater }}" != "true" ]; then
echo "desktop_updater lane off: skipping tests/test_desktop_update_windows_*.py"
EXTRA_ARGS+=(--ignore-glob='*test_desktop_update_windows_*.py')
fi
status=0
uv run --no-sync python -m pytest \
"$@" \
${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} \
-m "${{ matrix.marker }} and not integration" \
-v --tb=short || status=$?
if [ "$status" -eq 5 ]; then