--- name: sigterm-kill-test-stub description: "Pin \"code kills its child process\" behavior with a bash stub player: make the stub SIGTERM-reactive via sleep&wait, log KILLED via trap; diagnose deferred-trap failures." --- Use when a test must prove production code sends SIGTERM to a spawned child (player, worker, server stub) and the test asserts the kill happened — or when such a test mysteriously fails with `KILLED` missing and wall time equal to the stub's sleep duration. ## The trap-during-sleep gotcha A bash stub that sleeps in the **foreground** does NOT run its TERM trap when signalled: bash defers traps until the current foreground command completes. The stub dies (or logs) only after the sleep finishes — long after the test read the evidence file. Symptoms: - Assertion on a `KILLED` marker line fails although production code demonstrably calls `child.kill("SIGTERM")`. - Test wall time equals the stub's full sleep (the kill "had no effect"). ## Fix: reactive stub skeleton ```bash #!/usr/bin/env bash printf '%s\n' "$*" >> "$STUB_LOG" # call record trap 'printf "KILLED\n" >> "$STUB_LOG"; exit 143' TERM # BEFORE the sleep for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$COPY" ;; esac; done sleep "${STUB_HOLD:-0.1}" & # background sleep… wait $! # …wait is interruptible printf 'DONE\n' >> "$STUB_LOG" ``` - `sleep N & wait $!` is the idiom: a signal makes builtin `wait` return immediately, the trap runs at once, `exit` inside the trap prevents fallthrough to `DONE`. - Tune duration per test via the env knob (`STUB_HOLD=5`) so the child is provably still alive when the kill is triggered. - Put the trap BEFORE any long command; a trap registered after the sleep never guards it. ## Harness wiring (black-box driver pattern) - Scenario: start the long work → settle (>spawn latency, ` per line) so tests grep output, not files: black-box seam stays output-only. - Assert `KILLED` present; for interrupt-semantics also count (`kills -eq 1`) and total call lines (`PLAYS 3` = call, KILLED, call). ## Orphan hygiene Second child still sleeping when the driver exits: spawn with `stdio: "ignore"` so the orphan holds no capture pipe — it SIGPIPEs/quiets at teardown; do not lengthen tests waiting for it. ## Verify before trusting Run the pair separately: kill-tests green while a byte-equality test (`cp` of the payload before sleep) stays green — the reactive rewrite must not drop the copy step (it sits between trap and sleep).