diff --git a/claude/statusline.isaacaudet.sh b/claude/statusline.isaacaudet.sh index 928cb56..0be5212 100755 --- a/claude/statusline.isaacaudet.sh +++ b/claude/statusline.isaacaudet.sh @@ -19,6 +19,7 @@ GIT_CACHE_SECS=10 # seconds to cache git status (git diff is slow on large USAGE_CACHE_SECS=300 # seconds to cache the usage API response (the 5h/7d bars) USAGE_RETRY_SECS=60 # seconds to wait before retrying a failed usage API fetch TOKEN_BAR_WIDTH=8 # width of token progress bar +SL_WORK_DAYS="${SL_WORK_DAYS:-12345}" # ISO weekdays you work: Mon=1 ... Sun=7 # Where the git and usage-API caches live. Overridable via the environment so a # test run can render into its own directory: ~/.claude/statusline.sh is a @@ -26,6 +27,14 @@ TOKEN_BAR_WIDTH=8 # width of token progress bar # the next redraw and shown as real usage until USAGE_CACHE_SECS is up. CACHE_DIR="${STATUSLINE_CACHE_DIR:-/tmp/claude}" +# "Now", overridable so a test can pin the day of the week: the pace figure +# counts the working days left before the reset, so it moves with today's +# weekday and would otherwise be unassertable. Only the pace code reads it -- +# the cache-age checks below stay on the real clock deliberately, so pinning a +# time cannot make a fresh fixture look stale and send a test to the network. +SL_NOW="${SL_NOW:-}" +now_ts() { [ -n "$SL_NOW" ] && printf '%s' "$SL_NOW" || date +%s; } + # Terminal width detection. # Claude Code exports COLUMNS for this subprocess. There is no controlling # terminal, so the stty fallback fails; when both fail we default to 80 @@ -169,6 +178,25 @@ build_bar() { printf "${bar_color}${f}${dim}${e}${reset}" } +# Colour for the sustainable %/day, measured against the window's OWN even-burn +# baseline (100 / workdays in the window) rather than a fixed number of points +# per day: on a five-day week healthy is 20%/day, so a threshold tuned to the +# 14.3%/day calendar baseline would still call 12%/day green -- by then two +# fifths of the budget has been overspent. The ratios reproduce the 12/8/5 +# thresholds at a 14.3 baseline, whatever SL_WORK_DAYS is set to. +# +# A high pace means plenty of runway per working day, so it reads cool; a low +# one means the rest of the week has to be rationed. Both arguments are in +# tenths, which keeps the comparison integer. +pacecol() { + local p=$1 base=$2 + if [ "$p" -ge $(( base * 84 / 100 )) ]; then printf '%s' "$green" + elif [ "$p" -ge $(( base * 56 / 100 )) ]; then printf '%s' "$yellow" + elif [ "$p" -ge $(( base * 35 / 100 )) ]; then printf '%s' "$orange" + else printf '%s' "$red" + fi +} + # ===== Git info with per-directory caching ===== get_git_info() { local dir="$1" @@ -278,6 +306,16 @@ iso_to_epoch() { return 1 } +# Local midnight of the day an epoch falls in, plus that day's ISO weekday +# (1 = Monday), as " ". BSD date first -- it is the hot path +# here -- then GNU date, which has no -v. +day_start_dow() { + date -j -r "$1" -v0H -v0M -v0S +'%s %u' 2>/dev/null && return 0 + local d + d=$(date -d "@$1" +%F 2>/dev/null) || return 1 + date -d "$d 00:00:00" +'%s %u' 2>/dev/null +} + format_reset_time() { local iso_str="$1" style="$2" [ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return @@ -389,6 +427,7 @@ short_model() { out="" rl_bare="" rl_lean="" +rl_pace="" rl_mid="" rl_rich="" @@ -597,6 +636,7 @@ if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then IFS= read -r seven_day_reset_iso IFS= read -r scoped_name IFS= read -r scoped_pct + IFS= read -r scoped_reset_iso IFS= read -r extra_enabled IFS= read -r extra_pct IFS= read -r extra_used @@ -612,6 +652,7 @@ $(echo "$usage_data" | jq -r ' (.seven_day.resets_at // ""), (($scoped.scope.model.display_name // "") | gsub("[\\n\\r\\t]"; " ")), ($scoped.percent | num | round), + ($scoped.resets_at // ""), (.extra_usage.is_enabled // false), (.extra_usage.utilization | num | round), ((.extra_usage.used_credits | num) / 100 * 100 | round / 100), @@ -619,9 +660,104 @@ $(echo "$usage_data" | jq -r ' ] | .[] | tostring' 2>/dev/null) EOF - # Three variants of the rate-limit group, richest first: - # rl_rich bars + reset times + extra usage - # rl_mid bars + extra usage + # ===== Sustainable burn: pace + trend ===== + # pace = the limit's remaining points divided by the WORKING days left + # before it resets, i.e. what can be spent per working day and still + # land on 100% exactly at the reset. A five-day week spreads 100 points + # over 5 days, so a fresh window paces at 20%/day; the 14.3%/day + # calendar figure quietly assumes the weekend is worked too. + # trend compares used% against a BAND rather than a point: during + # workday n of m, anything from (n-1)/m to n/m of the budget is on + # track, because the whole of today's allowance is today's to spend. A + # point built from complete workdays only made every Monday morning red + # the moment 3 points had gone. Ahead of the band means the limit will + # cap before the week is out, behind it means part of the subscription + # goes unused; a +-3 slack outside the band stops it flickering at the + # edges. On a non-workday no allowance is in play, so the band + # collapses to its floor. + # + # Whole workdays only, no time-of-day interpolation: the percentages + # arrive as integers anyway, and "three working days left" is the + # granularity the decision actually gets made at. + # + # Measured against the per-model limit whenever one is being shown -- + # that is the limit that runs out first, and the bar this annotates. + # Same gate as seg_scoped below, so pace always describes the bar it is + # printed next to. Its own resets_at wins, falling back to the 7-day + # timestamp (the two reset together) when the payload omits it. + seg_pace="" + if $SHOW_MODEL_LIMIT && [ -n "$scoped_name" ]; then + pace_used="${scoped_pct:-0}" + pace_reset_iso="$scoped_reset_iso" + else + pace_used="${seven_day_pct:-0}" + pace_reset_iso="" + fi + case "$pace_reset_iso" in ''|null) pace_reset_iso="$seven_day_reset_iso" ;; esac + + pace_epoch="" + case "$pace_reset_iso" in ''|null) ;; *) pace_epoch=$(iso_to_epoch "$pace_reset_iso") ;; esac + if [ -n "$pace_epoch" ]; then + ds_now=$(day_start_dow "$(now_ts)") + ds_reset=$(day_start_dow "$pace_epoch") + # Whole local days from today to the reset's day. Rounded rather + # than divided: a DST change makes one of those days 23 or 25 hours + # long, and truncation would lose a day either side of it. + pace_days="" + if [ -n "$ds_now" ] && [ -n "$ds_reset" ]; then + pace_days=$(( (${ds_reset%% *} - ${ds_now%% *} + 43200) / 86400 )) + fi + + # No strftime in macOS awk, so the weekday of each day in the + # window is derived from today's, which bash passes in. + pace_tv="" + [ -n "$pace_days" ] && pace_tv=$(awk \ + -v used="$pace_used" -v dtr="$pace_days" \ + -v dow0="${ds_now##* }" -v wd=",${SL_WORK_DAYS}," 'BEGIN{ + # Day indices, 0 = today. The reset is 7 days after the + # previous one, so the window is [dtr-7, dtr) and today has to + # lie inside it; anything else is stale or nonsense data. + if (dtr < 0 || dtr > 7 || dow0 < 1 || dow0 > 7) exit 1 + for (i = dtr - 7; i < dtr; i++) { + dow = ((dow0 - 1 + i) % 7 + 7) % 7 + 1 + if (index(wd, dow) == 0) continue + total++ + if (i < 0) elapsed++; else left++ # today counts as left + } + if (total <= 0) exit 1 # no workdays: no baseline + rem = 100 - used; if (rem < 0) rem = 0 + d = (left < 1) ? 1 : left # nothing but today: all that remains + pr = rem / d + # Today is workday elapsed+1 of total, so its band runs from + # elapsed/total to (elapsed+1)/total. Only its distance is + # reported, signed: + past the top, - short of the floor, 0 + # anywhere inside the band or its slack. Comparing here rather + # than in bash keeps the band edges off a second rounding. + lo = elapsed / total * 100 + hi = (index(wd, dow0) > 0) ? (elapsed + 1) / total * 100 : lo + dv = (used > hi + 3) ? used - hi : (used < lo - 3) ? used - lo : 0 + # One decimal below 10, where rounding starts to matter. The + # last two fields are pace and the baseline in tenths, for + # the integer comparison in pacecol. + printf "%s %.0f %.0f %.0f\n", \ + (pr < 10) ? sprintf("%.1f", pr) : sprintf("%.0f", pr), \ + dv, pr * 10, 1000 / total + }' 2>/dev/null) + + if [ -n "$pace_tv" ]; then + read -r pace_val pace_trend pace_tenths pace_base <<< "$pace_tv" + seg_pace=" $(pacecol "$pace_tenths" "$pace_base")${pace_val}%/d${reset}" + if [ "$pace_trend" -gt 0 ] 2>/dev/null; then seg_pace+=" ${red}▲+${pace_trend}${reset}" + elif [ "$pace_trend" -lt 0 ] 2>/dev/null; then seg_pace+=" ${cyan}▼${pace_trend}${reset}" + else seg_pace+=" ${green}✓${reset}" + fi + fi + fi + + # Four variants of the rate-limit group, richest first: + # rl_rich bars + reset times + pace + extra usage + # rl_mid bars + pace + extra usage + # rl_pace bars + pace # rl_lean bars only # The ladder at the end emits the richest one that fits the space it # has. Keeping a lean variant matters: extra-usage credits add ~30 @@ -646,9 +782,15 @@ EOF # rl_bare drops the per-model bar too: the last thing worth giving up, # and the only way to fit at all when wrapping is disabled and the # terminal is narrow. + # + # rl_pace is its own rung so pace is given up BEFORE the per-model bar + # it annotates: a bar with no pace still says something, a pace with no + # bar does not. seg_pace lands after the per-model percentage, or after + # the 7d one on an account with no per-model limit (seg_scoped empty). rl_bare="${seg_5h}${seg_7d}" rl_lean="${rl_bare}${seg_scoped}" - rl_mid="${rl_lean}${seg_extra}" + rl_pace="${rl_lean}${seg_pace}" + rl_mid="${rl_pace}${seg_extra}" rl_rich="$rl_mid" # Formatting the two reset timestamps costs ~10 subprocesses (date has @@ -665,7 +807,7 @@ EOF seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime") r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}↺ ${five_hour_reset}${reset}" r7=""; [ -n "$seven_day_reset" ] && r7=" ${dim}↺ ${seven_day_reset}${reset}" - rl_rich="${seg_5h}${r5}${seg_7d}${seg_scoped}${r7}${seg_extra}" + rl_rich="${seg_5h}${r5}${seg_7d}${seg_scoped}${seg_pace}${r7}${seg_extra}" fi fi fi @@ -679,20 +821,23 @@ fi # one column narrower, rung 3 stops fitting and the wrap that follows has room # for the timestamps as well. Only wrapping being switched off makes rl_bare the # answer. -# 1-3. one line: with resets / with extra usage / bars + per-model -# 4-6. two lines: same order, line two having room the single line lacked -# 7. one line, bars only -- WRAP_NARROW=false, the least-bad single line +# 1-4. one line: with resets / with extra usage / with pace / bars + per-model +# 5-8. two lines: same order, line two having room the single line lacked +# 9. one line, bars only -- WRAP_NARROW=false, the least-bad single line # Every branch is fit-checked, so no layout is chosen that would be clipped by # the renderer, whatever the branch name, cwd, model name or extra-usage width. if [ -n "$rl_bare" ]; then rich_len=$(vis_len "$rl_rich") + pace_len=$(vis_len "$rl_pace") lean_len=$(vis_len "$rl_lean") if [ $(( line1_len + 3 + rich_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_rich}" elif [ $(( line1_len + 3 + mid_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_mid}" + elif [ $(( line1_len + 3 + pace_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_pace}" elif [ $(( line1_len + 3 + lean_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_lean}" elif $WRAP_NARROW; then if [ "$rich_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich" elif [ "$mid_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_mid" + elif [ "$pace_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_pace" elif [ "$lean_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean" else out+=$'\n'"$rl_bare" fi diff --git a/claude/tests/run.sh b/claude/tests/run.sh index a195446..7f31dfc 100755 --- a/claude/tests/run.sh +++ b/claude/tests/run.sh @@ -13,6 +13,7 @@ status=0 for t in statusline.isaacaudet.payload_test.sh \ statusline.isaacaudet.thinking_test.sh \ statusline.isaacaudet.width_test.sh \ + statusline.isaacaudet.pace_test.sh \ statusline.isaacaudet.cache_test.sh; do echo "== $t" bash "$t" || status=1 diff --git a/claude/tests/statusline.isaacaudet.pace_test.sh b/claude/tests/statusline.isaacaudet.pace_test.sh new file mode 100755 index 0000000..9b121ff --- /dev/null +++ b/claude/tests/statusline.isaacaudet.pace_test.sh @@ -0,0 +1,261 @@ +#!/bin/bash +# Tests for the sustainable-burn indicator (pace + trend) in +# claude/statusline.isaacaudet.sh -- the variant symlinked from +# ~/.claude/statusline.sh. The sibling statusline.burnrate.sh, which has a +# richer sleep-aware version of the same idea, is NOT covered here. +# +# pace answers "how much of the weekly limit can I spend per WORKING day and +# still land on 100% at the reset", so every expectation below depends on +# which day of the week "now" is. SL_NOW pins it; without that the assertions +# would pass or fail according to the day the suite happened to run. +# +# TZ is pinned too: pace counts LOCAL calendar days, so the local day a UTC +# reset timestamp falls in -- and therefore how many workdays are left -- is +# otherwise a property of the machine running the tests. +export TZ=UTC + +SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh" +# Widths below are USABLE widths, converted to a TERM_WIDTH here: the script +# picks its layout from USABLE_WIDTH (columns minus padding on both sides minus +# a margin), so a raw TERM_WIDTH would land on a different rung of the ladder +# the moment statusLine.padding changed -- same convention as width_test.sh. +PAD=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null || echo 0) +[ "${PAD:-0}" -ge 0 ] 2>/dev/null || PAD=0 +OVERHEAD=$(( 2 * PAD + 1 )) + +export STATUSLINE_CACHE_DIR="$(mktemp -d)" +CACHE="$STATUSLINE_CACHE_DIR/statusline-usage-cache.json" +REPO="$(mktemp -d)" +SHIM="$(mktemp -d)" +cleanup() { rm -rf "$STATUSLINE_CACHE_DIR" "$REPO" "$SHIM"; } +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 + +# Seed the usage cache. Without a fixture the first render treats the rate +# limits as live and curls /api/oauth/usage with the real OAuth token, which +# makes the test non-hermetic and sensitive to account state. +# +# $1 = seven_day utilization, $2 = seven_day resets_at (a JSON value, so `null` +# is expressible), $3 = the limits array, $4 = the extra_usage block. +EXTRA_OFF='{"is_enabled":false}' +EXTRA_ON='{"is_enabled":true,"monthly_limit":200000,"used_credits":123456,"utilization":62.0}' +fixture() { + cat > "$CACHE" </dev/null || date -d "$1" +%s; } + +MON=$(at 2026-08-24T09:00:00) # Monday +WED=$(at 2026-08-26T09:00:00) # Wednesday +SAT=$(at 2026-08-29T09:00:00) # Saturday +R_MON_ISO='"2026-08-31T02:00:00+00:00"' # the following Monday, 2am: a fresh week at MON +R_WED_ISO='"2026-09-02T02:00:00+00:00"' # the Wednesday after that + +# $1 = SL_NOW, $2 = SL_WORK_DAYS, $3 = usable width (default 200). ANSI stripped. +render() { + printf '%s' "{\"model\":{\"display_name\":\"Opus 5\"},\"cwd\":\"$REPO\", + \"cost\":{\"total_cost_usd\":0.5},\"context_window\":{\"context_window_size\":200000, + \"current_usage\":{\"input_tokens\":1000}}}" \ + | SL_NOW="$1" SL_WORK_DAYS="$2" TERM_WIDTH="$(( ${3:-200} + OVERHEAD ))" \ + PATH="$SHIM:$PATH" bash "$SCRIPT" 2>&1 | sed $'s/\033\[[0-9;]*m//g' +} + +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)); } + +# $1 = rendered output, $2 = wanted substring, $3 = label +has() { case "$1" in *"$2"*) ok "$3";; *) bad "$3" "want '$2' in: $1";; esac; } +lacks() { case "$1" in *"$2"*) bad "$3" "unwanted '$2' in: $1";; *) ok "$3";; esac; } + +# A curl that fails loudly, first on PATH for every render above. Nothing here +# should reach the network: the fixture cache is fresh, so an invocation means +# the test would otherwise be spending the user's real OAuth token. +cat > "$SHIM/curl" <&2 +: > "$SHIM/curl-was-called" +exit 1 +EOF +chmod +x "$SHIM/curl" + +echo "Pace against the per-model limit (SL_WORK_DAYS=12345, Mon-Fri):" +# Fresh week on a Monday: the workdays left are Mon-Fri, so the whole 100 +# points spread over 5 days rather than the 7-day figure of 14. +fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" +o=$(render "$MON" 12345) +has "$o" "20%/d" "fresh week on a Monday over a 5-day week paces at 20%/d, not 14" +has "$o" "Fable ░░░░░░ 0% 20%/d" "pace sits inside the per-model segment, after its percentage" + +# Mid-week, overspent. Wed 09:00 with the reset the following Mon 02:00: +# workdays left = Wed, Thu, Fri = 3, so pace = (100-50)/3 = 16.7 -> "17%/d". +# Elapsed workdays = Mon, Tue = 2 of the window's 5, so expected = 40% and the +# trend is 50 - 40 = +10. +fixture 7.0 "$R_MON_ISO" "$(scoped 50 null)" +o=$(render "$WED" 12345) +has "$o" "17%/d" "mid-window Wednesday at 50% used paces at (100-50)/3 = 17%/d" + +# Saturday is not a workday, so today does not count itself. Reset on the +# Wednesday: workdays left = Mon, Tue = 2. +fixture 7.0 "$R_WED_ISO" "$(scoped 40 null)" +o=$(render "$SAT" 12345) +has "$o" "30%/d" "a Saturday does not count itself: pace spreads over Mon+Tue only" + +echo +echo "Trend against today's band, not against a point:" +# During workday n of m, anything between (n-1)/m and n/m of the budget is on +# track -- the whole of today's allowance is today's to spend. Comparing +# against a single point built from COMPLETE workdays only turned every Monday +# morning red as soon as 3 points had gone, which is what this replaces. +# $1 = used%, $2 = SL_NOW, $3 = reset iso, $4 = wanted token, $5 = label +trend_is() { + fixture 7.0 "$3" "$(scoped "$1" null)" + has "$(render "$2" 12345)" "$4" "$5" +} +# Monday of a fresh window is workday 1 of 5: the band is 0-20. +trend_is 6 "$MON" "$R_MON_ISO" "%/d ✓" "workday 1 of 5, 6% used: inside the day's own band" +trend_is 0 "$MON" "$R_MON_ISO" "%/d ✓" "workday 1 of 5, nothing used yet: on track" +trend_is 20 "$MON" "$R_MON_ISO" "%/d ✓" "workday 1 of 5, the whole day's allowance spent: still on track" +trend_is 30 "$MON" "$R_MON_ISO" "▲+10" "workday 1 of 5, 30% used: 10 points past the band top of 20" +# Wednesday with the reset the following Monday is workday 3 of 5: band 40-60. +trend_is 45 "$WED" "$R_MON_ISO" "%/d ✓" "workday 3 of 5, 45% used: inside the 40-60 band" +trend_is 70 "$WED" "$R_MON_ISO" "▲+10" "workday 3 of 5, 70% used: 10 points past the band top" +trend_is 30 "$WED" "$R_MON_ISO" "▼-10" "workday 3 of 5, 30% used: 10 points short of the band floor" +# The +-3 slack sits OUTSIDE the band, so the first flagged point is 4 past it. +trend_is 63 "$WED" "$R_MON_ISO" "%/d ✓" "3 points past the band top is still within the slack" +trend_is 64 "$WED" "$R_MON_ISO" "▲+4" "4 points past the band top breaks the slack" +trend_is 37 "$WED" "$R_MON_ISO" "%/d ✓" "3 points below the band floor is still within the slack" +trend_is 36 "$WED" "$R_MON_ISO" "▼-4" "4 points below the band floor breaks the slack" +# On a non-workday no day's allowance is in play, so the band collapses to the +# point where the elapsed workdays leave it. Saturday with the reset on the +# Wednesday: Wed, Thu, Fri elapsed of 5, so the point is 60. +trend_is 40 "$SAT" "$R_WED_ISO" "▼-20" "a non-workday collapses the band to a point and still trends" +trend_is 58 "$SAT" "$R_WED_ISO" "%/d ✓" "a non-workday within the slack of that point is on track" + +echo +echo "Degenerate windows:" +# Saturday with the reset on Monday 02:00 leaves NO workdays at all. The +# workdays-left clamp makes pace the whole remaining budget, which is the +# honest answer (nothing constrains you) rather than a division by zero. +fixture 7.0 "$R_MON_ISO" "$(scoped 7 null)" +o=$(render "$SAT" 12345) +has "$o" "93%/d" "no workdays left: pace is everything that remains" +p=$(printf '%s' "$o" | sed -n 's/.* \([0-9.]*\)%\/d.*/\1/p') +case "$p" in + ''|*[!0-9.]*) bad "the pace on a workday-less window is a plain number" "got '$p'" ;; + *) awk -v p="$p" 'BEGIN{exit !(p >= 0 && p <= 100)}' \ + && ok "the pace on a workday-less window stays within 0-100" \ + || bad "the pace on a workday-less window stays within 0-100" "got '$p'" ;; +esac + +# A 7-day working week is the old behaviour: 100/7 = 14.3, which the shared +# pv formatting (one decimal only below 10) renders as an integer. +fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" +o=$(render "$MON" 1234567) +has "$o" "14%/d" "SL_WORK_DAYS=1234567 reproduces the plain 100/7 figure" + +# Nonsense SL_WORK_DAYS leaves no workdays in the window at all, so there is +# no baseline to pace against and nothing to render. +o=$(render "$MON" "xyz") +lacks "$o" "%/d" "an SL_WORK_DAYS with no weekdays in it renders no pace" + +echo +echo "Which limit pace is computed against:" +# The per-model limit is the one that bites, so its own resets_at wins over the +# seven_day one even when the two disagree. +fixture 7.0 "\"not-a-date\"" "$(scoped 0 "$R_MON_ISO")" +o=$(render "$MON" 12345) +has "$o" "20%/d" "the scoped limit's own resets_at is used when it has one" + +# No per-model limit: fall back to the 7-day figure, and put pace where the +# per-model segment would have been. 20% used over 5 workdays -> 80/5 = 16. +fixture 20.0 "$R_MON_ISO" "[]" +o=$(render "$MON" 12345) +has "$o" "16%/d" "with no per-model limit pace falls back to the 7-day figure" +lacks "$o" "Fable" "the fallback really has no per-model segment" + +echo +echo "Missing and unusable reset times:" +# Every one of these must render the rest of the status line untouched: no +# pace, no trend, and no separator left dangling where they would have been. +no_pace() { # $1 = output, $2 = label + lacks "$1" "%/d" "$2: no pace" + lacks "$1" "│ │" "$2: no doubled separator" + case "$1" in + *'│') bad "$2: no trailing separator" "$1" ;; + *) ok "$2: no trailing separator" ;; + esac +} +fixture 7.0 null "$(scoped 0 null)" +no_pace "$(render "$MON" 12345)" "an absent reset time" +fixture 7.0 "\"not-a-date\"" "$(scoped 0 "\"also-not-a-date\"")" +no_pace "$(render "$MON" 12345)" "an unparseable reset time" +fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" +no_pace "$(render "$(at 2026-09-10T09:00:00)" 12345)" "a reset time already in the past" + +echo +echo "Width ladder (pace must never be the reason a line overflows):" +fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" +widths() { python3 -c " +import sys,re,unicodedata +for ln in sys.stdin.read().rstrip('\n').split('\n'): + p=re.sub(r'\033\[[0-9;]*m','',ln) + print(sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in p))"; } +over=0; toomany=0; orphan=0; dropped=0; shown=0 +for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68; do + o=$(render "$MON" 12345 "$u") + ws=$(printf '%s' "$o" | widths) + while read -r c; do [ "$c" -gt "$u" ] && over=1; done <<< "$ws" + [ "$(printf '%s\n' "$ws" | wc -l | tr -d ' ')" -gt 2 ] && toomany=1 + # pace is given up BEFORE the per-model bar it annotates, so it can never + # be the last thing standing. + case "$o" in + *"%/d"*) shown=1; case "$o" in *Fable*) ;; *) orphan=1 ;; esac ;; + *) case "$o" in *Fable*) dropped=1 ;; esac ;; + esac +done +[ "$over" = 0 ] && ok "no line exceeds the usable width at any width" || bad "no line exceeds the usable width at any width" +[ "$toomany" = 0 ] && ok "never more than two lines" || bad "never more than two lines" +[ "$orphan" = 0 ] && ok "pace never outlives the per-model bar" || bad "pace never outlives the per-model bar" +[ "$shown" = 1 ] && ok "pace is actually rendered somewhere in the sweep" || bad "pace is actually rendered somewhere in the sweep" +[ "$dropped" = 1 ] && ok "the pace rung is reachable: some width keeps the bar but drops pace" \ + || bad "the pace rung is reachable: some width keeps the bar but drops pace" + +# Pace outranks the extra-usage credits: the credits are ~30 columns of dollar +# readout, pace is the number this segment exists for, so the credits go first. +fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" "$EXTRA_ON" +pace_alone=0; credits_alone=0 +for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68; do + o=$(render "$MON" 12345 "$u") + case "$o" in + *"%/d"*) case "$o" in *extra*) ;; *) pace_alone=1 ;; esac ;; + *) case "$o" in *extra*) credits_alone=1 ;; esac ;; + esac +done +[ "$credits_alone" = 0 ] && ok "the extra-usage credits never outlive pace" \ + || bad "the extra-usage credits never outlive pace" +[ "$pace_alone" = 1 ] && ok "some width keeps pace but drops the credits" \ + || bad "some width keeps pace but drops the credits" + +echo +echo "Hermeticity:" +[ -f "$SHIM/curl-was-called" ] \ + && bad "curl is never invoked (the fixture cache is fresh)" \ + || ok "curl is never invoked (the fixture cache is fresh)" + +echo +echo " $pass passed, $fail failed" +[ "$fail" -eq 0 ]