Skip to content

test(windows): isolate the daemon guards' rendezvous and stop hiding their failures - #2026

Merged
DeusData merged 1 commit into
mainfrom
fix/windows-guard-daemon-isolation
Sep 4, 2026
Merged

test(windows): isolate the daemon guards' rendezvous and stop hiding their failures#2026
DeusData merged 1 commit into
mainfrom
fix/windows-guard-daemon-isolation

Conversation

@DeusData

@DeusData DeusData commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What

The Windows daemon guards shared one machine-global daemon, and their failure output hid why anything went red. This isolates test_daemon_stability.py per section via CBM_RUNTIME_DIR and makes every failure path print what actually happened.

Why — the false premise

test_daemon_stability.py documented that "each section runs under its OWN cache directory: daemon coordination is cache-scoped". It is not:

  • the rendezvous key is FNV-1a over a compile-time domain string (service.c: cbm_daemon_rendezvous_key);
  • the parent is win_default_runtime_parent()SHGetFolderPathW(CSIDL_LOCAL_APPDATA), which ignores the LOCALAPPDATA variable (so even test_non_ascii_path.py's redirect does not relocate it);
  • CBM_CACHE_DIR moves the cache, never the rendezvous (bootstrap.c says exactly this).

So the guard's own daemon stop / kill -9 sections, the sibling guards, and anything else on the runner met the same daemon, and a daemon wedged anywhere failed every check after it. That is the shape of this week's test-windows-guards red on #1874: lifecycle's daemon start connected, the UI-config handshake never answered, then stability / hook_augment / ui_drive / cli_non_ascii went red in a row with an orphan daemon killed at cleanup — on a diff and a base that were each green elsewhere.

CBM_RUNTIME_DIR is the only relocation hook (resolved in cbm_daemon_bootstrap_endpoint_new for every product endpoint). Each section now gets a private runtime parent next to its cache, created before the binary runs (the endpoint validates the parent and never creates it); the daemon inherits it from the client that spawns it.

The reporting fixes

Before After
SETUP FAIL printed nothing prints the daemon's full stdout+stderr
every RED printed a 300–400 char head slice (= the allocator preamble) tail excerpt, 1500 chars
a timeout on a parallel-client thread was lost → rc=none, no output kept as a failed result with partial output; a section-level timeout is a named RED with command + output
MCP session stderr → DEVNULL → file, printed when initialize never answers — and that is a RED against a running daemon, not a setup skip
exit 2 documented, never produced; first failure aborted the run only daemon start failing yields 2 (test_daemon_lifecycle.py owns that surface as a regression, so the board loses nothing); every section runs and all failing ones are named
test_hook_augment.py / test_cli_non_ascii_arg.py: 300 chars of stdout on setup failure full stdout and stderr

Verification

  • Real Windows (UTM arm64 VM): win.sh guards — all guards GREEN, stability all 7 sections, guards rc=0.
  • macOS (local iteration): guard GREEN in 1:35; isolation proof — a daemon started on the default rendezvous kept its pid (41915) across the whole guard, i.e. through three daemon stops and a kill -9 inside it.
  • The failure formatting was exercised for real: the first local run refused every section (see below) and the new output named all seven with full text and the setup/red split.

Out of scope, noted

…their failures

Every Windows guard shared ONE daemon. test_daemon_stability.py said each
section "runs under its OWN cache directory: daemon coordination is
cache-scoped" -- it is not. The rendezvous key is FNV-1a over a compile-time
domain string (service.c: cbm_daemon_rendezvous_key) under a machine-global
parent (win_default_runtime_parent -> SHGetFolderPathW(CSIDL_LOCAL_APPDATA),
which ignores the LOCALAPPDATA variable). So the guard's own `daemon stop`
and kill -9 sections, the sibling guards, and anything else on the runner
all met the same daemon, and a daemon wedged anywhere failed every check
after it. That is the shape of this week's test-windows-guards red on
#1874: lifecycle's `daemon start` connected, the UI-config handshake never
answered, and stability / hook_augment / ui_drive / cli_non_ascii went red
in a row with an orphan daemon killed at cleanup -- on a diff and a base that
were each green elsewhere.

CBM_RUNTIME_DIR is the only relocation hook (bootstrap.c, resolved in
cbm_daemon_bootstrap_endpoint_new for every product endpoint). Each stability
section now gets a private runtime parent next to its cache, created before
the binary runs -- the endpoint validates the parent and never creates it --
and the daemon inherits it from the client that spawns it. Proven on macOS:
a daemon on the default rendezvous keeps its pid across the whole guard
(three stops and a kill -9 inside it).

The reporting was hiding the cause as well:
* every SETUP FAIL printed nothing at all, and every RED printed a 300-400
  character HEAD slice, which is the allocator preamble, never the error.
  Excerpts now show the tail (1500 chars); setup paths print the daemon's
  full output.
* a subprocess timeout raised on a parallel-client worker thread was lost
  and the client reported as "rc=none" with no output; it is now kept as a
  failed result with its partial output. A section-level timeout is a
  named RED with the command and output, not a bare traceback.
* the MCP session's stderr went to DEVNULL; it now goes to a file that is
  printed when initialize never answers -- and that case is a RED against a
  running daemon, not a "setup" skip.
* exit 2 was documented and never produced. Only `daemon start` failing
  raises it now (test_daemon_lifecycle.py owns that surface as a
  regression, so the board loses nothing); main() runs every section and
  names all of the failing ones.
* test_hook_augment.py / test_cli_non_ascii_arg.py print full stdout AND
  stderr on their index-setup failures instead of 300 chars of stdout.

On POSIX (local iteration only) the namespace lives under /tmp: sun_path
caps the socket at 104 bytes on macOS and TMPDIR there is ~50 bytes deep.

Out of scope, noted: test_daemon_lifecycle.py, test_non_ascii_path.py and
test_ui_drive_listing.py still use the machine-global rendezvous; a too-long
CBM_RUNTIME_DIR on POSIX is refused (ipc.c sun_path check) with no
validation detail.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData added bug Something isn't working github_actions Pull requests that update GitHub Actions code windows Windows-specific issues priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Sep 3, 2026
@DeusData
DeusData merged commit 44fa3ae into main Sep 4, 2026
61 of 63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working github_actions Pull requests that update GitHub Actions code priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. windows Windows-specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant