From 81a8a03c820cb8bccafd0ebac355eede21b5e8bf Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Sun, 13 Sep 2026 13:52:00 +0300 Subject: [PATCH] fix(test): isolate soak harness daemon runtime scripts/soak-test.sh claimed to isolate daemon coordination from interactive sessions through a private CBM_CACHE_DIR, but only CBM_RUNTIME_DIR moves the daemon rendezvous, so the soak shared the operator's account daemon and asserted that its own shutdown stopped it. Source scripts/test-runtime.sh for a harness-owned runtime and cache, and let the helper stop the private daemon before the root is removed. Add tests/test_soak_runtime_isolation_contract.sh, which fails before this change, and wire it into scripts/test.sh. Part of #1696. Signed-off-by: Anton Standrik --- scripts/soak-test.sh | 27 ++++-- scripts/test.sh | 3 + tests/test_soak_runtime_isolation_contract.sh | 82 +++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) create mode 100755 tests/test_soak_runtime_isolation_contract.sh diff --git a/scripts/soak-test.sh b/scripts/soak-test.sh index a12029a30..31a16259f 100755 --- a/scripts/soak-test.sh +++ b/scripts/soak-test.sh @@ -66,9 +66,17 @@ CBM_SOAK_MODE="${CBM_SOAK_MODE:-default}" RESULTS_DIR="${RESULTS_DIR:-soak-results}" mkdir -p "$RESULTS_DIR" -# Isolate daemon coordination from interactive CBM sessions and give this run -# a deterministic host-side daemon log. Wine needs a Windows-form cache path -# in the child environment while this Bash harness retains the host path. +# Every product process below must reach a daemon rendezvous this run owns. +# Only CBM_RUNTIME_DIR moves that rendezvous — a private CBM_CACHE_DIR alone +# still shares the operator's account daemon (#1691, #1696). +# shellcheck source=test-runtime.sh +source "$(dirname "${BASH_SOURCE[0]}")/test-runtime.sh" +cbm_test_runtime_init +trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT + +# Give this run a deterministic host-side daemon log. Wine needs a Windows-form +# cache path in the child environment while this Bash harness retains the host +# path. # # On native Windows the cache CANNOT live under msys /tmp: the server's # cache-private executable-identity check walks the cache path's ancestors @@ -146,7 +154,7 @@ if [[ "$BINARY" == *.exe ]] && command -v cygpath >/dev/null 2>&1 && exit 1 fi else - SOAK_CACHE_DIR_HOST=$(mktemp -d "${TMPDIR:-/tmp}/cbm-soak-cache.XXXXXX") + SOAK_CACHE_DIR_HOST="$CBM_TEST_CACHE_DIR_HOST" fi SOAK_CACHE_DIR_VALUE="$SOAK_CACHE_DIR_HOST" if [[ "$BINARY" == *.exe ]] && command -v winepath >/dev/null 2>&1; then @@ -206,7 +214,12 @@ soak_cleanup() { if [ -f "$DAEMON_LOG" ]; then cp "$DAEMON_LOG" "$RESULTS_DIR/cbm-daemon.log" 2>/dev/null || true fi - rm -rf -- "$SOAK_PROJECT" "$SOAK_CACHE_DIR_HOST" + rm -rf -- "$SOAK_PROJECT" + # The helper stops this run's private daemon before removing its root and + # leaves the root behind for diagnosis when the daemon will not stop. The + # native-Windows cache sits under SOAK_WIN_ROOT, which goes only after the + # daemon check because the binary it probes with is the copy inside it. + cbm_test_runtime_cleanup "$BINARY" [ -z "${SOAK_WIN_ROOT:-}" ] || rm -rf -- "$SOAK_WIN_ROOT" } @@ -807,7 +820,7 @@ if [ "$SKIP_CRASH" != "--skip-crash-test" ] && [ "$CBM_SOAK_MODE" != "query-leak SERVER_PID="" exec 3>&- 4<&- if ! wait_for_daemon_stop "$DAEMON_STOP_COUNT"; then - echo "FAIL: last-session crash did not stop the shared daemon" + echo "FAIL: last-session crash did not stop the private daemon" exit 1 fi @@ -848,7 +861,7 @@ kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true SERVER_PID="" if ! wait_for_daemon_stop "$FINAL_DAEMON_STOP_COUNT"; then - echo "FAIL: final frontend shutdown did not stop the shared daemon" + echo "FAIL: final frontend shutdown did not stop the private daemon" PASS=false fi diff --git a/scripts/test.sh b/scripts/test.sh index ecfc26998..7263dd616 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -212,6 +212,9 @@ bash "$ROOT/tests/test_ui_dev_proxy_security.sh" echo "=== Step 0d: daemon soak recovery contract ===" bash "$ROOT/tests/test_soak_daemon_recovery_contract.sh" +echo "=== Step 0d2: soak harness runtime isolation contract (#1696) ===" +bash "$ROOT/tests/test_soak_runtime_isolation_contract.sh" + echo "=== Step 0e: Windows launcher bundle contract ===" bash "$ROOT/tests/test_windows_bundle_contract.sh" diff --git a/tests/test_soak_runtime_isolation_contract.sh b/tests/test_soak_runtime_isolation_contract.sh new file mode 100755 index 000000000..340df7f0a --- /dev/null +++ b/tests/test_soak_runtime_isolation_contract.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runtime-isolation contract for the soak harness (#1696, follow-up to #1691). +# +# scripts/soak-test.sh must be the only client of the daemon it measures: it +# asserts that a session crash and the final shutdown each stop "the daemon". +# It used to claim isolation from interactive sessions through a private +# CBM_CACHE_DIR, but only CBM_RUNTIME_DIR moves the daemon rendezvous +# (docs/CONFIGURATION.md), so the soak shared the operator's account daemon +# and either stopped it or was refused with a cache-root conflict. Drive the +# harness with an environment-probe fixture and require that no product +# process ever receives the caller's runtime or cache. + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +normalize_path() { + local path=${1%$'\r'} + if command -v cygpath >/dev/null 2>&1; then + cygpath -u "$path" 2>/dev/null && return 0 + fi + printf '%s\n' "${path//\\//}" +} + +ENV_PROBE="$WORKDIR/environment-probe" +cat > "$ENV_PROBE" <<'EOF' +#!/usr/bin/env bash +printf '%s\t%s\n' "${CBM_CACHE_DIR-}" "${CBM_RUNTIME_DIR-}" >> "$CBM_SOAK_ENV_PROBE" +[[ "${1-} ${2-}" == "daemon status" ]] && exit 1 +exit 0 +EOF +chmod +x "$ENV_PROBE" + +CALLER_CACHE="$WORKDIR/caller-cache" +CALLER_RUNTIME="$WORKDIR/caller-runtime" +ENV_LOG="$WORKDIR/environment.log" +mkdir -p "$CALLER_CACHE" "$CALLER_RUNTIME" + +# The fixture exits as soon as it has recorded its environment, so the soak +# fails at "server did not start"; only the environment it handed to the +# product is under test here. RESULTS_DIR keeps the soak's metrics out of cwd. +CBM_CACHE_DIR="$CALLER_CACHE" \ +CBM_RUNTIME_DIR="$CALLER_RUNTIME" \ +CBM_SOAK_ENV_PROBE="$ENV_LOG" \ +RESULTS_DIR="$WORKDIR/results" \ + "$ROOT/scripts/soak-test.sh" "$ENV_PROBE" 1 --skip-crash-test \ + > "$WORKDIR/soak.out" 2>&1 || true + +[[ -s "$ENV_LOG" ]] || fail "soak-test did not execute the environment-probe fixture" + +CALLER_CACHE_NORMALIZED=$(normalize_path "$CALLER_CACHE") +CALLER_RUNTIME_NORMALIZED=$(normalize_path "$CALLER_RUNTIME") +private_root="" +while IFS=$'\t' read -r child_cache_raw child_runtime_raw; do + child_cache=$(normalize_path "$child_cache_raw") + child_runtime=$(normalize_path "$child_runtime_raw") + if [[ -z "$child_runtime" || "$child_runtime" == "$CALLER_RUNTIME_NORMALIZED" ]]; then + fail "soak-test exposed the caller CBM_RUNTIME_DIR to a product process" + fi + if [[ -z "$child_cache" || "$child_cache" == "$CALLER_CACHE_NORMALIZED" ]]; then + fail "soak-test exposed the caller CBM_CACHE_DIR to a product process" + fi + if [[ "${child_runtime%/*}" != "${child_cache%/*}" || + "${child_runtime##*/}" != "runtime" || "${child_cache##*/}" != "cache" ]]; then + fail "soak runtime/cache were not isolated beneath one private root" + fi + if [[ -n "$private_root" && "$private_root" != "${child_runtime%/*}" ]]; then + fail "soak-test switched private roots mid-run" + fi + private_root="${child_runtime%/*}" +done < "$ENV_LOG" + +[[ ! -e "$private_root" ]] || fail "soak-test left its private root behind: $private_root" + +echo "PASS: soak harness isolates its daemon runtime and cache from the caller"