From 806cae0fc9e1d002017eba02fa9570fdab70729c Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:50:13 -0400 Subject: [PATCH 1/2] test(desktop-notification): measure C1 fd1-leak invariant differentially The C1 slow-sink detector asserted a fixed `< 2000ms` wall-clock bound against a sink that sleeps to prove the hook does not wait on the backgrounded telemetry sink. On Windows Git Bash the hook's own process-spawn overhead is ~1.6s solo and 4-10s under parallel-suite load, so the fixed bound had a thin-to-negative margin and false-failed with no fd1 leak present. Measure the invariant differentially instead: a fast-sink baseline run captures the machine's current spawn overhead under the same capture, and the slow-sink run's excess over it isolates the leak signal. Overhead cancels, so the check holds under sustained load while still catching a real fd1 leak (the sink's whole sleep lands in the delta). Sink sleep kept small (6s) so it self-expires before the suite's EXIT cleanup rather than locking its stub file on Windows. Co-Authored-By: Claude Opus --- .../.claude-plugin/plugin.json | 2 +- plugins/desktop-notification/CHANGELOG.md | 13 ++++ .../hooks/desktop-notification.test.sh | 59 +++++++++++++++---- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/plugins/desktop-notification/.claude-plugin/plugin.json b/plugins/desktop-notification/.claude-plugin/plugin.json index deff00bc7..2356672ed 100644 --- a/plugins/desktop-notification/.claude-plugin/plugin.json +++ b/plugins/desktop-notification/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "desktop-notification", - "version": "0.4.1", + "version": "0.4.2", "description": "Alert you when Claude Code needs input — an audible terminal bell, an OSC 9 terminal notification, and an OS-native toast (macOS/Linux) on permission and idle prompts.", "author": { "name": "Melodic Software", diff --git a/plugins/desktop-notification/CHANGELOG.md b/plugins/desktop-notification/CHANGELOG.md index b01d27655..0f7f75fa2 100644 --- a/plugins/desktop-notification/CHANGELOG.md +++ b/plugins/desktop-notification/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to the `desktop-notification` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.4.2] + +### Changed + +- C1 fd1-leak detector in `desktop-notification.test.sh` now measures the slow-sink + invariant differentially — a fast-sink baseline run captures the machine's current + process-spawn overhead, and the slow-sink run's excess over it isolates the leak + signal — instead of asserting a fixed `< 2000ms` wall-clock bound. On Windows Git + Bash the hook's own spawn overhead (~1.6s solo, 4-10s under parallel-suite load) + left the fixed bound with a thin-to-negative margin and false-failed even with no + leak. The differential form cancels ambient overhead, so the check holds under load + while still catching a real fd1 leak (the sink's whole sleep lands in the delta). + ## [0.4.1] ### Changed diff --git a/plugins/desktop-notification/hooks/desktop-notification.test.sh b/plugins/desktop-notification/hooks/desktop-notification.test.sh index d07c5ebf3..8c4d6e73e 100755 --- a/plugins/desktop-notification/hooks/desktop-notification.test.sh +++ b/plugins/desktop-notification/hooks/desktop-notification.test.sh @@ -96,6 +96,16 @@ bel_count() { jq -r '.terminalSequence' <<<"$1" | tr -cd '\007' | wc -c | tr -d ' \r' } +# Whole milliseconds between two $EPOCHREALTIME captures. Locale-agnostic: the +# fractional separator may be '.' or ','; 10# forces base-10 on the (possibly +# zero-padded) microseconds so a leading 0 is not read as octal. +epoch_delta_ms() { + local start="$1" end="$2" ss sf es ef + ss="${start%[.,]*}" sf="${start#*[.,]}" + es="${end%[.,]*}" ef="${end#*[.,]}" + echo $(((es * 1000000 + 10#$ef - ss * 1000000 - 10#$sf) / 1000)) +} + # --- Case 1: master kill switch false → silent exit 0 ----------------------- OUT="$(cd "$UNRELATED" && build_input permission_prompt | env -u HOOK_TELEMETRY_SINK CLAUDE_PROJECT_DIR="$FAKE_REPO" \ @@ -202,19 +212,48 @@ else fi rm -f "$TEL" -# --- Case 13: slow sink (C1 fd1-leak detector) → hook returns << 3s --------- -SLOW="$(make_sink "cat >/dev/null; sleep 3")" -TS_START=$EPOCHREALTIME +# --- Case 13: slow sink (C1 fd1-leak detector) → hook does not wait on it ----- +# Invariant: the backgrounded telemetry sink must not keep fd1 open, so the +# hook's command substitution returns as soon as the hook exits, NOT after the +# sink finishes. A leak would make $() block until the sink closes fd1 — i.e. +# for the sink's whole sleep. +# +# We measure that differentially rather than against a fixed wall-clock bound. +# Spawn overhead is machine- and load-dependent (on Windows Git Bash a single +# hook is already ~1.6s, and parallel test suites push it to 4-10s), so any +# fixed threshold either false-fails under load or has to be set so high the +# sink must sleep >10s — which then lingers past the suite's EXIT cleanup and +# locks its stub file on Windows. A baseline run (fast sink, no sleep) captures +# the SAME ambient overhead as the slow run under the SAME capture; subtracting +# it cancels the overhead and isolates the leak signal. Under a leak the delta +# is ~SINK_SLEEP; with no leak it is ~0 (± scheduling jitter). Threshold is half +# the sleep: comfortably above jitter, comfortably below the leak signal. +SINK_SLEEP=6 +BASE_SINK="$(make_sink "cat >/dev/null")" +SLOW_SINK="$(make_sink "cat >/dev/null; sleep $SINK_SLEEP")" + +_TS0=$EPOCHREALTIME +# shellcheck disable=SC2034 # captured so $() blocks until fd1 closes (baseline overhead) +_OUT_BASE="$(run "$(build_input permission_prompt)" HOOK_TELEMETRY_SINK="$BASE_SINK")" +_TS1=$EPOCHREALTIME +BASE_MS=$(epoch_delta_ms "$_TS0" "$_TS1") + +_TS0=$EPOCHREALTIME # shellcheck disable=SC2034 # captured so $() blocks until fd1 closes — proves no fd1 leak -_OUT_SLOW="$(run "$(build_input permission_prompt)" HOOK_TELEMETRY_SINK="$SLOW")" +_OUT_SLOW="$(run "$(build_input permission_prompt)" HOOK_TELEMETRY_SINK="$SLOW_SINK")" RC_SLOW=$? -TS_END=$EPOCHREALTIME -SS="${TS_START%[.,]*}" SF="${TS_START#*[.,]}" -ES="${TS_END%[.,]*}" EF="${TS_END#*[.,]}" -SLOW_MS=$(((ES * 1000000 + 10#$EF - SS * 1000000 - 10#$SF) / 1000)) -echo " (C1 slow-sink elapsed: ${SLOW_MS}ms)" +_TS1=$EPOCHREALTIME +SLOW_MS=$(epoch_delta_ms "$_TS0" "$_TS1") + +DELTA_MS=$((SLOW_MS - BASE_MS)) +THRESHOLD_MS=$((SINK_SLEEP * 1000 / 2)) +echo " (C1 fd1-leak: base=${BASE_MS}ms slow=${SLOW_MS}ms delta=${DELTA_MS}ms, threshold <${THRESHOLD_MS}ms, sink sleeps ${SINK_SLEEP}s)" if [[ $RC_SLOW -eq 0 ]]; then ok "telemetry/slow-sink: hook exit 0"; else fail "telemetry/slow-sink: hook exit $RC_SLOW"; fi -if [[ $SLOW_MS -lt 2000 ]]; then ok "telemetry/slow-sink: returned in ${SLOW_MS}ms (<<3000 = no fd1 leak)"; else fail "telemetry/slow-sink: ${SLOW_MS}ms — fd1 leak blocks"; fi +if [[ $DELTA_MS -lt $THRESHOLD_MS ]]; then + ok "telemetry/slow-sink: hook did not wait for the sink (delta ${DELTA_MS}ms << ${SINK_SLEEP}s sleep = no fd1 leak)" +else + fail "telemetry/slow-sink: delta ${DELTA_MS}ms ≈ sink's ${SINK_SLEEP}s sleep — fd1 leak blocks \$() until the sink exits" +fi # --- Case 14: failing sink → hook exit 0 unaffected ------------------------- FAIL_FILE="$(mktemp)" From ffa565f9fa0055e140327eaeb5f643fe52b83704 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:41:33 -0400 Subject: [PATCH 2/2] test(desktop-notification): min-of-N baseline closes C1 leak false-negative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C1 fd1-leak detector subtracted a single fast-sink baseline from the slow-sink run. Sequential samples run under different scheduling, so one baseline descheduled >THRESHOLD_MS longer than the slow run could shrink DELTA_MS below threshold and let a real fd1 leak PASS — a false-negative, the detector reporting green while a leak exists (fail-open under load). Take the baseline as the minimum of several fast runs: only an inflated baseline can mask a leak, so the minimum reflects true-minimal overhead and keeps the leak signal (~SINK_SLEEP) in the delta. The inverse error (inflated slow sample, no leak) only re-fails a green run (fail-safe), so the slow run stays single. Suite passes 54/0; shellcheck clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HZqwoLTHPpPRzLpRGzbKRq --- plugins/desktop-notification/CHANGELOG.md | 4 +++ .../hooks/desktop-notification.test.sh | 27 ++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/plugins/desktop-notification/CHANGELOG.md b/plugins/desktop-notification/CHANGELOG.md index 58dd58c40..8733251f2 100644 --- a/plugins/desktop-notification/CHANGELOG.md +++ b/plugins/desktop-notification/CHANGELOG.md @@ -15,6 +15,10 @@ All notable changes to the `desktop-notification` plugin are documented here. Fo left the fixed bound with a thin-to-negative margin and false-failed even with no leak. The differential form cancels ambient overhead, so the check holds under load while still catching a real fd1 leak (the sink's whole sleep lands in the delta). + The baseline is the minimum of several fast runs so that one unluckily-descheduled + baseline sample cannot inflate the subtracted overhead and let a real leak pass as a + false-negative under load; an inflated *slow* sample only re-fails a green run + (fail-safe), so only the baseline is sampled repeatedly. ## [0.4.2] diff --git a/plugins/desktop-notification/hooks/desktop-notification.test.sh b/plugins/desktop-notification/hooks/desktop-notification.test.sh index 8c4d6e73e..052d1a1b3 100755 --- a/plugins/desktop-notification/hooks/desktop-notification.test.sh +++ b/plugins/desktop-notification/hooks/desktop-notification.test.sh @@ -228,15 +228,30 @@ rm -f "$TEL" # it cancels the overhead and isolates the leak signal. Under a leak the delta # is ~SINK_SLEEP; with no leak it is ~0 (± scheduling jitter). Threshold is half # the sleep: comfortably above jitter, comfortably below the leak signal. +# +# The baseline is the MINIMUM of several fast runs, not a single sample. A lone +# baseline that happened to be descheduled longer than the slow run would shrink +# DELTA_MS below the threshold and let a real fd1 leak PASS — a false-NEGATIVE, +# the detector reporting "no leak" while one exists (fail-open). Only an +# *inflated* baseline can mask a leak, so the minimum reflects true-minimal +# overhead and keeps the leak signal (~SINK_SLEEP) in the delta regardless of one +# unlucky sample. The opposite error — an inflated slow sample with no leak — +# only re-fails a green run (fail-safe, re-runnable), so just the baseline needs +# min-of-N; the slow run stays single (each slow sample costs SINK_SLEEP). SINK_SLEEP=6 +BASE_SAMPLES=3 BASE_SINK="$(make_sink "cat >/dev/null")" SLOW_SINK="$(make_sink "cat >/dev/null; sleep $SINK_SLEEP")" -_TS0=$EPOCHREALTIME -# shellcheck disable=SC2034 # captured so $() blocks until fd1 closes (baseline overhead) -_OUT_BASE="$(run "$(build_input permission_prompt)" HOOK_TELEMETRY_SINK="$BASE_SINK")" -_TS1=$EPOCHREALTIME -BASE_MS=$(epoch_delta_ms "$_TS0" "$_TS1") +BASE_MS="" +for ((_i = 0; _i < BASE_SAMPLES; _i++)); do + _TS0=$EPOCHREALTIME + # shellcheck disable=SC2034 # captured so $() blocks until fd1 closes (baseline overhead) + _OUT_BASE="$(run "$(build_input permission_prompt)" HOOK_TELEMETRY_SINK="$BASE_SINK")" + _TS1=$EPOCHREALTIME + _b=$(epoch_delta_ms "$_TS0" "$_TS1") + if [[ -z "$BASE_MS" || $_b -lt $BASE_MS ]]; then BASE_MS=$_b; fi +done _TS0=$EPOCHREALTIME # shellcheck disable=SC2034 # captured so $() blocks until fd1 closes — proves no fd1 leak @@ -247,7 +262,7 @@ SLOW_MS=$(epoch_delta_ms "$_TS0" "$_TS1") DELTA_MS=$((SLOW_MS - BASE_MS)) THRESHOLD_MS=$((SINK_SLEEP * 1000 / 2)) -echo " (C1 fd1-leak: base=${BASE_MS}ms slow=${SLOW_MS}ms delta=${DELTA_MS}ms, threshold <${THRESHOLD_MS}ms, sink sleeps ${SINK_SLEEP}s)" +echo " (C1 fd1-leak: base=${BASE_MS}ms (min of ${BASE_SAMPLES}) slow=${SLOW_MS}ms delta=${DELTA_MS}ms, threshold <${THRESHOLD_MS}ms, sink sleeps ${SINK_SLEEP}s)" if [[ $RC_SLOW -eq 0 ]]; then ok "telemetry/slow-sink: hook exit 0"; else fail "telemetry/slow-sink: hook exit $RC_SLOW"; fi if [[ $DELTA_MS -lt $THRESHOLD_MS ]]; then ok "telemetry/slow-sink: hook did not wait for the sink (delta ${DELTA_MS}ms << ${SINK_SLEEP}s sleep = no fd1 leak)"