diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc5584277..dbca8e5c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,17 @@ name: ci on: + # The only way to run the rate-limit-guard bench lanes in CI. They are gated + # on BENCH_LANES inside their suite because spawning a lane is running a + # benchmark, and a scheduled lane of their own would be unread advisory + # automation outside `ci-status`. A dispatch runs the full contract corpus, + # so this input only decides whether the lanes run inside it. + workflow_dispatch: + inputs: + bench_lanes: + description: "Run the rate-limit-guard statusline bench lanes" + type: boolean + default: false push: branches: [main] pull_request: @@ -1492,6 +1503,9 @@ jobs: if: needs.changes.outputs.run_tests == 'true' env: BASE_REF: ${{ github.base_ref }} + # Empty on every event but a dispatch that asked for the lanes, which + # is what the gate in the bench suite reads as "stay deferred". + BENCH_LANES: ${{ inputs.bench_lanes && '1' || '' }} run: | if [ "$GITHUB_EVENT_NAME" != pull_request ]; then scripts/run-plugin-tests.sh --jobs 3 --shard "$LEG/$LEGS" diff --git a/plugins/rate-limit-guard/.claude-plugin/plugin.json b/plugins/rate-limit-guard/.claude-plugin/plugin.json index 96a256eb4..2bf692e1c 100644 --- a/plugins/rate-limit-guard/.claude-plugin/plugin.json +++ b/plugins/rate-limit-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "rate-limit-guard", - "version": "0.8.18", + "version": "0.8.19", "description": "Shared rate-limit guard for loop lanes: a statusline wrapper tees the subscription rate-limit windows to a fixed machine-scope file, a StopFailure hook records rate-limit stops reactively, and a reader contract fixes how consuming sessions pause and resume.", "author": { "name": "Melodic Software", diff --git a/plugins/rate-limit-guard/CHANGELOG.md b/plugins/rate-limit-guard/CHANGELOG.md index 30be65ba9..263ca4015 100644 --- a/plugins/rate-limit-guard/CHANGELOG.md +++ b/plugins/rate-limit-guard/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the `rate-limit-guard` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.8.19] + +### Changed + +- `bench.test.sh` gates its five lane cases on `BENCH_LANES`. Without it the suite runs only the lib-helper assertions and prints a `SKIP:` line naming the coverage that did not run; `BENCH_LANES=1` runs every case, locally or through `ci.yml`'s `bench_lanes` dispatch input. + ## [0.8.18] ### Changed diff --git a/plugins/rate-limit-guard/bench/README.md b/plugins/rate-limit-guard/bench/README.md index e92e1aea0..ed6c7d071 100644 --- a/plugins/rate-limit-guard/bench/README.md +++ b/plugins/rate-limit-guard/bench/README.md @@ -83,9 +83,19 @@ HOME="$(mktemp -d)" bash plugins/rate-limit-guard/bench/bench-idle.sh The **benchmarks gate nothing**. Wall-clock numbers on shared CI runners are noise, so no lane's timing ever runs in CI. What does run is `bench.test.sh`, a contract smoke suite discovered by -`scripts/run-plugin-tests.sh` like every other `*.test.sh`: it unit-tests the lib helpers and -runs each lane once with tiny parameters against the repo tee under an isolated `HOME`, asserting -behaviour and output shape, never timing. That keeps the harness runnable from a clean checkout -(an unrunnable harness is exactly the defect that made #2521's measurements unreproducible) and -maps these files into `scripts/affected-tests.sh` coverage. The tee's behavioural coverage lives -in `../scripts/statusline-tee.test.sh`. +`scripts/run-plugin-tests.sh` like every other `*.test.sh`, which maps these files into +`scripts/affected-tests.sh` coverage. It splits in two: + +- **Every run** unit-tests the lib helpers: `median`, `pace_sleep_arg`, `now_ms` and the refusal + on a bash without `EPOCHREALTIME`. No lane is spawned, so it costs milliseconds. +- **`BENCH_LANES=1`** adds the lane cases: one tiny-parameter run of each lane against the repo + tee under an isolated `HOME`, plus the two failing-render aborts, asserting behaviour and + output shape, never timing. Spawning a lane is running a benchmark whatever the parameters, so + those five cases are gated; without the variable the suite prints a `SKIP:` line and the + runner's summary names the coverage that did not run. + +Run them locally with `BENCH_LANES=1 bash plugins/rate-limit-guard/bench/bench.test.sh`, or in CI +by dispatching `ci.yml` with its `bench_lanes` input set. That is the deliberate run that keeps +the harness runnable from a clean checkout; an unrunnable harness is exactly the defect behind the +unreproducible measurements in #2521. The tee's behavioural coverage lives in +`../scripts/statusline-tee.test.sh`. diff --git a/plugins/rate-limit-guard/bench/bench.test.sh b/plugins/rate-limit-guard/bench/bench.test.sh index f866e57fd..1b4c6745e 100755 --- a/plugins/rate-limit-guard/bench/bench.test.sh +++ b/plugins/rate-limit-guard/bench/bench.test.sh @@ -8,6 +8,12 @@ # unrunnable harness is exactly the defect that let #2521's measurements go # unreproducible (#2582). # +# The lane runs are GATED on BENCH_LANES: running a lane is running a benchmark +# however small its parameters, so an ordinary CI run stops after the lib +# assertions and reports the lanes as skipped coverage. BENCH_LANES=1 runs them +# — locally, or in CI through ci.yml's `bench_lanes` dispatch input, which is +# the deliberate run the #2582 guard rests on. +# # Self-contained: defines its own assertion helpers — installed plugins are # cache-isolated with no shared test lib. @@ -26,6 +32,13 @@ ok() { echo "ok: $*" PASS=$((PASS + 1)) } +# One summary shape for both exits: the gated stop after the lib assertions and +# the full run. +summary() { + echo + echo "PASS=$PASS FAIL=$FAIL" + [[ $FAIL -eq 0 ]] +} WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT @@ -77,6 +90,25 @@ else fail "lib: EPOCHREALTIME guard rc=$RC out=$OUT" fi +# --- the gate ---------------------------------------------------------------- +# Everything above asserts pure functions and costs milliseconds. Everything +# below SPAWNS the lanes, which is a benchmark run whatever the parameters, and +# is the only part of this suite that spends real wall-clock seconds on a +# shared runner. +# +# The deferral prints a SKIP line rather than counting an ok. scripts/ +# run-plugin-tests.sh reads `^SKIP:` and names the suite under "Suites with +# skipped coverage (exit 0 here is NOT evidence those cases ran)", which is +# what this is: five cases that did not run. An ok would make the aggregate +# read as full coverage. --strict-skips therefore fails here, correctly — a +# caller declaring a fully provisioned environment is asking for every case to +# run, and BENCH_LANES=1 is how it gets them. +if [[ -z "${BENCH_LANES:-}" ]]; then + echo "SKIP: bench lanes deferred; set BENCH_LANES=1 to run them" + summary + exit +fi + # --- bench-idle: smoke run against the repo tee, isolated HOME --------------- HOME1="$WORK/home1" mkdir -p "$HOME1" @@ -132,6 +164,4 @@ else fail "trace-probe: rc=$RC out=${OUT:0:400}" fi -echo -echo "PASS=$PASS FAIL=$FAIL" -[[ $FAIL -eq 0 ]] +summary diff --git a/scripts/run-plugin-tests-serial.txt b/scripts/run-plugin-tests-serial.txt index 8e62de661..b386522cc 100644 --- a/scripts/run-plugin-tests-serial.txt +++ b/scripts/run-plugin-tests-serial.txt @@ -39,7 +39,10 @@ plugins/context-guard/hooks/zone-gate.test.sh # A/B timing harness; its verdicts are elapsed-time comparisons. plugins/performance/scripts/ab.test.sh -# The statusline bench measures render latency. +# The statusline bench measures render latency. Its lane runs are gated on +# BENCH_LANES, so an ordinary run stops before them and needs no seat here; the +# entry is for the runs that DO set it (a local full corpus with the lanes +# enabled), where the lanes measure exactly what contention distorts. plugins/rate-limit-guard/bench/bench.test.sh # Cancellation-window cases kill a parked rename shim on a short fuse.