From a28ad76868747d21020c4e7d16a5f22ccb7e4d63 Mon Sep 17 00:00:00 2001 From: golem Date: Fri, 21 Aug 2026 04:57:14 -0600 Subject: [PATCH] fix: close demo lifecycle races --- docs/LEARNING_GUIDE.md | 12 +-- test/NamedErrorExercises.t.sol | 92 ++++++++++++++++++++++ tools/demo-local.sh | 22 +++++- tools/process-lib.sh | 78 ++++++++++++------- tools/test-process-safety.sh | 138 ++++++++++++++++++++++++++++++--- 5 files changed, 295 insertions(+), 47 deletions(-) create mode 100644 test/NamedErrorExercises.t.sol diff --git a/docs/LEARNING_GUIDE.md b/docs/LEARNING_GUIDE.md index c719ba6..138ad58 100644 --- a/docs/LEARNING_GUIDE.md +++ b/docs/LEARNING_GUIDE.md @@ -106,17 +106,17 @@ forge test --match-test testLegacyParallelActorManifestIsRejected -vv # DemoScript.InvalidActorConfiguration forge test --match-test testAnvilManifestRequiresOwnerAtActorZero -vv -# DemoScript.UnexpectedState example payload: label, expected, actual -cast calldata 'UnexpectedState(string,uint256,uint256)' 'reserves' 1400000000 1399000000 +# DemoScript.UnexpectedState, including label and expected/actual values +forge test --match-test testUnexpectedStateExerciseRevertsThroughAssertionBranch -vv # DemoScript.UnexpectedAddress, including expected/actual implementation addresses forge test --match-test testCheckStateRejectsManifestImplementationMismatch -vv -# CheckState.Insolvent example payload: reserves below ledger liabilities -cast calldata 'Insolvent(uint256,uint256)' 1399000000 1400000000 +# CheckState.Insolvent with reserves below ledger liabilities +forge test --match-test testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches -vv -# CheckState.UnknownStage example payload -cast calldata 'UnknownStage(string)' 'mystery' +# CheckState.UnknownStage through the real state checker +forge test --match-test testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches -vv ``` Finish by running `make verify`; it combines unit, fuzz, invariant, script, process-safety, scanner, and web gates. diff --git a/test/NamedErrorExercises.t.sol b/test/NamedErrorExercises.t.sol new file mode 100644 index 0000000..89fec74 --- /dev/null +++ b/test/NamedErrorExercises.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.35; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {BankV1} from "../src/BankV1.sol"; +import {MockUSDC} from "../src/MockUSDC.sol"; +import {CheckState} from "../script/CheckState.s.sol"; +import {DemoScript} from "../script/lib/DemoScript.sol"; + +contract NamedErrorHarness is DemoScript { + function assertUint(string calldata label, uint256 expected, uint256 actual) external pure { + _assertUint(label, expected, actual); + } + + function writeManifest(string calldata path, Manifest calldata manifest) external { + _writeManifest(path, manifest); + } +} + +contract NamedErrorExercisesTest is Test { + address internal constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address internal constant ALICE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address internal constant BOB = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; + + NamedErrorHarness internal harness; + + function setUp() public { + vm.chainId(31337); + harness = new NamedErrorHarness(); + } + + function testUnexpectedStateExerciseRevertsThroughAssertionBranch() public { + vm.expectRevert( + abi.encodeWithSelector(DemoScript.UnexpectedState.selector, "reserves", uint256(1_400e6), uint256(1_399e6)) + ); + harness.assertUint("reserves", 1_400e6, 1_399e6); + } + + function testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches() public { + (MockUSDC token, BankV1 bank) = _deployFixture("insolvent.json"); + token.mint(ALICE, 1_400e6); + vm.startPrank(ALICE); + token.approve(address(bank), 1_400e6); + bank.deposit(1_400e6); + vm.stopPrank(); + deal(address(token), address(bank), 1_399e6); + vm.setEnv("DEMO_EXPECTED_STAGE", "invariants"); + CheckState checker = new CheckState(); + + vm.expectRevert(abi.encodeWithSelector(CheckState.Insolvent.selector, uint256(1_399e6), uint256(1_400e6))); + checker.run(); + + _deployFixture("unknown-stage.json"); + vm.setEnv("DEMO_EXPECTED_STAGE", "mystery"); + checker = new CheckState(); + + vm.expectRevert(abi.encodeWithSelector(CheckState.UnknownStage.selector, "mystery")); + checker.run(); + } + + function _deployFixture(string memory fixtureName) internal returns (MockUSDC token, BankV1 bank) { + token = new MockUSDC(address(this)); + BankV1 implementation = new BankV1(); + ERC1967Proxy proxy = + new ERC1967Proxy(address(implementation), abi.encodeCall(BankV1.initialize, (address(token), OWNER))); + bank = BankV1(address(proxy)); + + DemoScript.Actor[] memory actors = new DemoScript.Actor[](3); + actors[0] = DemoScript.Actor({label: "owner", address_: OWNER}); + actors[1] = DemoScript.Actor({label: "Alice", address_: ALICE}); + actors[2] = DemoScript.Actor({label: "Bob", address_: BOB}); + DemoScript.Manifest memory manifest = DemoScript.Manifest({ + schemaVersion: 1, + network: "anvil", + chainId: 31337, + deploymentBlock: 1, + rpcUrl: "http://127.0.0.1:8545", + explorerBaseUrl: "", + token: address(token), + proxy: address(proxy), + implementation: address(implementation), + owner: OWNER, + actors: actors + }); + string memory fixtureDir = string.concat(vm.projectRoot(), "/deployments/test-named-error-exercises"); + vm.createDir(fixtureDir, true); + string memory manifestPath = string.concat(fixtureDir, "/", fixtureName); + harness.writeManifest(manifestPath, manifest); + vm.setEnv("DEPLOYMENT_MANIFEST_PATH", manifestPath); + } +} diff --git a/tools/demo-local.sh b/tools/demo-local.sh index f7cfa3b..e870509 100755 --- a/tools/demo-local.sh +++ b/tools/demo-local.sh @@ -8,6 +8,7 @@ ANVIL_DEV_WORDS=(test test test test test test test test test test test junk) CLEANING=0 ANVIL_PID='' ANVIL_START='' ANVIL_PGID='' VITE_PID='' VITE_START='' VITE_PGID='' +ANVIL_VALIDATED=0 VITE_VALIDATED=0 cleanup() { local status=$? cleanup_status=0 @@ -15,12 +16,14 @@ cleanup() { CLEANING=1 trap - INT TERM EXIT if [[ -n "$VITE_PID" && -n "$VITE_START" && -n "$VITE_PGID" ]]; then - demo_stop_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1 + if ((VITE_VALIDATED == 1)); then demo_stop_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1 + else demo_stop_raw_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1; fi else demo_stop_recorded "$ROOT" vite || cleanup_status=1 fi if [[ -n "$ANVIL_PID" && -n "$ANVIL_START" && -n "$ANVIL_PGID" ]]; then - demo_stop_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1 + if ((ANVIL_VALIDATED == 1)); then demo_stop_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1 + else demo_stop_raw_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1; fi else demo_stop_recorded "$ROOT" anvil || cleanup_status=1 fi @@ -34,10 +37,23 @@ trap cleanup EXIT capture_and_publish() { local kind=$1 pid=$2 start pgid for _ in {1..50}; do - if demo_capture_process "$ROOT" "$kind" "$pid" start pgid; then + if demo_capture_raw_launch "$pid" start pgid; then if [[ "$kind" == anvil ]]; then ANVIL_START=$start; ANVIL_PGID=$pgid else VITE_START=$start; VITE_PGID=$pgid; fi + break + fi + demo_pid_is_running "$pid" || break + sleep 0.02 + done + [[ -n ${start:-} && -n ${pgid:-} ]] || { + printf 'Could not capture raw ownership for launched %s PID %s; no signal will be guessed.\n' "$kind" "$pid" >&2 + return 1 + } + for _ in {1..50}; do + if demo_identity_matches "$ROOT" "$kind" "$pid" "$start" "$pgid"; then demo_publish_process "$ROOT" "$kind" "$pid" "$start" "$pgid" + if [[ "$kind" == anvil ]]; then ANVIL_VALIDATED=1 + else VITE_VALIDATED=1; fi return fi demo_pid_is_running "$pid" || break diff --git a/tools/process-lib.sh b/tools/process-lib.sh index b2eea7d..4267922 100755 --- a/tools/process-lib.sh +++ b/tools/process-lib.sh @@ -89,28 +89,25 @@ demo_interpreter_for_entry() { else demo_resolve_command "${interpreter[0]}"; fi } -demo_has_sequence_from() { +demo_has_exact_tail() { local array_name=$1 begin=$2 local -n argv_ref=$array_name shift 2 local -a wanted=("$@") - local i j - for ((i = begin; i + ${#wanted[@]} <= ${#argv_ref[@]}; i++)); do - for ((j = 0; j < ${#wanted[@]}; j++)); do - [[ ${argv_ref[i+j]} == "${wanted[j]}" ]] || break - done - ((j == ${#wanted[@]})) && return 0 + local index + ((${#argv_ref[@]} - begin == ${#wanted[@]})) || return 1 + for index in "${!wanted[@]}"; do + [[ ${argv_ref[begin+index]} == "${wanted[index]}" ]] || return 1 done - return 1 } demo_command_matches() { local root=$1 pid=$2 kind=$3 expected executable interpreter entry_index local -a arguments=() [[ -r "/proc/$pid/cmdline" && -e "/proc/$pid/exe" ]] || return 1 - mapfile -d '' -t arguments <"/proc/$pid/cmdline" + { mapfile -d '' -t arguments <"/proc/$pid/cmdline"; } 2>/dev/null || return 1 ((${#arguments[@]} > 0)) || return 1 - executable=$(readlink -f -- "/proc/$pid/exe") || return 1 + executable=$(readlink -f -- "/proc/$pid/exe" 2>/dev/null) || return 1 if [[ "$kind" == anvil ]]; then expected=$(demo_resolve_command anvil) || return 1 @@ -128,13 +125,11 @@ demo_command_matches() { fi if [[ "$kind" == anvil ]]; then - demo_has_sequence_from arguments "$((entry_index + 1))" --host 127.0.0.1 || return 1 - demo_has_sequence_from arguments "$((entry_index + 1))" --port 8545 || return 1 - demo_has_sequence_from arguments "$((entry_index + 1))" --chain-id 31337 || return 1 + local dev_phrase='test test test test test test test test test test test junk' + demo_has_exact_tail arguments "$((entry_index + 1))" \ + --host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "$dev_phrase" else - local -a expected_tail=(web --host 127.0.0.1 --port 5173) - local -a actual_tail=("${arguments[@]:entry_index+1}") - [[ "${actual_tail[*]}" == "${expected_tail[*]}" && ${#actual_tail[@]} -eq ${#expected_tail[@]} ]] || return 1 + demo_has_exact_tail arguments "$((entry_index + 1))" web --host 127.0.0.1 --port 5173 fi } @@ -146,10 +141,17 @@ demo_identity_matches() { demo_command_matches "$root" "$pid" "$kind" } +demo_capture_raw_launch() { + local pid=$1 start_name=$2 pgid_name=$3 state raw_actual_start raw_actual_pgid sid + demo_read_process_identity "$pid" state raw_actual_start raw_actual_pgid sid || return 1 + [[ "$state" != Z && "$pid" == "$raw_actual_pgid" && "$pid" == "$sid" ]] || return 1 + printf -v "$start_name" '%s' "$raw_actual_start" + printf -v "$pgid_name" '%s' "$raw_actual_pgid" +} + demo_capture_process() { - local root=$1 kind=$2 pid=$3 start_name=$4 pgid_name=$5 state actual_start actual_pgid sid - demo_read_process_identity "$pid" state actual_start actual_pgid sid || return 1 - [[ "$state" != Z && "$pid" == "$actual_pgid" && "$pid" == "$sid" ]] || return 1 + local root=$1 kind=$2 pid=$3 start_name=$4 pgid_name=$5 actual_start actual_pgid + demo_capture_raw_launch "$pid" actual_start actual_pgid || return 1 demo_command_matches "$root" "$pid" "$kind" || return 1 printf -v "$start_name" '%s' "$actual_start" printf -v "$pgid_name" '%s' "$actual_pgid" @@ -201,20 +203,29 @@ demo_collect_group_members() { } demo_anchor_allows_signal() { - local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 state start pgid sid + local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 identity_mode=${6:-validated} state start pgid sid if ! demo_read_process_identity "$leader" state start pgid sid; then return 0; fi [[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_pgid" ]] || return 1 [[ "$state" == Z ]] && return 0 + [[ "$identity_mode" == raw ]] && return 0 demo_process_has_command_line "$leader" || return 0 - demo_command_matches "$root" "$leader" "$kind" + if demo_command_matches "$root" "$leader" "$kind"; then return 0; fi + if ! demo_read_process_identity "$leader" state start pgid sid; then return 0; fi + [[ "$state" == Z ]] && return 0 + [[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_pgid" ]] || return 1 + return 1 } demo_signal_member() { - local pid=$1 expected_start=$2 expected_pgid=$3 expected_sid=$4 signal=$5 state start pgid sid + local pid=$1 expected_start=$2 expected_pgid=$3 expected_sid=$4 signal=$5 state start pgid sid signal_status if ! demo_read_process_identity "$pid" state start pgid sid; then return 0; fi [[ "$state" != Z ]] || return 0 [[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || return 2 - kill -"$signal" -- "$pid" + if kill -"$signal" -- "$pid"; then return 0; else signal_status=$?; fi + if ! demo_read_process_identity "$pid" state start pgid sid; then return 0; fi + [[ "$state" != Z ]] || return 0 + [[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || return 0 + return "$signal_status" } demo_group_has_live_members() { @@ -224,12 +235,13 @@ demo_group_has_live_members() { ((${#members[@]} > 0)) } -demo_stop_launch() { - local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 member member_pid member_start result +demo_stop_owned_session() { + local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 identity_mode=$6 member member_pid member_start result local -a members + [[ "$identity_mode" == validated || "$identity_mode" == raw ]] || return 1 demo_is_process_id "$leader" && demo_is_uint "$expected_start" && demo_is_process_id "$expected_pgid" \ && [[ "$leader" == "$expected_pgid" ]] || return 1 - demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || { + demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode" || { printf 'Refusing %s PID %s: process identity changed\n' "$kind" "$leader" >&2 return 1 } @@ -243,13 +255,13 @@ demo_stop_launch() { for member in "${members[@]}"; do IFS=: read -r member_pid member_start <<<"$member" [[ "$member_pid" != "$leader" ]] || continue - demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1 + demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode" || return 1 demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1 done for member in "${members[@]}"; do IFS=: read -r member_pid member_start <<<"$member" [[ "$member_pid" == "$leader" ]] || continue - demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1 + demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode" || return 1 demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1 done for ((result = 0; result < ${DEMO_TERM_WAIT_ATTEMPTS:-50}; result++)); do @@ -264,7 +276,7 @@ demo_stop_launch() { ((${#members[@]} > 0)) || break for member in "${members[@]}"; do IFS=: read -r member_pid member_start <<<"$member" - if ! demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid"; then + if ! demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" "$identity_mode"; then printf 'Refusing KILL escalation: %s leader identity changed\n' "$kind" >&2 return 1 fi @@ -287,6 +299,14 @@ demo_stop_launch() { demo_remove_record "$root" "$kind" } +demo_stop_launch() { + demo_stop_owned_session "$1" "$2" "$3" "$4" "$5" validated +} + +demo_stop_raw_launch() { + demo_stop_owned_session "$1" "$2" "$3" "$4" "$5" raw +} + demo_stop_recorded() { local root=$1 kind=$2 pid start pgid local pid_path="$root/.demo/$kind.pid" start_path="$root/.demo/$kind.start" pgid_path="$root/.demo/$kind.pgid" diff --git a/tools/test-process-safety.sh b/tools/test-process-safety.sh index 1d38c96..09e4b0b 100755 --- a/tools/test-process-safety.sh +++ b/tools/test-process-safety.sh @@ -8,6 +8,8 @@ TEST_ROOT=$(mktemp -d /tmp/uups-bank-process-test.XXXXXX) PATH="$TEST_ROOT/bin:$PATH" export DEMO_TERM_WAIT_ATTEMPTS=5 export DEMO_KILL_WAIT_ATTEMPTS=10 +ANVIL_DEV_PHRASE='test test test test test test test test test test test junk' +ANVIL_EXACT_ARGS=(--host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "$ANVIL_DEV_PHRASE") declare -a TEST_IDENTITIES=() PASSED=0 STARTED_PID= @@ -15,7 +17,7 @@ STARTED_PID= cleanup() { local identity root relative for identity in "${TEST_IDENTITIES[@]}"; do test_safe_stop "$identity"; done - for root in absent stale nonnumeric wrong-command false-anvil false-vite reused atomic matching group mutated partial local-reset base-canonical base-active sentinel; do + for root in absent stale nonnumeric wrong-command false-anvil false-vite exact-anvil raw-capture reused atomic matching group mutated partial local-reset base-canonical base-active sentinel; do for relative in \ .demo/anvil.pid .demo/anvil.start .demo/anvil.pgid .demo/anvil.log \ .demo/vite.pid .demo/vite.start .demo/vite.pgid .demo/vite.log \ @@ -29,7 +31,8 @@ cleanup() { "$TEST_ROOT/$root/web" "$TEST_ROOT/$root/deployments" "$TEST_ROOT/$root/tools" "$TEST_ROOT/$root/.demo" \ "$TEST_ROOT/$root" 2>/dev/null || true done - rm -f -- "$TEST_ROOT/bin/anvil" "$TEST_ROOT/bin/vite" "$TEST_ROOT/vite-child.pid" + rm -f -- "$TEST_ROOT/bin/anvil" "$TEST_ROOT/bin/vite" "$TEST_ROOT/bin/leaderless" \ + "$TEST_ROOT/vite-child.pid" "$TEST_ROOT/leaderless-child.pid" rmdir -- "$TEST_ROOT/bin" "$TEST_ROOT" 2>/dev/null || true } trap cleanup EXIT @@ -75,15 +78,17 @@ test_safe_stop() { local identity=$1 leader expected_start expected_pgid expected_sid state start pgid sid signal line member member_start local current_state current_start current_pgid current_sid IFS=: read -r leader expected_start expected_pgid expected_sid <<<"$identity" - if test_process_identity "$leader" state start pgid sid && [[ "$start" != "$expected_start" ]]; then return 0; fi + if test_process_identity "$leader" state start pgid sid \ + && [[ "$start" != "$expected_start" || "$pgid" != "$expected_pgid" || "$sid" != "$expected_sid" ]]; then return 0; fi for signal in TERM KILL; do while read -r member pgid sid state; do [[ "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" && "$state" != Z ]] || continue test_process_identity "$member" state member_start pgid sid || continue [[ "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || continue - test_process_identity "$leader" current_state current_start current_pgid current_sid || continue - [[ "$current_start" == "$expected_start" && "$current_pgid" == "$expected_pgid" \ - && "$current_sid" == "$expected_sid" ]] || continue + if test_process_identity "$leader" current_state current_start current_pgid current_sid; then + [[ "$current_start" == "$expected_start" && "$current_pgid" == "$expected_pgid" \ + && "$current_sid" == "$expected_sid" ]] || continue + fi test_process_identity "$member" current_state current_start current_pgid current_sid || continue [[ "$current_state" != Z && "$current_start" == "$member_start" \ && "$current_pgid" == "$expected_pgid" && "$current_sid" == "$expected_sid" ]] || continue @@ -138,7 +143,7 @@ wait HELPER chmod +x "$helper" if [[ "$kind" == anvil ]]; then - setsid "$helper" --host 127.0.0.1 --port 8545 --chain-id 31337 >/dev/null 2>&1 & + setsid "$helper" "${ANVIL_EXACT_ARGS[@]}" >/dev/null 2>&1 & else setsid "$helper" web --host 127.0.0.1 --port 5173 >/dev/null 2>&1 & fi @@ -152,7 +157,17 @@ HELPER STARTED_PID=$pid } -printf '1..16\n' +assert_anvil_args_rejected() { + local root=$1 label=$2 state start pgid sid pid + shift 2 + setsid "$TEST_ROOT/bin/anvil" "$@" & pid=$! + track_process "$pid" + test_process_identity "$pid" state start pgid sid + if demo_command_matches "$root" "$pid" anvil; then fail "$label Anvil arguments were accepted"; fi + demo_stop_raw_launch "$root" anvil "$pid" "$start" "$pgid" +} + +printf '1..21\n' # Catches cleanup treating a missing record as an error or signaling an inferred PID. root=$(make_root absent) @@ -205,6 +220,81 @@ if demo_stop_recorded "$root" vite 2>/dev/null; then fail 'argument-only Vite si assert_alive "$false_vite_pid" pass 'Vite identity requires the exact project-local entry and interpreter' +# Catches accepting extra or overriding Anvil arguments after a valid-looking prefix. +root=$(make_root exact-anvil) +mkdir -p "$TEST_ROOT/bin" +cat >"$TEST_ROOT/bin/anvil" <<'HELPER' +#!/usr/bin/env bash +sleep 120 & +wait +HELPER +chmod +x "$TEST_ROOT/bin/anvil" +assert_anvil_args_rejected "$root" extra "${ANVIL_EXACT_ARGS[@]}" --silent +assert_anvil_args_rejected "$root" duplicate "${ANVIL_EXACT_ARGS[@]}" --port 9999 +assert_anvil_args_rejected "$root" reordered \ + --port 8545 --host 127.0.0.1 --chain-id 31337 --mnemonic "$ANVIL_DEV_PHRASE" +pass 'Anvil identity requires the exact complete launch argument tail' + +# Catches treating an ESRCH-like signal race as fatal after the captured member has exited. +setsid /bin/bash -c 'trap "exit 0" TERM; while :; do sleep 0.01; done' & vanished_pid=$! +track_process "$vanished_pid" +vanished_state='' vanished_start='' vanished_pgid='' vanished_sid='' +test_process_identity "$vanished_pid" vanished_state vanished_start vanished_pgid vanished_sid +[[ "$vanished_state" != Z ]] || fail 'vanishing race fixture exited before injection' +kill() { + builtin kill -TERM -- "$vanished_pid" 2>/dev/null || true + for _ in {1..50}; do + vanished_state_after='' + if ! test_process_identity "$vanished_pid" vanished_state_after vanished_start_after vanished_pgid_after vanished_sid_after \ + || [[ "$vanished_state_after" == Z ]]; then break; fi + sleep 0.01 + done + return 1 +} +if demo_signal_member "$vanished_pid" "$vanished_start" "$vanished_pgid" "$vanished_sid" TERM; then + vanished_result=0 +else + vanished_result=$? +fi +unset -f kill +wait "$vanished_pid" 2>/dev/null || true +((vanished_result == 0)) || fail 'a vanished member made its raced signal fatal' +pass 'a failed signal is idempotent after the captured member vanishes' + +# Catches swallowing a real signal failure while the exact captured member remains live. +setsid /bin/sleep 120 & unsignaled_pid=$! +track_process "$unsignaled_pid" +unsignaled_state='' unsignaled_start='' unsignaled_pgid='' unsignaled_sid='' +test_process_identity "$unsignaled_pid" unsignaled_state unsignaled_start unsignaled_pgid unsignaled_sid +[[ "$unsignaled_state" != Z ]] || fail 'unchanged signal-failure fixture exited early' +kill() { return 1; } +if demo_signal_member "$unsignaled_pid" "$unsignaled_start" "$unsignaled_pgid" "$unsignaled_sid" TERM; then + unsignaled_result=0 +else + unsignaled_result=$? +fi +unset -f kill +((unsignaled_result != 0)) || fail 'a failed signal to the unchanged live member was accepted' +assert_alive "$unsignaled_pid" +pass 'a failed signal remains fatal for the same live member tuple' + +# Catches command validation failure occurring before launch ownership is retained for cleanup. +root=$(make_root raw-capture) +setsid /bin/bash -c 'sleep 120 & wait' unexpected-entry --not-the-demo & raw_pid=$! +track_process "$raw_pid" +raw_start='' raw_pgid='' +declare -F demo_capture_raw_launch >/dev/null || fail 'missing raw launch identity capture' +declare -F demo_stop_raw_launch >/dev/null || fail 'missing command-independent raw launch cleanup' +for _ in {1..50}; do + if demo_capture_raw_launch "$raw_pid" raw_start raw_pgid; then break; fi + sleep 0.02 +done +[[ -n ${raw_start:-} && "$raw_pgid" == "$raw_pid" ]] || fail 'raw launch identity was not captured' +if demo_command_matches "$root" "$raw_pid" anvil; then fail 'raw test command unexpectedly passed Anvil validation'; fi +demo_stop_raw_launch "$root" anvil "$raw_pid" "$raw_start" "$raw_pgid" +assert_dead "$raw_pid" +pass 'raw launch ownership safely cleans a command-validation failure' + # Catches recycled PID ownership being inferred from PID and command alone. root=$(make_root reused) start_owned "$root" anvil @@ -267,7 +357,7 @@ trap 'exec -a changed-after-term /bin/sleep 120' TERM while :; do sleep 0.05; done HELPER chmod +x "$mutating_helper" -setsid "$mutating_helper" --host 127.0.0.1 --port 8545 --chain-id 31337 & mutated_pid=$! +setsid "$mutating_helper" "${ANVIL_EXACT_ARGS[@]}" & mutated_pid=$! track_process "$mutated_pid" write_record "$root" anvil "$mutated_pid" "$(start_tick "$mutated_pid")" "$mutated_pid" if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'changed identity was escalated instead of rejected'; fi @@ -288,6 +378,36 @@ assert_absent "$root/.demo/anvil.start" assert_absent "$root/.demo/anvil.pgid" pass 'in-memory launch identity safely cleans a partial unpublished record' +# Catches test cleanup leaking a TERM-resistant session member after its original leader is reaped. +leaderless_helper="$TEST_ROOT/bin/leaderless" +leaderless_child_file="$TEST_ROOT/leaderless-child.pid" +cat >"$leaderless_helper" <<'HELPER' +#!/usr/bin/env bash +bash -c 'trap "" TERM; exec sleep 120' & +printf '%s\n' "$!" >"$DEMO_CHILD_PID_FILE" +sleep 1 +HELPER +chmod +x "$leaderless_helper" +DEMO_CHILD_PID_FILE="$leaderless_child_file" setsid "$leaderless_helper" & leaderless_pid=$! +track_process "$leaderless_pid" +leaderless_identity=${TEST_IDENTITIES[${#TEST_IDENTITIES[@]}-1]} +for _ in {1..50}; do [[ -s "$leaderless_child_file" ]] && break; sleep 0.02; done +[[ -s "$leaderless_child_file" ]] || fail 'leaderless cleanup child did not start' +leaderless_child=$(<"$leaderless_child_file") +child_state='' child_start='' child_pgid='' child_sid='' +test_process_identity "$leaderless_child" child_state child_start child_pgid child_sid +[[ "$child_state" != Z ]] || fail 'leaderless cleanup child exited early' +wait "$leaderless_pid" +test_safe_stop "$leaderless_identity" +if test_process_identity "$leaderless_child" current_state current_start current_pgid current_sid \ + && [[ "$current_state" != Z ]]; then + if [[ "$current_start" == "$child_start" && "$current_pgid" == "$child_pgid" && "$current_sid" == "$child_sid" ]]; then + builtin kill -KILL -- "$leaderless_child" 2>/dev/null || true + fi + fail 'test cleanup leaked a session member after the leader was reaped' +fi +pass 'test cleanup safely stops surviving members after leader reaping' + # Catches reset deleting arbitrary neighbors or leaving known reproducible local artifacts. root=$(make_root local-reset) for kind in anvil vite; do printf 'log\n' >"$root/.demo/$kind.log"; printf '1\n' >"$root/.demo/$kind.start"; printf '1\n' >"$root/.demo/$kind.pgid"; done