65c54416f3
Pure functions for the watcher's core: compute_alerts decides what to emit given prior state vs. fresh usage, with configurable threshold list and a reset-shift tolerance to filter minute-boundary jitter. State is atomically persisted as JSON. Alerts render to (plain, html) tuples for Matrix, with severity-colored percentages, blockquote framing, and a Unicode progress bar for the cold-start armed message.
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
from claude_matrix_bot.reset_watcher.diff import Alert, WindowSnapshot
|
|
from claude_matrix_bot.reset_watcher.format import (
|
|
_format_delta,
|
|
_progress_bar,
|
|
format_alert,
|
|
format_armed,
|
|
)
|
|
|
|
|
|
def test_delta_under_minute_shows_seconds() -> None:
|
|
assert _format_delta(45) == "+45s"
|
|
assert _format_delta(-30) == "-30s"
|
|
assert _format_delta(0) == "+0s"
|
|
|
|
|
|
def test_delta_under_hour_shows_minutes() -> None:
|
|
assert _format_delta(60) == "+1 min"
|
|
assert _format_delta(720) == "+12 min"
|
|
assert _format_delta(-300) == "-5 min"
|
|
|
|
|
|
def test_delta_at_or_above_hour_shows_hours_and_minutes() -> None:
|
|
assert _format_delta(3_600) == "+1h"
|
|
assert _format_delta(3_660) == "+1h 1m"
|
|
assert _format_delta(12_000) == "+3h 20m"
|
|
assert _format_delta(-7_200) == "-2h"
|
|
|
|
|
|
def test_reset_shift_alert_renders_with_seconds_when_small() -> None:
|
|
alert = Alert(
|
|
kind="reset_shift",
|
|
window="five_hour",
|
|
detail={
|
|
"old_resets_at": 1_700_000_000,
|
|
"new_resets_at": 1_700_000_045,
|
|
"delta_seconds": 45,
|
|
},
|
|
)
|
|
plain, html = format_alert(alert)
|
|
assert "+45s" in plain
|
|
assert "+45s" in html
|
|
# Should never display "0 min" for a sub-minute delta.
|
|
assert "0 min" not in plain
|
|
# Aligned visual frame with armed message
|
|
assert "<blockquote>" in html
|
|
assert "<table>" in html
|
|
assert "<i>5-hour session</i>" in html
|
|
|
|
|
|
def test_threshold_alert_renders_emoji_and_percent() -> None:
|
|
alert = Alert(
|
|
kind="threshold_crossed",
|
|
window="seven_day",
|
|
detail={"threshold": 80, "utilization_pct": 82.5, "resets_at": 1_700_000_000},
|
|
)
|
|
plain, html = format_alert(alert)
|
|
assert "🔥" in plain
|
|
assert "82.5%" in plain
|
|
assert "weekly (all)" in html
|
|
# Aligned visual frame with armed message
|
|
assert "<blockquote>" in html
|
|
assert "<table>" in html
|
|
assert "<small>" in html
|
|
assert "<i>weekly (all)</i>" in html
|
|
assert "#c0392b" in html # 82.5% -> red severity
|
|
|
|
|
|
def test_threshold_50_uses_warn_emoji_not_fire() -> None:
|
|
"""50% threshold gets the warning emoji; only 80%+ uses the fire emoji."""
|
|
alert = Alert(
|
|
kind="threshold_crossed",
|
|
window="seven_day_sonnet",
|
|
detail={"threshold": 50, "utilization_pct": 51.0, "resets_at": 1_700_000_000},
|
|
)
|
|
plain, html = format_alert(alert)
|
|
assert "⚠️" in plain
|
|
assert "🔥" not in plain
|
|
|
|
|
|
def test_progress_bar_proportional() -> None:
|
|
assert _progress_bar(0, width=10) == "░░░░░░░░░░"
|
|
assert _progress_bar(50, width=10) == "█████░░░░░"
|
|
assert _progress_bar(100, width=10) == "██████████"
|
|
# Out-of-range stays clamped (defensive, not expected in practice)
|
|
assert _progress_bar(120, width=10) == "██████████"
|
|
assert _progress_bar(-5, width=10) == "░░░░░░░░░░"
|
|
|
|
|
|
def test_reset_shift_uses_blockquote_and_table() -> None:
|
|
alert = Alert(
|
|
kind="reset_shift",
|
|
window="five_hour",
|
|
detail={
|
|
"old_resets_at": 1_700_000_000,
|
|
"new_resets_at": 1_700_000_300,
|
|
"delta_seconds": 300,
|
|
},
|
|
)
|
|
plain, html = format_alert(alert)
|
|
assert "<blockquote>" in html
|
|
assert "<table>" in html
|
|
assert "<i>5-hour session</i>" in html
|
|
# delta in plain still uses the arrow form for narrow display
|
|
assert "→" in plain
|
|
assert "+5 min" in plain
|
|
# delta colored amber for <30min shifts
|
|
assert "#d68910" in html
|
|
|
|
|
|
def test_armed_message_renders_blockquote_table() -> None:
|
|
fresh = {
|
|
"five_hour": WindowSnapshot(utilization=0.06, resets_at=1_700_000_000),
|
|
"seven_day": WindowSnapshot(utilization=0.55, resets_at=1_700_500_000),
|
|
"seven_day_opus": WindowSnapshot(utilization=0.10, resets_at=1_700_500_000),
|
|
"seven_day_sonnet": WindowSnapshot(utilization=0.62, resets_at=1_700_500_000),
|
|
}
|
|
plain, html = format_armed(fresh)
|
|
|
|
assert "🤖" in plain
|
|
assert "reset-watcher armed" in plain
|
|
# Each window appears in both plain and html
|
|
for label in ("5-hour session", "weekly (all)", "weekly Opus", "weekly Sonnet"):
|
|
assert label in plain
|
|
assert label in html
|
|
# New visual frame: blockquote, italic title, code subtitle, table layout
|
|
assert "<blockquote>" in html
|
|
assert "<i>armed</i>" in html
|
|
assert "<code>baseline captured</code>" in html
|
|
assert "<table>" in html
|
|
assert "<small>resets</small>" in html
|
|
# No progress bars anymore
|
|
assert "█" not in plain
|
|
assert "█" not in html
|
|
# Severity colors applied per row (matches user's mockup palette)
|
|
assert "#27ae60" in html # 6% -> green
|
|
assert "#d68910" in html # 62% -> amber (50-80% band)
|