diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index ca27edcc6..31a69b9f0 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -22,6 +22,8 @@ they stage the release fixture, start the fixture server, and sandbox HOME/TEMP/agent-config destinations. Called bare, the download/checksum/ install-script phases (12-13) SKIP for lack of a fixture server, and the run mutates the REAL profile — the venue-parity contract forbids that in any venue. +The daemon runtime and cache are private to the run either way: every product +process is started under a CBM_RUNTIME_DIR/CBM_CACHE_DIR this harness owns. Arguments: product binary to smoke @@ -41,6 +43,15 @@ if [ -n "$SMOKE_MODE" ] && [ "$SMOKE_MODE" != "--agent-config-only" ]; then fi REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" +# Every product process below — the phases, the install/update E2E and the +# daemon retirements — must reach a daemon rendezvous this run owns. Only +# CBM_RUNTIME_DIR moves that rendezvous; the wrappers' HOME/TMPDIR/CBM_CACHE_DIR +# sandbox does not, so without this the retirements land on the operator's live +# account daemon (#1691, #1696). +# shellcheck source=test-runtime.sh +source "$REPO_ROOT/scripts/test-runtime.sh" +cbm_test_runtime_init + smoke_mktemp_file() { if [ -n "${SMOKE_TEMP_ROOT:-}" ]; then mktemp "$SMOKE_TEMP_ROOT/cbm-smoke.XXXXXX" @@ -100,8 +111,8 @@ copy_smoke_binary() { cp "$BINARY" "$destination" } -# Retire the shared account daemon (if one is running) and wait until it -# reports not-running. Install/uninstall flows leave an ephemeral daemon +# Retire this run's private account daemon (if one is running) and wait until +# it reports not-running. Install/uninstall flows leave an ephemeral daemon # draining asynchronously whose mapped generation backing and open logs # block rm on Windows (POSIX rm doesn't care) — so every cleanup of a # fixture HOME that received an install, and the final cache removal, must @@ -149,7 +160,7 @@ CODEX_LIFECYCLE_HOME="" if command -v cygpath &>/dev/null; then TMPDIR=$(cygpath -m "$TMPDIR") fi -trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT +trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"; cbm_test_runtime_cleanup "$BINARY"' EXIT CLI_STDERR=$(smoke_mktemp_file) # 10 of the cli call sites assign directly (VAR=$(cli ...)). Under diff --git a/scripts/test.sh b/scripts/test.sh index ecfc26998..a20a8526b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -266,6 +266,9 @@ bash "$ROOT/tests/test_release_gate_chain_contract.sh" echo "=== Step 0t: test runtime isolation contract (#1691) ===" bash "$ROOT/tests/test_runtime_isolation_contract.sh" +echo "=== Step 0t2: smoke harness runtime isolation contract (#1696) ===" +bash "$ROOT/tests/test_smoke_runtime_isolation_contract.sh" + echo "=== Step 0u: shell line-ending contract ===" bash "$ROOT/tests/test_shell_line_endings.sh" diff --git a/tests/test_smoke_runtime_isolation_contract.sh b/tests/test_smoke_runtime_isolation_contract.sh new file mode 100755 index 000000000..e91e1d38d --- /dev/null +++ b/tests/test_smoke_runtime_isolation_contract.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runtime-isolation contract for the smoke harness (#1696, follow-up to #1691). +# +# scripts/smoke-test.sh is the process that actually starts the product during +# a smoke run, and it retires "the account daemon" seven times through +# `daemon stop`. Its wrappers sandbox HOME/XDG/TMPDIR and CBM_CACHE_DIR, but +# only CBM_RUNTIME_DIR moves the daemon rendezvous (docs/CONFIGURATION.md), so +# without a private runtime every one of those stops lands on the operator's +# live daemon. 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_SMOKE_ENV_PROBE" +case "${1-}" in + --version) echo "v0.0.0-probe"; exit 0 ;; + daemon) [[ "${2-}" == status ]] && exit 1; exit 0 ;; +esac +echo '{}' +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 answers nothing beyond --version, so the smoke fails early; only +# the environment it handed to the product is under test here. +CBM_CACHE_DIR="$CALLER_CACHE" \ +CBM_RUNTIME_DIR="$CALLER_RUNTIME" \ +CBM_SMOKE_ENV_PROBE="$ENV_LOG" \ + "$ROOT/scripts/smoke-test.sh" "$ENV_PROBE" > "$WORKDIR/smoke.out" 2>&1 || true + +[[ -s "$ENV_LOG" ]] || fail "smoke-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 "smoke-test exposed the caller CBM_RUNTIME_DIR to a product process" + fi + if [[ -z "$child_cache" || "$child_cache" == "$CALLER_CACHE_NORMALIZED" ]]; then + fail "smoke-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 "smoke runtime/cache were not isolated beneath one private root" + fi + if [[ -n "$private_root" && "$private_root" != "${child_runtime%/*}" ]]; then + fail "smoke-test switched private roots mid-run" + fi + private_root="${child_runtime%/*}" +done < "$ENV_LOG" + +[[ ! -e "$private_root" ]] || fail "smoke-test left its private root behind: $private_root" + +echo "PASS: smoke harness isolates its daemon runtime and cache from the caller"