diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 275509b..13b00c1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,13 +7,13 @@ }, "metadata": { "description": "Per-repository Claude account switching: keep personal and work subscriptions separate.", - "version": "0.14.0" + "version": "0.15.0" }, "plugins": [ { "name": "cprof", "source": "./", - "version": "0.14.0", + "version": "0.15.0", "description": "Select which Claude account a session uses, by default profile, per-repo pin, or directory rule. Each profile is its own config directory, so a work subscription and a personal one never share credentials.", "category": "workflow", "keywords": [ diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 626e927..f0d636a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cprof", - "version": "0.14.0", + "version": "0.15.0", "description": "Select which Claude account a session uses, by default profile, per-repo pin, or directory rule.", "author": { "name": "Diego Cotelo", diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ad77e..214b16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ release workflow reads its notes from the section matching the tag. ## [Unreleased] +## [0.15.0] + +### Added + +- A `weekly` statusline segment: the 7-day usage window as a bar, with the time + until it resets. It renders only once the window is at or above + `statusline.weekly_threshold` — a whole number from 1 to 100, 50 by default — + and renders nothing below it, so the line costs no space early in the week and + appears before the weekly cap ends a working day. Give it a line of its own in + `statusline.lines` and that line disappears with it. The figure comes from the + cache `cprof list` fills and is never fetched, because a Claude Code payload + carries the 5-hour window and the context but never the week, and because the + statusline must not add latency — so a profile whose usage has never been + fetched shows no weekly bar. + +### Changed + +- A reset a day or more away is reported as days and hours, `3d 13h`, rather + than as hours and minutes, `85h 40m` — the same instant, told legibly. The + 5-hour window cannot reach a day, so what it shows is unchanged. + ## [0.14.0] ### Added - `cprof doctor` reports two install problems that used to be invisible. It diff --git a/README.md b/README.md index f137e52..b3f59fd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Platform](https://img.shields.io/badge/Platform-macOS-1a1b27?style=for-the-badge&color=7aa2f7)](#install) [![Bash](https://img.shields.io/badge/Bash-3.2%2B-1a1b27?style=for-the-badge&color=414868)](CONTRIBUTING.md#development) [![Requires](https://img.shields.io/badge/Requires-jq-1a1b27?style=for-the-badge&color=7aa2f7)](#install) -[![Tests](https://img.shields.io/badge/Tests-1130%20assertions-1a1b27?style=for-the-badge&color=414868)](CONTRIBUTING.md#development) +[![Tests](https://img.shields.io/badge/Tests-1170%20assertions-1a1b27?style=for-the-badge&color=414868)](CONTRIBUTING.md#development) diff --git a/docs/statusline.md b/docs/statusline.md index a73f47a..126d340 100644 --- a/docs/statusline.md +++ b/docs/statusline.md @@ -56,6 +56,7 @@ defaults behind the render above: "lines": [["badge", "model", "dir", "git"], ["context", "usage"]], "bar": {"filled": "▓", "empty": "░", "width": 10}, "thresholds": {"warn": 70, "critical": 90}, + "weekly_threshold": 50, "colors": {"model": "cyan", "dir": "yellow", "git": "magenta", "branch": "cyan", "label": "dim"} } } @@ -81,6 +82,7 @@ rely on them: | `statusline.bar.filled` / `.empty` | exactly one character, and not an invisible one — a tab is one character and is rejected, with a message of its own | `▓` / `░` | | `statusline.bar.width` | a whole number from 1 to 40 | `10` | | `statusline.thresholds.warn` **and** `.critical` | both, together: whole numbers from 1 to 100 with `warn` below `critical` | `70` **and** `90` — setting only one, or an out-of-order pair, reverts both | +| `statusline.weekly_threshold` | a whole number from 1 to 100 | `50` — its own setting, so a rejected `thresholds` pair does not change it, and a rejected value here does not change the pair | | `statusline.colors.*` — wrong shape | a string, 1-19 characters, with no invisible character in it — a trailing tab is rejected, with a message of its own | its own default (`cyan` for `model`/`branch`, `yellow` for `dir`, `magenta` for `git`, `dim` for `label`) | | `statusline.colors.*` — right shape, unknown name | any name from [the palette](#colours), plus `dim` | *(not a fallback — see below)* | @@ -168,6 +170,35 @@ Usage ██···· 30% (resets in 2h 19m) | `git` | `git:(main*)`, the star meaning uncommitted changes | two git calls | | `context` | `Context ▓▓▓▓░░░░░░ 37%` | the payload | | `usage` | `Usage ▓▓▓░░░░░░░ 30% (resets in 2h 19m)` | the payload, else the profile's cached usage | +| `weekly` | `Usage Weekly ▓▓▓▓▓▓░░░░ 64% (resets in 3d 13h)`, and nothing at all below the threshold | the profile's cached usage — a payload never carries the week | + +### The weekly bar appears only when it matters + +`weekly` is the one segment whose presence depends on data. It renders when the +7-day window is at or above `statusline.weekly_threshold` (default 50) and +stays invisible below it, so the line costs nothing on a Monday and warns you +before the weekly cap ends a working day: + +```console +⚑ work │ [Opus 5 (1M context)] │ cprof git:(main*) +Context ▓▓▓▓░░░░░░ 39% │ Usage ▓▓░░░░░░░░ 18% (resets in 4h 2m) +Usage Weekly ▓▓▓▓▓▓░░░░ 64% (resets in 3d 13h) +``` + +Give it a line of its own and that line disappears with it — a configured line +whose segments all render nothing is dropped rather than printed empty. Put it +beside other segments and only the bar goes; the rest of the line stays. + +Two consequences of where the figure comes from. It is read from the same cache +`cprof list` fills and never fetched, because a Claude Code payload carries the +5-hour window and the context but never the week, and because the statusline +must not add latency — so a profile whose usage has never been fetched shows no +weekly bar. And a reset days away is rendered as `3d 13h` rather than `85h +40m`, which is the same instant told legibly. + +It is its own threshold, not `thresholds.warn`. Those two are colour +thresholds, validated as a pair; retuning them should not silently change when +a line appears. ## Colours diff --git a/scripts/cprof b/scripts/cprof index 478068f..0879ed3 100755 --- a/scripts/cprof +++ b/scripts/cprof @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -u -CP_VERSION='0.14.0' +CP_VERSION='0.15.0' # The plugin's own marketplace and scoped name, consumed by cp_cmd_update. # 'update' always targets this specific listing — it is not something a user # configures, so it lives next to CP_VERSION rather than in ~/.cprof.json. diff --git a/scripts/lib/statusline.sh b/scripts/lib/statusline.sh index cc5cd29..63c03d6 100644 --- a/scripts/lib/statusline.sh +++ b/scripts/lib/statusline.sh @@ -108,7 +108,7 @@ cp_sl_bar() { cp_sl_config() { local cfg="${1:-}" out rc=0 four=0 nl local d_layout='[["badge","model","dir","git"],["context","usage"]]' - local d_fill='▓' d_empty='░' d_width=10 d_warn=70 d_crit=90 + local d_fill='▓' d_empty='░' d_width=10 d_warn=70 d_crit=90 d_weekly=50 local d_model=cyan d_dir=yellow d_git=magenta d_branch=cyan d_label=dim nl=' ' @@ -117,9 +117,10 @@ cp_sl_config() { --argjson deflayout "$d_layout" \ --arg fill "$d_fill" --arg empty "$d_empty" \ --argjson width "$d_width" --argjson warn "$d_warn" --argjson crit "$d_crit" \ + --argjson weekly "$d_weekly" \ --arg model "$d_model" --arg dir "$d_dir" --arg git "$d_git" \ --arg branch "$d_branch" --arg label "$d_label" ' - def known: ["badge","model","dir","git","context","usage"]; + def known: ["badge","model","dir","git","context","usage","weekly"]; # Nothing below character 32 in a value that is kept. These four lines are # read by their delimiters, and an invisible character collides with them: # a tab shifts every field after it on its row -- so the directory colour @@ -146,10 +147,14 @@ cp_sl_config() { | (whole($t.warn; 1; 100; 0)) as $w | (whole($t.critical; 1; 100; 0)) as $cr | (if $w > 0 and $cr > 0 and $w < $cr then [$w, $cr] else [$warn, $crit] end) as $th + # Its own setting, not a third member of the pair above: warn and critical + # are colour thresholds validated together, and a bad pair must not drag + # the visibility threshold for the weekly bar down with it. + | (whole($s.weekly_threshold; 1; 100; $weekly)) as $wk | ([ $layout[] | join(" ") ] | join(";")), ([glyph($b.filled; $fill), glyph($b.empty; $empty), (whole($b.width; 1; 40; $width) | tostring)] | join("\t")), - ($th | map(tostring) | join("\t")), + (($th + [$wk]) | map(tostring) | join("\t")), ([pick($c.model; $model), pick($c.dir; $dir), pick($c.git; $git), pick($c.branch; $branch), pick($c.label; $label)] | join("\t")) ' 2>/dev/null)" || rc=$? @@ -161,9 +166,9 @@ cp_sl_config() { printf '%s\n' "$out" return 0 fi - printf '%s\n%s\t%s\t%s\n%s\t%s\n%s\t%s\t%s\t%s\t%s\n' \ + printf '%s\n%s\t%s\t%s\n%s\t%s\t%s\n%s\t%s\t%s\t%s\t%s\n' \ "$(printf '%s' "$d_layout" | jq -r '[.[] | join(" ")] | join(";")')" \ - "$d_fill" "$d_empty" "$d_width" "$d_warn" "$d_crit" \ + "$d_fill" "$d_empty" "$d_width" "$d_warn" "$d_crit" "$d_weekly" \ "$d_model" "$d_dir" "$d_git" "$d_branch" "$d_label" } @@ -227,7 +232,7 @@ cp_sl_config() { # back to, and one bad section can never suppress another's report. cp_sl_config_problems() { local cfg="${1:-}" key name default state colors_ok tab defaults esc_def - local d_fill='' d_empty='' d_width='' d_warn='' d_crit='' + local d_fill='' d_empty='' d_width='' d_warn='' d_crit='' d_weekly='' local d_model='' d_dir='' d_git='' d_branch='' d_label='' tab="$(printf '\t')" # The fallbacks these messages name are read off the resolver itself, @@ -236,7 +241,7 @@ cp_sl_config_problems() { { read -r _ IFS="$tab" read -r d_fill d_empty d_width - IFS="$tab" read -r d_warn d_crit + IFS="$tab" read -r d_warn d_crit d_weekly IFS="$tab" read -r d_model d_dir d_git d_branch d_label } </dev/null # Colours last, and in bash: cp_sl_code decides what a usable name is, and @@ -542,7 +555,8 @@ cp_sl_assemble() { # resolved configuration says. # # Each segment renders into its own shell variable -- CP_SL_badge, -# CP_SL_model, CP_SL_dir, CP_SL_git, CP_SL_context, CP_SL_usage -- holding +# CP_SL_model, CP_SL_dir, CP_SL_git, CP_SL_context, CP_SL_usage, +# CP_SL_weekly -- holding # only that segment's own text, with no separator. cp_sl_assemble then walks # the configured layout and joins what is there. A segment the layout does # not name is never rendered at all, so an unconfigured git segment runs no @@ -564,7 +578,8 @@ cp_cmd_statusline() { # layout's own segment names, which is why nothing in this function appears # to use them. local CP_SL_badge='' CP_SL_model='' CP_SL_dir='' CP_SL_git='' - local CP_SL_context='' CP_SL_usage='' + local CP_SL_context='' CP_SL_usage='' CP_SL_weekly='' + local th_weekly='' w_data='' w_pct='' w_at='' w_reset='' w_bar='' w_code='' while [ "$#" -gt 0 ]; do case "$1" in @@ -588,6 +603,7 @@ cp_cmd_statusline() { thresh_cfg="$(printf '%s' "$config" | sed -n '3p')" th_warn="$(printf '%s' "$thresh_cfg" | cut -f1)" th_crit="$(printf '%s' "$thresh_cfg" | cut -f2)" + th_weekly="$(printf '%s' "$thresh_cfg" | cut -f3)" colors_cfg="$(printf '%s' "$config" | sed -n '4p')" col_model="$(cp_sl_code "$(printf '%s' "$colors_cfg" | cut -f1)" 2>/dev/null)" col_dir="$(cp_sl_code "$(printf '%s' "$colors_cfg" | cut -f2)" 2>/dev/null)" @@ -716,6 +732,40 @@ cp_cmd_statusline() { fi fi + # The 7-day window, shown only once it is worth watching. Cache-only by + # nature: a Claude Code payload carries the 5-hour window and the context, + # never the week, so this reads the cache `cprof list` fills and never + # fetches -- the statusline must not add latency. Below the threshold the + # variable stays empty and cp_sl_assemble drops the line, which is how a + # quiet week costs no screen space. + if cp_sl_wants "$layout" weekly; then + w_data="$(cp_usage_read_cached_only "$name" 2>/dev/null)" + w_pct="$(cp_usage_pct "$w_data" seven_day 2>/dev/null)" + case "$w_pct" in ''|*[!0-9]*) w_pct='' ;; esac + case "$th_weekly" in ''|*[!0-9]*) th_weekly=50 ;; esac + if [ -n "$w_pct" ] && [ "$w_pct" -ge "$th_weekly" ]; then + w_at="$(cp_usage_resets_at "$w_data" seven_day 2>/dev/null)" + case "$w_at" in + '') ;; + *[!0-9]*) w_at="$(cp_time_epoch "$w_at")" || w_at='' ;; + esac + if [ -n "$w_at" ]; then + w_reset="$(cp_usage_reset_in "$w_at")" || w_reset='' + fi + w_bar="$(cp_usage_bar "$w_pct" "$b_fill" "$b_empty" "$b_width")" + w_code="$(cp_color_code "$(cp_usage_severity_colour "$w_pct" "$th_warn" "$th_crit" 2>/dev/null)" 2>/dev/null)" + if [ "$colour_on" -eq 1 ]; then + CP_SL_weekly="$(printf '%sUsage Weekly%s %s \033[%sm%s%%\033[0m' \ + "$label_open" "$label_close" "$(cp_sl_bar "$w_bar" "$w_code" "$b_empty")" "$w_code" "$w_pct")" + [ -n "$w_reset" ] && CP_SL_weekly="$CP_SL_weekly$(printf ' %s(resets in %s)%s' "$label_open" "$w_reset" "$label_close")" + else + # shellcheck disable=SC2034 # read by cp_sl_assemble via eval + CP_SL_weekly="Usage Weekly $w_bar $w_pct%" + [ -n "$w_reset" ] && CP_SL_weekly="$CP_SL_weekly (resets in $w_reset)" + fi + fi + fi + cp_sl_assemble "$layout" "$sep" return 0 } diff --git a/scripts/lib/usage.sh b/scripts/lib/usage.sh index 1a13fea..d8e1fde 100644 --- a/scripts/lib/usage.sh +++ b/scripts/lib/usage.sh @@ -279,17 +279,23 @@ cp_usage_detail() { return 0 } -# cp_usage_reset_in [] -> "4h 37m", "37m" or "<1m"; -# nothing, return 1, when the reset is not a future epoch. +# cp_usage_reset_in [] -> "3d 13h", "4h 37m", "37m" or +# "<1m"; nothing, return 1, when the reset is not a future epoch. +# +# The coarsest useful pair of units, never three: a 7-day window resets days +# out, where hours alone stop being readable -- 85h 40m and 3d 13h are the same +# instant and only one of them is legible. Minutes are dropped once a day is +# involved for the same reason. cp_usage_reset_in() { - local at="${1:-}" now="${2:-}" left h m + local at="${1:-}" now="${2:-}" left d h m case "$at" in ''|*[!0-9]*) return 1 ;; esac [ -n "$now" ] || now="$(date +%s)" case "$now" in ''|*[!0-9]*) return 1 ;; esac left=$(( at - now )) [ "$left" -gt 0 ] || return 1 - h=$(( left / 3600 )); m=$(( (left % 3600) / 60 )) - if [ "$h" -gt 0 ]; then printf '%sh %sm\n' "$h" "$m" + d=$(( left / 86400 )); h=$(( left / 3600 )); m=$(( (left % 3600) / 60 )) + if [ "$d" -gt 0 ]; then printf '%sd %sh\n' "$d" "$(( (left % 86400) / 3600 ))" + elif [ "$h" -gt 0 ]; then printf '%sh %sm\n' "$h" "$m" elif [ "$m" -gt 0 ]; then printf '%sm\n' "$m" else printf '<1m\n' fi diff --git a/tests/test_statusline.sh b/tests/test_statusline.sh index 52f8b30..a9c3107 100755 --- a/tests/test_statusline.sh +++ b/tests/test_statusline.sh @@ -148,6 +148,78 @@ out="$(printf '{"cwd":"%s","context_window":{"used_percentage":5}}' "$R" | NO_CO assert_eq "⚑ work │ repo git:($(gitq rev-parse --short HEAD)) Context $(cp_usage_bar 5) 5% │ Usage $(cp_usage_bar 73) 73%" "$out" \ 'no model in the payload: that field is skipped, the rest still renders' +# --- the weekly bar: shown only once the 7-day window is worth watching ---- +# The 7-day figure is cache-only by nature: a Claude Code payload carries the +# 5-hour window and the context, never the week, so this segment reads the +# same cache `cprof list` fills and never fetches. +wk_at="$(date -u -r $(( $(date +%s) + 3*86400 + 13*3600 + 30*60 )) '+%Y-%m-%dT%H:%M:%SZ')" +wk_cache() { # $1 = seven_day utilization + printf '{"fetched_at":1,"five_hour":{"utilization":20},"seven_day":{"utilization":%s,"resets_at":"%s"},"limits":[]}\n' \ + "$1" "$wk_at" > "$CP_T_TMP/state/usage/work.json" +} +wk_cfg() { # $1 = the statusline block, or empty for the default + cp_t_write_config </dev/null)" \ + 'past the threshold: the weekly bar, with days and hours to reset' + +wk_cache 50 +assert_eq "⚑ work +Usage Weekly $(cp_usage_bar 50) 50%" \ + "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null | sed 's/ (resets in .*)$//')" \ + 'exactly at the threshold it is shown' + +wk_cache 49 +assert_eq '⚑ work' "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null)" \ + 'below the threshold the segment renders nothing and its line vanishes' + +wk_cfg '"weekly_threshold":70'; wk_cache 64 +assert_eq '⚑ work' "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null)" \ + 'a higher configured threshold hides a percentage the default would show' +wk_cache 70 +assert_eq '1' "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null | grep -c 'Usage Weekly')" \ + 'and shows it once the window reaches that threshold' + +wk_cfg '"weekly_threshold":1'; wk_cache 0 +assert_eq '⚑ work' "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null)" \ + 'a zero percentage is below every valid threshold' + +# A cache with no seven_day at all, and no cache at all: nothing to show, +# nothing to fail. +wk_cfg '' +printf '{"fetched_at":1,"five_hour":{"utilization":20},"limits":[]}\n' > "$CP_T_TMP/state/usage/work.json" +assert_eq '⚑ work' "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null)" \ + 'a cache without a 7-day window renders no weekly bar' +rm -f "$CP_T_TMP/state/usage/work.json" +assert_eq '⚑ work' "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null)" \ + 'no cache at all renders no weekly bar' +assert_ok bash -c "NO_COLOR=1 '$CLI' statusline >/dev/null 2>&1" + +# The configured glyphs and width apply to this bar like any other. +wk_cfg '"bar":{"filled":"#","empty":".","width":6}'; wk_cache 64 +assert_eq "Usage Weekly $(cp_usage_bar 64 '#' '.' 6) 64%" \ + "$(NO_COLOR=1 "$CLI" statusline 2>/dev/null | sed -n '2p' | sed 's/ (resets in .*)$//')" \ + 'the weekly bar honours the configured glyphs and width' + +# Restore the fixture the rest of the file expects. +rm -f "$CP_T_TMP/state/usage/work.json" +cp_t_write_config < -> one line per key the resolver did not keep cp_t_sl_fallbacks() { local cfg="$1" resolved tab key rv state - local rfill='' rempty='' rwidth='' rwarn='' rcrit='' + local rfill='' rempty='' rwidth='' rwarn='' rcrit='' rweekly='' local rmodel='' rdir='' rgit='' rbranch='' rlabel='' tab="$(printf '\t')" # The block and the sections: structural, so no resolved value is needed. @@ -783,14 +887,14 @@ cp_t_sl_fallbacks() { { read -r _ IFS="$tab" read -r rfill rempty rwidth - IFS="$tab" read -r rwarn rcrit + IFS="$tab" read -r rwarn rcrit rweekly IFS="$tab" read -r rmodel rdir rgit rbranch rlabel } </dev/null # The colours have a second resolution stage that cp_sl_code owns: a name # pick() keeps but the palette does not know is rendered plain, which is a @@ -869,6 +974,12 @@ long31="$(printf 'x%.0s' $(seq 1 31))" cfg31="$(printf '{"statusline":{"colors":{"label":"%s"}}}' "$long31")" SL_RULE_CFG=( '{}' + '{"statusline":{"weekly_threshold":0}}' + '{"statusline":{"weekly_threshold":101}}' + '{"statusline":{"weekly_threshold":"40"}}' + '{"statusline":{"weekly_threshold":50.5}}' + '{"statusline":{"weekly_threshold":40}}' + '{"statusline":{"thresholds":{"warn":80,"critical":50},"weekly_threshold":40}}' '{"statusline":null}' '{"statusline":false}' '{"statusline":""}' @@ -945,7 +1056,7 @@ assert_eq "$LINES_BAD" "$(cp_sl_config_problems '{"statusline":{"lines":[]}}')" assert_eq "$LINES_BAD" "$(cp_sl_config_problems '{"statusline":{"lines":[[]]}}')" \ 'a lines array of empty lines is reported' assert_eq "$LINES_BAD -statusline.lines: unknown segment \"nonsense\" (known: badge model dir git context usage)" \ +statusline.lines: unknown segment \"nonsense\" (known: badge model dir git context usage weekly)" \ "$(cp_sl_config_problems '{"statusline":{"lines":[["nonsense"]]}}')" \ 'a layout whose only segment is unknown is reported both ways' assert_eq '' "$(cp_sl_config_problems '{"statusline":{"lines":[["badge"],"junk"]}}')" \ diff --git a/tests/test_usage.sh b/tests/test_usage.sh index 22652dd..1759dc4 100644 --- a/tests/test_usage.sh +++ b/tests/test_usage.sh @@ -431,6 +431,16 @@ now=1700000000 assert_eq '2h 19m' "$(cp_usage_reset_in $((now + 2*3600 + 19*60 + 30)) "$now")" 'reset_in renders hours and minutes' assert_eq '37m' "$(cp_usage_reset_in $((now + 37*60 + 5)) "$now")" 'reset_in renders minutes alone under an hour' assert_eq '<1m' "$(cp_usage_reset_in $((now + 20)) "$now")" 'reset_in renders <1m under a minute' +# A 7-day window resets days out, where hours alone stop being readable: 85h +# 13m is the same instant as 3d 13h and nobody reads the first one. +assert_eq '3d 13h' "$(cp_usage_reset_in $((now + 3*86400 + 13*3600 + 40*60)) "$now")" \ + 'reset_in renders days and hours once a day out' +assert_eq '1d 0h' "$(cp_usage_reset_in $((now + 86400)) "$now")" \ + 'reset_in switches to days at exactly 24h' +assert_eq '23h 59m' "$(cp_usage_reset_in $((now + 23*3600 + 59*60 + 30)) "$now")" \ + 'reset_in keeps hours and minutes just under a day' +assert_eq '6d 23h' "$(cp_usage_reset_in $((now + 6*86400 + 23*3600 + 59*60)) "$now")" \ + 'reset_in renders a nearly-full 7-day window' assert_fail cp_usage_reset_in "$now" "$now" # a reset that is due assert_fail cp_usage_reset_in $((now - 5)) "$now" # a reset already past assert_fail cp_usage_reset_in 'soon' "$now" # a non-epoch reset