feat(controller-peers-env-fallback): fall back to XCE_CONTROLLER_PEERS in the worker (task 3.2)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-03 07:07:19 +00:00
parent 55193fcb4b
commit 05a7e6dd0c
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
3 changed files with 203 additions and 17 deletions

View file

@ -1,7 +1,8 @@
#!/usr/bin/env bash
# Helpers and the *_BIN variables are used by the cases later tasks add (and
# on_exit via trap), which shellcheck cannot see while the case list is empty.
# shellcheck disable=SC2329,SC2034
# main() runs every case through the registry ("$fn"), an indirect call that
# the linter cannot follow, so it reports each case_* function -- and every
# helper only cases call -- as never invoked (SC2329).
# shellcheck disable=SC2329
# verify-controller-peers-env.sh -- controller-peers-env-fallback regression
# script (.kiro/specs/controller-peers-env-fallback, design.md "Testing
# Strategy -> Scripted Regression Verification").
@ -95,6 +96,8 @@ unset XCE_CONTROLLER_PEERS
CONTROLLER_BIN=""
SCHEDULER_BIN=""
WORKER_BIN=""
# Set by preflight; the harness cases (task 3.3) are its only reader.
# shellcheck disable=SC2034
HARNESS_BIN=""
# component:relative-path:variable
@ -650,6 +653,150 @@ case_scheduler_sim_ignores_variable() {
add_case case_scheduler_sim_ignores_variable \
"scheduler: --sim never reaches the peers block, even with the variable set"
# ── Task 3.2 (Req 1.1-1.5, 1.7, 1.8): the worker (xcecd_virtual, which needs
# no root) resolves its candidate list from --peers, else XCE_CONTROLLER_PEERS,
# else the single-controller path. Its startup line is logged before the log
# file is set up, so it lands on STDOUT. tlog writes it there unbuffered (the
# line is in the redirected stdout file within ~0.3s while the worker keeps
# running), so it is waited for while the worker runs, like the scheduler's.
# The worker ignores SIGTERM for longer than stop_proc's 3s grace, so it is
# SIGKILLed once the line is there. Argument errors exit on their own, before
# the log directory is created; in an ASAN build those early returns must not
# leak (the -l directory string), which the env-value error cases check.
WORKER_ANCHOR_RE='HA cluster config|No --peers flag'
WORKER_STANDALONE_RE='No --peers flag and XCE_CONTROLLER_PEERS unset or empty; using single-controller path \(host=localhost port=9990\), version '
# _split_env_args: shared by start_worker/run_worker -- fills WENV with the
# env(1) arguments before `--` and WARGS with the worker arguments after it.
WENV=()
WARGS=()
_split_env_args() {
WENV=(); WARGS=()
while [ $# -gt 0 ] && [ "$1" != -- ]; do
WENV+=("$1"); shift
done
[ $# -gt 0 ] && shift
WARGS=("$@")
}
# start_worker <dir> <env-args...> -- [worker args...]
# Start xcecd_virtual in the background with -l <dir>/logs, wait for its
# startup line on stdout, then kill it. Fails (with the output tails) when
# the line never appears.
start_worker() {
local dir="$1" pid; shift
_split_env_args "$@"
start_bg "$dir" "${WENV[@]}" "$WORKER_BIN" -l "$dir/logs" \
"${WARGS[@]+"${WARGS[@]}"}"
pid="$LAST_PID"
if ! wait_for_line "$dir/stdout" "$WORKER_ANCHOR_RE" 10 "$pid"; then
kill -KILL "$pid" 2>/dev/null || true
stop_proc "$pid"
red " worker never printed its startup line"
_show_tail "$dir/stdout"
_show_tail "$dir/stderr"
return 1
fi
kill -KILL "$pid" 2>/dev/null || true
stop_proc "$pid"
}
# run_worker <dir> <env-args...> -- [worker args...]
# Run xcecd_virtual to completion (argument errors, -h) with -l <dir>/logs.
run_worker() {
local dir="$1"; shift
_split_env_args "$@"
run_to_completion "$dir" 10 "${WENV[@]}" "$WORKER_BIN" -l "$dir/logs" \
"${WARGS[@]+"${WARGS[@]}"}"
}
case_worker_env_list() {
local d; d="$(new_scratch worker-env-list)"
start_worker "$d" "XCE_CONTROLLER_PEERS=$ENV3" -- -n w-env || return 1
assert_grep "$d/stdout" \
'HA cluster config: 3 candidate\(s\), identity=w-env \(from XCE_CONTROLLER_PEERS\), version ' "stdout" || return 1
assert_no_grep "$d/stdout" 'No --peers flag' "stdout" || return 1
}
add_case case_worker_env_list \
"worker: XCE_CONTROLLER_PEERS used when --peers is absent (1.2)"
case_worker_peers_wins() {
local d; d="$(new_scratch worker-peers-wins)"
start_worker "$d" "XCE_CONTROLLER_PEERS=$ENV3" -- -n w-flag \
--peers solo=localhost:7 || return 1
assert_grep "$d/stdout" \
'HA cluster config: 1 candidate\(s\), identity=w-flag, version ' "stdout" || return 1
assert_no_grep "$d/stdout" '3 candidate|from XCE_CONTROLLER_PEERS' "stdout" || return 1
}
add_case case_worker_peers_wins \
"worker: explicit --peers wins over XCE_CONTROLLER_PEERS (1.1)"
case_worker_neither_set() {
local d; d="$(new_scratch worker-neither)"
start_worker "$d" -u XCE_CONTROLLER_PEERS -- -n w-none || return 1
assert_grep "$d/stdout" "$WORKER_STANDALONE_RE" "stdout" || return 1
assert_no_grep "$d/stdout" 'HA cluster config' "stdout" || return 1
}
add_case case_worker_neither_set \
"worker: neither --peers nor the variable -> single-controller path, line names the variable (1.3, 1.8)"
case_worker_env_empty() {
local d; d="$(new_scratch worker-env-empty)"
start_worker "$d" "XCE_CONTROLLER_PEERS=" -- -n w-empty || return 1
assert_grep "$d/stdout" "$WORKER_STANDALONE_RE" "stdout" || return 1
assert_no_grep "$d/stdout" 'HA cluster config' "stdout" || return 1
}
add_case case_worker_env_empty \
"worker: empty XCE_CONTROLLER_PEERS behaves as unset (1.7, 1.8)"
case_worker_parse_error_source() {
local d
d="$(new_scratch worker-parse-env)"
run_worker "$d" "XCE_CONTROLLER_PEERS=a=nocolon" -- -n w-perr
assert_rc 1 || return 1
assert_grep "$d/stderr" 'xcecd: XCE_CONTROLLER_PEERS parse error' "stderr (env value)" || return 1
assert_no_grep "$d/stderr" 'LeakSanitizer' "stderr (ASAN build: early return must free)" || return 1
d="$(new_scratch worker-parse-flag)"
run_worker "$d" "XCE_CONTROLLER_PEERS=$ENV3" -- -n w-perr --peers a=nocolon
assert_rc 1 || return 1
assert_grep "$d/stderr" 'xcecd: --peers parse error' "stderr (--peers value)" || return 1
assert_no_grep "$d/stderr" 'XCE_CONTROLLER_PEERS' "stderr (--peers value)" || return 1
}
add_case case_worker_parse_error_source \
"worker: parse error names --peers or XCE_CONTROLLER_PEERS (1.4)"
case_worker_validate_error_source() {
local d
d="$(new_scratch worker-port0-env)"
run_worker "$d" "XCE_CONTROLLER_PEERS=a=localhost:0" -- -n w-verr
assert_rc 1 || return 1
assert_grep "$d/stderr" 'xcecd: XCE_CONTROLLER_PEERS: invalid cluster config .* in "a=localhost:0"' "stderr (env value)" || return 1
assert_no_grep "$d/stderr" 'LeakSanitizer' "stderr (ASAN build: early return must free)" || return 1
d="$(new_scratch worker-port0-flag)"
run_worker "$d" "XCE_CONTROLLER_PEERS=$ENV3" -- -n w-verr --peers a=localhost:0
assert_rc 1 || return 1
assert_grep "$d/stderr" 'xcecd: --peers: invalid cluster config .* in "a=localhost:0"' "stderr (--peers value)" || return 1
assert_no_grep "$d/stderr" 'XCE_CONTROLLER_PEERS' "stderr (--peers value)" || return 1
}
add_case case_worker_validate_error_source \
"worker: cluster-config validation error (port 0) names --peers or XCE_CONTROLLER_PEERS (1.4)"
case_worker_help_names_variable() {
local d; d="$(new_scratch worker-help)"
run_to_completion "$d" 10 -u XCE_CONTROLLER_PEERS "$WORKER_BIN" -h
assert_rc 0 || return 1
tr '\n' ' ' <"$d/stdout" | tr -s ' ' >"$d/help.txt"
assert_grep "$d/help.txt" 'XCE_CONTROLLER_PEERS' "-h output" || return 1
assert_grep "$d/help.txt" 'unset or empty' "-h output" || return 1
assert_grep "$d/help.txt" 'single-controller' "-h output" || return 1
assert_grep "$d/help.txt" 'sudo --preserve-env=XCE_CONTROLLER_PEERS' "-h output" || return 1
}
add_case case_worker_help_names_variable \
"worker: -h documents XCE_CONTROLLER_PEERS, the opt-out and that sudo drops it (1.5, 1.7)"
# ════════════════════════════════════════════════════════════════════════════
# main