diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ee9a4..8aa9c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## v0.23.0 — 2026-07-29 — the trace chip, and a second line that holds its edge + +**New: cctrace trace chip.** A session whose wire is being captured by +[cctrace](https://github.com/thevibeworks/cctrace) (`deva --trace`, or +`cctrace` directly) now says so: `[cctrace:9317]` on the left, next to +path and branch — the port is the live trace UI on localhost. Session +identity, not pressure, so it's dim in the neutral lane. Detection takes +the strongest signal available: the trace env cctrace exports into the +traced process (`CCTRACE_SERVER_PORT`), the capture's CA plumbing +(`NODE_EXTRA_CA_CERTS` under a cctrace dir), or deva's `DEVA_TRACE=1`; +plumbing-only captures resolve the port through cctrace's live-instance +registry — by session id (sid8 prefix: the registry stores ids redacted +past the first 8 hex), then project path, then by being the only live +capture; the fallbacks trust heartbeat-fresh entries only, since crashed +runs leave "live" files behind. No resolvable port still shows a bare +`[cctrace]`: "recorded" matters even portless. + +**Fixed: the advisor row right-aligns to line 1's actual edge.** The +statusline runs with stdout on a pipe, where `tput cols` answers a flat +80 no matter how wide the terminal is — line 1 overflowed the phantom +edge while the advisor anchored on it, leaving the advice dangling +mid-line under a much longer badge cluster. The advisor now anchors on +line 1's actual rendered width, so the second line's right edge meets +the first's whatever the width guess was. Width detection itself also +got honest: the controlling tty (`stty size /dev/null || echo 80) +# Terminal width, best signal first. The statusline runs with stdout on a +# pipe, where bare `tput cols` answers a flat 80 no matter how wide the real +# terminal is — the controlling tty (when we still have one) and an inherited +# COLUMNS both beat it. The advisor row additionally anchors on line 1's +# actual rendered width (see format_output), so a wrong width degrades the +# gap, never the alignment between the two rows. +term_width="" +if [ -e /dev/tty ]; then + term_width=$({ stty size /dev/null | awk '{print $2}') +fi +if ! [ "$term_width" -gt 0 ] 2>/dev/null; then + term_width="${COLUMNS:-}" +fi +if ! [ "$term_width" -gt 0 ] 2>/dev/null; then + term_width=$(tput cols 2>/dev/null || echo 80) +fi +debug_log "TERM WIDTH: $term_width" # Palette is organized in three lanes so a glance is unambiguous: # STATUS (green/yellow/red) — pressure ONLY: quota, context, cache, premium @@ -3099,6 +3115,73 @@ if [ -d "$current_dir/.git" ] || git -C "$current_dir" rev-parse --git-dir >/dev fi fi +# cctrace trace chip: is this session's wire being captured, and where does +# the live UI serve? Session identity, so it lives on the LEFT with path and +# branch (structure lane, dim — red stays reserved for pressure). Detection, +# strongest signal first: +# CCTRACE_SERVER_PORT / CCTRACE_TRACE_FILE cctrace >= 0.29 exports trace +# identity into the child env +# NODE_EXTRA_CA_CERTS under a cctrace dir older mitm captures leave only +# proxy plumbing behind +# DEVA_TRACE=1 deva --trace says so outright +# Without the port env (old cctrace), the live-instance registry +# (/instances/*.json, a documented contract) is matched by this +# session's id, then by project path, then by being the only live capture. +# The registry stores session ids REDACTED past the first 8 hex +# ("c3a6e0f3-****-…" — capture-time redaction, ids never land on disk in +# full), so the sid8 prefix is the join key — the same convention cctrace's +# own UI uses. The id match trusts any non-tombstone entry (if OUR capture +# died, this session's proxy died with it); the path/only-live fallbacks +# trust only heartbeat-fresh files (<2min, heartbeat is 30s) — crashed runs +# leave "live" entries behind for up to a day, and a stale port is worse +# than no port. No match still shows the bare chip: "recorded" matters +# even portless. +build_trace_component() { + local port="${CCTRACE_SERVER_PORT:-}" + local traced="" + [ -n "$port" ] && traced=1 + [ -n "${CCTRACE_TRACE_FILE:-}" ] && traced=1 + case "${NODE_EXTRA_CA_CERTS:-}" in + */cctrace/*) traced=1 ;; + esac + [ "${DEVA_TRACE:-}" = "1" ] && traced=1 + [ -n "$traced" ] || return 0 + + if [ -z "$port" ]; then + local reg="${CCTRACE_DATA_DIR:-}" + if [ -z "$reg" ]; then + case "${NODE_EXTRA_CA_CERTS:-}" in + */cctrace/*) reg=$(dirname "$(dirname "$NODE_EXTRA_CA_CERTS")") ;; + *) reg="$CLAUDE_HOME/.local/share/cctrace" ;; + esac + fi + reg="$reg/instances" + if [ -d "$reg" ]; then + port=$(cat "$reg"/*.json 2>/dev/null | jq -rs --arg sid "${stdin_session_id:-}" ' + [ .[] | select(type == "object" and .endedAt == null) ] + | map(select(($sid | length) >= 8 and + ((.sessionId // "")[0:8] == $sid[0:8]))) + | first.port // empty' 2>/dev/null) + if [ -z "$port" ]; then + port=$(find "$reg" -name '*.json' -newermt '-120 seconds' 2>/dev/null \ + | xargs cat 2>/dev/null | jq -rs --arg cwd "${current_dir:-}" ' + [ .[] | select(type == "object" and .endedAt == null) ] as $fresh | + (($fresh | map(select(.projectPath == $cwd and $cwd != ""))) + + (if ($fresh | length) == 1 then $fresh else [] end)) + | first.port // empty' 2>/dev/null) + fi + fi + fi + + if [ -n "$port" ]; then + echo " ${DIM}[cctrace:${port}]${RESET}" + else + echo " ${DIM}[cctrace]${RESET}" + fi +} + +trace_info=$(build_trace_component) + # settings.json .model is only a fallback for the rare case stdin carries no # model id — it's a static default a session can override, so it must not be # read (let alone preferred) when stdin already tells us the running model. @@ -3678,8 +3761,14 @@ for item in "${order_array[@]}"; do esac done +# Renders line 1 and records its actual width in line1_cols: the advisor row +# right-aligns to THAT edge, not to term_width — when the width guess is +# wrong (tput's flat 80 under a pipe) line 1 overflows its phantom edge, and +# anchoring line 2 on the same phantom left it dangling mid-line. +line1_cols=0 + format_output() { - local path_part="${DIM}${display_path}${RESET}${git_info}" + local path_part="${DIM}${display_path}${RESET}${git_info}${trace_info}" local stats_part="${right_parts}" # Strip ANSI escapes for length calculation (portable: printf %b instead of echo -e) @@ -3690,11 +3779,13 @@ format_output() { "left-right") local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5)) [ $padding -lt 6 ] && padding=6 + line1_cols=$((${#path_plain} + padding + ${#stats_plain})) printf "%b%*s%b\n" "$path_part" $padding "" "$stats_part" ;; "right-left") local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5)) [ $padding -lt 6 ] && padding=6 + line1_cols=$((${#stats_plain} + padding + ${#path_plain})) printf "%b%*s%b\n" "$stats_part" $padding "" "$path_part" ;; "center") @@ -3703,16 +3794,19 @@ format_output() { local left_padding=$(((term_width - ${#path_plain}) / 2)) local right_padding=$((term_width - left_padding - ${#path_plain} - ${#stats_plain})) [ $right_padding -lt 2 ] && right_padding=2 + line1_cols=$((left_padding + ${#path_plain} + right_padding + ${#stats_plain})) printf "%*s%b%*s%b\n" $left_padding "" "$path_part" $right_padding "" "$stats_part" else local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5)) [ $padding -lt 6 ] && padding=6 + line1_cols=$((${#path_plain} + padding + ${#stats_plain})) printf "%b%*s%b\n" "$path_part" $padding "" "$stats_part" fi ;; *) local padding=$((term_width - ${#path_plain} - ${#stats_plain} - 5)) [ $padding -lt 6 ] && padding=6 + line1_cols=$((${#path_plain} + padding + ${#stats_plain})) printf "%b%*s%b\n" "$path_part" $padding "" "$stats_part" ;; esac @@ -3723,15 +3817,18 @@ format_output # Advisor row: a second stdout line renders as its own row in Claude Code's # status area, and printing nothing produces no row — so a quiet advisor # costs zero height. Single-hue by design, which keeps truncation honest. -# Right-aligned to the same anchor the stats cluster ends at in format_output -# (term_width - 5), so the advice sits directly beneath the usage badges it -# interprets, stable across widths and message lengths; when the stats sit -# left (right-left alignment) the advisor stays left with them. +# Right-aligned to line 1's ACTUAL rendered edge (line1_cols, recorded by +# format_output) — not to term_width, which lies under a pipe (tput says 80, +# line 1 overflows it, and a term_width anchor left the advice dangling +# mid-line). Anchoring on the row above keeps the advice directly beneath +# the usage badges it interprets, whatever the width guess was; when the +# stats sit left (right-left alignment) the advisor stays left with them. if [ "$advisor_display_mode" != "off" ] && [ -n "${usage_data:-}" ]; then advisor_line=$(build_advisor_line "$usage_data" "$advisor_display_mode" "${model_id:-$model_display}") if [ -n "$advisor_line" ]; then advisor_plain=$(printf '%b' "$advisor_line" | sed 's/\x1b\[[0-9;]*m//g') - advisor_max=$((term_width - 1)) + advisor_anchor=$((line1_cols > 0 ? line1_cols : term_width - 5)) + advisor_max=$((advisor_anchor > term_width - 1 ? advisor_anchor : term_width - 1)) if [ "${#advisor_plain}" -gt "$advisor_max" ] 2>/dev/null && [ "$advisor_max" -gt 1 ]; then advisor_color=$(printf '%s' "$advisor_line" | grep -o '^\\033\[[0-9;]*m' | head -1) advisor_plain="${advisor_plain:0:$((advisor_max - 1))}…" @@ -3739,7 +3836,7 @@ if [ "$advisor_display_mode" != "off" ] && [ -n "${usage_data:-}" ]; then fi advisor_pad=0 if [ "$alignment" != "right-left" ]; then - advisor_pad=$((term_width - 5 - ${#advisor_plain})) + advisor_pad=$((advisor_anchor - ${#advisor_plain})) [ "$advisor_pad" -gt 0 ] 2>/dev/null || advisor_pad=0 fi debug_log "ADVISOR: $advisor_plain" diff --git a/t/helpers.bash b/t/helpers.bash index 4e35b7a..f8516f4 100644 --- a/t/helpers.bash +++ b/t/helpers.bash @@ -19,6 +19,12 @@ export STATUSLINE_NO_FETCH # would add an account chip and rescope cache dirs in every integration run. unset DEVA_AUTH_TAG DEVA_AUTH_METHOD DEVA_AUTH_DETAILS STATUSLINE_ACCOUNT ACCOUNT_TAG +# Trace identity must come from each test too: a test host running under +# `deva --trace` / cctrace (this repo's own dev loop, literally) carries the +# capture env and a live instance registry — leaking either in would put a +# [cctrace:PORT] chip into every integration render. +unset CCTRACE_SERVER_PORT CCTRACE_TRACE_FILE CCTRACE_INSTANCE_ID CCTRACE_DATA_DIR DEVA_TRACE NODE_EXTRA_CA_CERTS + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Color constants (needed by functions) @@ -64,7 +70,7 @@ debug_log() { # Source individual functions by extracting them from statusline.sh. # This is deliberate: we test the actual production code, not copies. eval "$(awk ' - /^(abbreviate_model_id|get_runtime_model|format_reset_relative|format_reset_absolute|get_reset_seconds|format_duration|should_show_extra|get_cache_health|infer_cache_ttl_class|build_cache_indicator|get_usage_color|get_seven_day_color|seven_day_elapsed|seven_day_pace|weekend_secs_ahead|get_adaptive_ttl|curl_ca_bundle|acquire_lock|reap_stale_lock|fetch_usage_for_session|merge_stdin_rate_limits|rotate_usage_log|build_seven_day_profile|seven_day_forecast|premium_band_level|abbrev_effort|effort_color|_epoch_from_ts|_fmt_epoch|render_bar|format_money_minor|oauth_token_expired|refresh_oauth_credentials_file|is_default_1m_family|get_context_limit|is_1m_model|rotate_debug_log|build_display_path|delta_flash|delta_flash_part|quota_bump_notice|record_fetch_error|fetch_error_remaining|fetch_error_badge|model_scope_abbrev|build_scoped_quota_display|build_usage_display|build_extra_usage_display|build_user_info|get_user_tier|build_advisor_line|build_advisor_fleet_hint|_seven_day_walk|forecast_pct_per_window|log_usage_snapshot|detect_session_boundary|run_usage_report|run_check|run_session_summary)\(\)/ { capture=1 } + /^(abbreviate_model_id|get_runtime_model|format_reset_relative|format_reset_absolute|get_reset_seconds|format_duration|should_show_extra|get_cache_health|infer_cache_ttl_class|build_cache_indicator|get_usage_color|get_seven_day_color|seven_day_elapsed|seven_day_pace|weekend_secs_ahead|get_adaptive_ttl|curl_ca_bundle|acquire_lock|reap_stale_lock|fetch_usage_for_session|merge_stdin_rate_limits|rotate_usage_log|build_seven_day_profile|seven_day_forecast|premium_band_level|abbrev_effort|effort_color|_epoch_from_ts|_fmt_epoch|render_bar|format_money_minor|oauth_token_expired|refresh_oauth_credentials_file|is_default_1m_family|get_context_limit|is_1m_model|rotate_debug_log|build_display_path|build_trace_component|delta_flash|delta_flash_part|quota_bump_notice|record_fetch_error|fetch_error_remaining|fetch_error_badge|model_scope_abbrev|build_scoped_quota_display|build_usage_display|build_extra_usage_display|build_user_info|get_user_tier|build_advisor_line|build_advisor_fleet_hint|_seven_day_walk|forecast_pct_per_window|log_usage_snapshot|detect_session_boundary|run_usage_report|run_check|run_session_summary)\(\)/ { capture=1 } capture { print } capture && /^}$/ { capture=0 } ' "$SCRIPT_DIR/statusline.sh")" diff --git a/t/statusline.bats b/t/statusline.bats index 22e9188..782cfa4 100644 --- a/t/statusline.bats +++ b/t/statusline.bats @@ -2934,3 +2934,175 @@ _write_ppw_fixture() { # dir ppw [ "$result" = "a 5h[8%] free" ] rm -rf "$tmpdir" } + +# --- cctrace trace chip (left cluster) --- + +@test "build_trace_component: silent without any trace signal" { + result=$(stdin_session_id="s1" current_dir="/t"; build_trace_component) + [ -z "$result" ] +} + +@test "build_trace_component: CCTRACE_SERVER_PORT renders the chip directly" { + result=$( (CCTRACE_SERVER_PORT=9317; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9317]" ] +} + +@test "build_trace_component: registry stores the sid REDACTED — sid8 prefix still matches" { + # cctrace's capture-time redaction masks session ids past the first 8 hex + # before anything lands on disk; the sid8 prefix is the join key. + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/elsewhere","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"c3a6e0f3-****-****-****-************"}' \ + > "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="c3a6e0f3-871b-4047-a282-60ca3d2244e6" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9319]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: sid8 claims its own entry even when the heartbeat looks stale" { + # If OUR capture died the session's proxy died with it — an identity + # match never needs freshness. + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/elsewhere","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"c3a6e0f3-****-****-****-************"}' \ + > "$tmpdir/instances/r1.json" + touch -d '10 minutes ago' "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="c3a6e0f3-871b-4047-a282-60ca3d2244e6" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9319]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: a stale live entry never lends its port via fallback" { + # Crashed captures leave non-tombstoned entries behind for up to a day; + # the path/only-live fallbacks trust heartbeat-fresh files only. + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/my/proj","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + touch -d '10 minutes ago' "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen-sid-123" current_dir="/my/proj"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: tombstoned runs never lend their port (bare chip)" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9319,"project":"p","projectPath":"/t","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"sid-1","endedAt":"2026-01-01T01:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="sid-1" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: NODE_EXTRA_CA_CERTS marker derives the registry dir" { + base=$(mktemp -d) + tmpdir="$base/cctrace" + mkdir -p "$tmpdir/mitm" "$tmpdir/instances" + touch "$tmpdir/mitm/ca-cert.pem" + printf '{"id":"r1","pid":1,"port":9321,"project":"p","projectPath":"/proj","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z","sessionId":"sid-x"}' \ + > "$tmpdir/instances/r1.json" + result=$( (NODE_EXTRA_CA_CERTS="$tmpdir/mitm/ca-cert.pem" stdin_session_id="sid-x" current_dir="/t"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9321]" ] + rm -rf "$base" +} + +@test "build_trace_component: project-path fallback when the session id is not registered yet" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9320,"project":"p","projectPath":"/my/proj","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + printf '{"id":"r2","pid":2,"port":9325,"project":"q","projectPath":"/other","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r2.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen" current_dir="/my/proj"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9320]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: two live captures with no match stay honest (bare chip)" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9320,"project":"p","projectPath":"/a","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + printf '{"id":"r2","pid":2,"port":9325,"project":"q","projectPath":"/b","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r2.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen" current_dir="/c"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace]" ] + rm -rf "$tmpdir" +} + +@test "build_trace_component: a lone live capture is claimed without a match" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/instances" + printf '{"id":"r1","pid":1,"port":9322,"project":"p","projectPath":"/a","logFile":"","mode":"mitm","startedAt":"2026-01-01T00:00:00Z"}' \ + > "$tmpdir/instances/r1.json" + result=$( (DEVA_TRACE=1 CCTRACE_DATA_DIR="$tmpdir" stdin_session_id="unseen" current_dir="/c"; build_trace_component) ) + plain=$(strip_ansi "$result") + [ "$plain" = " [cctrace:9322]" ] + rm -rf "$tmpdir" +} + +@test "integration: traced session wears the chip on the left, next to path" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude" + out=$(echo '{"session_id":"trace-int","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" CCTRACE_SERVER_PORT=9317 bash "$SCRIPT_DIR/statusline.sh" --test) + plain=$(strip_ansi "$out") + [[ "$plain" == "proj [cctrace:9317]"* ]] + rm -rf "$tmpdir" +} + +@test "integration: untraced session has no chip" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude" + out=$(echo '{"session_id":"trace-int2","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" bash "$SCRIPT_DIR/statusline.sh" --test) + plain=$(strip_ansi "$out") + [[ "$plain" != *"cctrace"* ]] + rm -rf "$tmpdir" +} + +# --- advisor row alignment (second line meets line 1's right edge) --- + +@test "integration: advisor row right-aligns to line 1's actual edge" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude/statusline" + printf '{"claudeAiOauth":{"accessToken":"tok"}}' > "$tmpdir/.claude/.credentials.json" + # 85% only 2h into the 5h window: pace-hot, the advisor projects the cap. + reset_5h=$(date -u -d '+3 hours' '+%Y-%m-%dT%H:%M:%SZ') + printf '{"five_hour":{"utilization":85,"resets_at":"%s"},"seven_day":{"utilization":10},"fetched_at":%s}' \ + "$reset_5h" "$(date +%s)" > "$tmpdir/.claude/statusline/usage.cache" + out=$(echo '{"session_id":"align-test","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" COLUMNS=110 bash "$SCRIPT_DIR/statusline.sh" --test) + line1=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 1p)") + line2=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 2p)") + [ -n "$line2" ] + [ "${#line1}" -eq "${#line2}" ] + rm -rf "$tmpdir" +} + +@test "integration: advisor row still meets the edge when the width guess is low" { + tmpdir=$(mktemp -d) + mkdir -p "$tmpdir/.claude/statusline" + printf '{"claudeAiOauth":{"accessToken":"tok"}}' > "$tmpdir/.claude/.credentials.json" + reset_5h=$(date -u -d '+3 hours' '+%Y-%m-%dT%H:%M:%SZ') + printf '{"five_hour":{"utilization":85,"resets_at":"%s"},"seven_day":{"utilization":10},"fetched_at":%s}' \ + "$reset_5h" "$(date +%s)" > "$tmpdir/.claude/statusline/usage.cache" + # A phantom 40-col terminal: line 1 overflows it (padding clamps to 6); + # the advisor must anchor on line 1's real edge, not the phantom one. + out=$(echo '{"session_id":"align-low","model":{"id":"claude-opus-4-6","display_name":"Opus"},"cwd":"/t/proj","workspace":{"current_dir":"/t/proj"},"version":"2.1.174","cost":{"total_cost_usd":0}}' \ + | HOME="$tmpdir" COLUMNS=40 bash "$SCRIPT_DIR/statusline.sh" --test) + line1=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 1p)") + line2=$(strip_ansi "$(printf '%s\n' "$out" | sed -n 2p)") + [ -n "$line2" ] + [ "${#line1}" -eq "${#line2}" ] + rm -rf "$tmpdir" +}