dotfiles/claude/tests/statusline.isaacaudet.cache_test.sh
Jonny Barnes f2dbe61276
Refresh the usage bars every 5 minutes, not every hour
The 5h/7d bars were served from a cache with a one-hour TTL, so a bar
could sit unchanged for most of a 5-hour window. The TTL is now a named
config setting next to GIT_CACHE_SECS at 300s: worst case 12 requests an
hour, shared across every session through /tmp/claude.

The section header claimed "cached 60s" — inherited from upstream and
never true here — which is what set the expectation the code did not
meet. It now names the setting instead of a number that can drift.

The refresh is gated on the same width-tier check as the bars, so a
render that shows them is a render that refreshes them: the new tests
pin that from full width down to WRAP_FLOOR, and pin that one column
below it, where the bars are dropped, no request is paid for either.
Those widths are given in usable columns, as in the sibling width test,
so the suite does not depend on the padding in the live settings.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:07:19 +01:00

136 lines
5.7 KiB
Shell
Executable file

#!/bin/bash
# Tests for the usage-API cache in claude/statusline.isaacaudet.sh: how long a
# cached response is reused before the 5h/7d bars are refetched, and that any
# render which SHOWS the bars is also a render that refreshes them.
#
# Widths are given in USABLE columns, as in the sibling width test: the
# script derives those from COLUMNS minus statusLine.padding on both sides
# minus a margin, so a raw TERM_WIDTH would make these assertions depend on
# the padding in the live settings.json.
#
# Sibling variants in claude/ (statusline.burnrate.sh, statusline.original.sh)
# are NOT covered by these tests.
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh"
PAD=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null || echo 0)
OVERHEAD=$(( 2 * PAD + 1 ))
# Render into a throwaway cache directory. The live cache must not be touched:
# ~/.claude/statusline.sh is a symlink to the script under test, so a fixture
# written there is shown in the running TUI as real usage until it expires.
export STATUSLINE_CACHE_DIR="$(mktemp -d)"
CACHE="$STATUSLINE_CACHE_DIR/statusline-usage-cache.json"
REPO="$(mktemp -d)"
STUB="$(mktemp -d)"
cleanup() { rm -rf "$STATUSLINE_CACHE_DIR" "$REPO" "$STUB"; }
trap cleanup EXIT
git -C "$REPO" init -q 2>/dev/null
git -C "$REPO" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init 2>/dev/null
# A curl stub, first on PATH, records each call and replies with $CURL_REPLY.
# Every refresh in this file goes through it, so no test can reach the real
# usage API. The token is faked too, so the keychain is never read.
export CLAUDE_CODE_OAUTH_TOKEN=test-token
export CURL_CALLS="$STUB/calls"
cat > "$STUB/curl" <<'SH'
#!/bin/bash
echo call >> "$CURL_CALLS"
printf '%s' "$CURL_REPLY"
SH
chmod +x "$STUB/curl"
export PATH="$STUB:$PATH"
export CURL_REPLY='{"five_hour":{"utilization":42.0,"resets_at":"2026-08-27T15:40:00+00:00"},
"seven_day":{"utilization":44.0,"resets_at":"2026-08-28T02:00:00+00:00"},
"extra_usage":{"is_enabled":false},"limits":[]}'
pass=0; fail=0
ok() { echo " PASS $1"; pass=$((pass+1)); }
bad() { echo " FAIL $1"; [ -n "$2" ] && printf '%s\n' "$2" | sed 's/^/ /'; fail=$((fail+1)); }
stdin_json() {
CWD="$1" python3 -c '
import json, os
print(json.dumps({"model": {"display_name": "Opus 5"},
"cwd": os.environ["CWD"],
"cost": {"total_cost_usd": 0.5},
"context_window": {"context_window_size": 200000,
"current_usage": {"input_tokens": 1000}}}))'
}
# Render, with ANSI stripped. $1 = USABLE width.
render() {
stdin_json "$REPO" | TERM_WIDTH=$(( $1 + OVERHEAD )) bash "$SCRIPT" 2>&1 \
| sed $'s/\033\[[0-9;]*m//g'
}
# A cached response whose bars read 5% / 7%, aged $1 seconds.
cached_response_aged() {
cat > "$CACHE" <<'JSON'
{"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"},
"seven_day":{"utilization":7.0,"resets_at":"2026-08-28T02:00:00+00:00"},
"extra_usage":{"is_enabled":false},"limits":[]}
JSON
AGE="$1" FILE="$CACHE" python3 -c '
import os, time
f = os.environ["FILE"]; t = time.time() - int(os.environ["AGE"])
os.utime(f, (t, t))'
: > "$CURL_CALLS"
}
calls() { wc -l < "$CURL_CALLS" | tr -d ' '; }
# ------------------------------------------------------------------- 5min TTL
echo "Usage cache TTL (5 minutes):"
cached_response_aged 240
out=$(render 245)
[ "$(calls)" = "0" ] && ok "4min old: served from cache, no API call" \
|| bad "4min old: served from cache, no API call" "$out"
case "$out" in *"5%"*) ok "4min old: renders the cached percentage" ;;
*) bad "4min old: renders the cached percentage" "$out" ;; esac
cached_response_aged 360
out=$(render 245)
[ "$(calls)" = "1" ] && ok "6min old: refetched" || bad "6min old: refetched" "$out"
case "$out" in *"42%"*) ok "6min old: renders the fresh percentage" ;;
*) bad "6min old: renders the fresh percentage" "$out" ;; esac
case "$(cat "$CACHE")" in *"42.0"*) ok "6min old: fresh response written back" ;;
*) bad "6min old: fresh response written back" "$(cat "$CACHE")" ;; esac
# Half an hour was inside the old one-hour TTL: the bars would sit unchanged.
cached_response_aged 1800
out=$(render 245)
[ "$(calls)" = "1" ] && ok "30min old: refetched, not held for the hour" \
|| bad "30min old: refetched, not held for the hour" "$out"
# ------------------------------------------------- bars shown => bars refreshed
# The refresh is gated on the same tier check as the bars themselves, so the
# two must agree at every width: wrapped onto line two included, and 68 being
# WRAP_FLOOR, the narrowest width that still shows them.
echo
echo "Every width that shows the bars refreshes them:"
for u in 245 115 85 68; do
cached_response_aged 360
out=$(render "$u")
if case "$out" in *"5h"*) true ;; *) false ;; esac; then
[ "$(calls)" = "1" ] && ok "usable $u: bars shown and refreshed" \
|| bad "usable $u: bars shown but served stale" "$out"
else
bad "usable $u: expected the bars to be shown" "$out"
fi
done
# One column below the wrap floor the group is dropped entirely — nothing is
# shown, so nothing should be paid for either.
cached_response_aged 360
out=$(render 67)
case "$out" in *"5h"*) bad "usable 67: no bars at the narrow tier" "$out" ;;
*) ok "usable 67: no bars at the narrow tier" ;; esac
[ "$(calls)" = "0" ] && ok "usable 67: no API call when no bars are shown" \
|| bad "usable 67: no API call when no bars are shown" "$out"
echo
echo " $pass passed, $fail failed"
[ "$fail" -eq 0 ]