iop/apps/node/internal/adapters/cli/status/parser_test.go

307 lines
14 KiB
Go

package status
import (
"testing"
)
func TestParseStatusOutput_CodexLimits(t *testing.T) {
rawOutput := "5h limit: [████████████████████] 98% left (resets 18:38)\n" +
"Weekly limit: [████░░░░░░░░░░░░░░░░] 22% left (resets 10:20 on 8 May)"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "98%" {
t.Errorf("expected daily limit 98%%, got %q", status.DailyLimit)
}
if status.DailyResetTime != "18:38" {
t.Errorf("expected daily reset 18:38, got %q", status.DailyResetTime)
}
if status.WeeklyLimit != "22%" {
t.Errorf("expected weekly limit 22%%, got %q", status.WeeklyLimit)
}
if status.WeeklyResetTime != "10:20 on 8 May" {
t.Errorf("expected weekly reset 10:20 on 8 May, got %q", status.WeeklyResetTime)
}
}
func TestParseStatusOutput_ClaudeUsage(t *testing.T) {
rawOutput := "Current session: [███░░░░░░░░░░░░░░░░░] 2% used\nResets 10am (Asia/Seoul)\n\n" +
"Current week (all models): [#░░░░░░░░░░░░░░░░░░░░] 0% used\nResets May 16, 6pm (Asia/Seoul)"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "98%" {
t.Errorf("expected daily limit 98%%, got %q", status.DailyLimit)
}
if status.DailyResetTime != "10am (Asia/Seoul)" {
t.Errorf("expected daily reset 10am (Asia/Seoul), got %q", status.DailyResetTime)
}
if status.WeeklyLimit != "100%" {
t.Errorf("expected weekly limit 100%%, got %q", status.WeeklyLimit)
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Errorf("expected weekly reset May 16, 6pm (Asia/Seoul), got %q", status.WeeklyResetTime)
}
if status.Metadata["daily_label"] != "Current session" {
t.Errorf("expected daily_label Current session, got %q", status.Metadata["daily_label"])
}
if status.Metadata["weekly_label"] != "Current week" {
t.Errorf("expected weekly_label Current week, got %q", status.Metadata["weekly_label"])
}
}
func TestParseStatusOutput_ClaudeUsageWithANSICursorSpacing(t *testing.T) {
rawOutput := "Current\x1b[1C session: [███░░░░░░░░░░░░░░░░░] 2%\x1b[1C used\nResets 10am (Asia/Seoul)\n\n" +
"Current week (all models): [#░░░░░░░░░░░░░░░░░░░░] 0% used\nResets May 16, 6pm (Asia/Seoul)"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "98%" {
t.Errorf("expected daily limit 98%%, got %q", status.DailyLimit)
}
if status.DailyResetTime != "10am (Asia/Seoul)" {
t.Errorf("expected daily reset 10am (Asia/Seoul), got %q", status.DailyResetTime)
}
if status.WeeklyLimit != "100%" {
t.Errorf("expected weekly limit 100%%, got %q", status.WeeklyLimit)
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Errorf("expected weekly reset, got %q", status.WeeklyResetTime)
}
}
func TestParseStatusOutput_ClaudeUsageWithCarriageReturns(t *testing.T) {
rawOutput := "Current session: [███░░░░░░░░░░░░░░░░░] 2% used\rResets 10am (Asia/Seoul)\r\n\n" +
"Current week (all models): [#░░░░░░░░░░░░░░░░░░░░] 0% used\rResets May 16, 6pm (Asia/Seoul)\r"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "98%" {
t.Errorf("expected daily limit 98%%, got %q", status.DailyLimit)
}
if status.DailyResetTime != "10am (Asia/Seoul)" {
t.Errorf("expected daily reset 10am (Asia/Seoul), got %q", status.DailyResetTime)
}
if status.WeeklyLimit != "100%" {
t.Errorf("expected weekly limit 100%%, got %q", status.WeeklyLimit)
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Errorf("expected weekly reset May 16, 6pm (Asia/Seoul), got %q", status.WeeklyResetTime)
}
}
func TestParseStatusOutput_ClaudeUsageFromRepaintedScreen(t *testing.T) {
// Mirrors the actual Claude /usage panel raw stream: an initial loading
// repaint ("Scanning local sessions...", "Esc to cancel") followed by a
// clear-screen sequence and the real data block. The parser must still
// extract Current session / Current week from the combined stream.
rawOutput := "\x1b[2J\x1b[H" +
" Usage\n" +
" Scanning local sessions...\n" +
" Refreshing...\n" +
" Esc to cancel\n" +
"\x1b[2J\x1b[H" +
" Settings Status Config Usage Stats\n\n" +
" Session\n\n" +
" Current session\n" +
" \x1b[48;5;102m\x1b[38;5;153m## \x1b[49m\x1b[39m 4% used\n" +
" Resets 10am (Asia/Seoul)\n\n" +
" Current week (all models)\n" +
" 0% used\n" +
" Resets May 16, 6pm (Asia/Seoul)\n\n" +
" Esc to cancel\n"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "96%" {
t.Errorf("expected daily limit 96%%, got %q", status.DailyLimit)
}
if status.DailyResetTime != "10am (Asia/Seoul)" {
t.Errorf("expected daily reset 10am (Asia/Seoul), got %q", status.DailyResetTime)
}
if status.WeeklyLimit != "100%" {
t.Errorf("expected weekly limit 100%%, got %q", status.WeeklyLimit)
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Errorf("expected weekly reset May 16, 6pm (Asia/Seoul), got %q", status.WeeklyResetTime)
}
if status.Metadata["daily_label"] != "Current session" {
t.Errorf("expected daily_label Current session, got %q", status.Metadata["daily_label"])
}
if status.Metadata["weekly_label"] != "Current week" {
t.Errorf("expected weekly_label Current week, got %q", status.Metadata["weekly_label"])
}
}
func TestParseStatusOutput_ClaudeUsageCurrentRealState(t *testing.T) {
// Reproduces the exact failure state observed in the previous review run:
// the /usage panel is fully open with the trailing "Last 24h", "d to day · w
// to week", "Extra usage", "Esc to cancel" sections rendered, AND the raw
// stream interleaves the week's "% used / Resets" payload before the
// session's payload (this happens when the TUI renders the bars via cursor
// positioning rather than sequentially). The previous lazy-regex parser
// paired the session header with the week's reset time on this input —
// the new section-aware parser must keep them separated.
rawOutput := "" +
" Settings Status Config Usage Stats\n\n" +
" Session\n\n" +
" Current session\n" +
" Current week (all models)\n" +
" ############## 95% used\n" +
" Resets May 16, 6pm (Asia/Seoul)\n" +
" ### 7% used\n" +
" Resets 10am (Asia/Seoul)\n\n" +
" What's contributing to your limits usage?\n" +
" Last 24h · these are independent characteristics of your usage, not a breakdown\n" +
" Nothing over 10% in this period — try the other window.\n" +
" d to day · w to week\n\n" +
" Extra usage\n" +
" Extra usage not enabled · /extra-usage to enable\n\n" +
" Esc to cancel\n"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
// First payload after "Current session" header (before "Current week") is
// the week's data because of the cursor-interleaved render order. The
// section-aware parser must NOT capture it for the session — it must leave
// the session payload empty since the session block has no payload of its
// own before the week header.
if status.DailyResetTime == "May 16, 6pm (Asia/Seoul)" {
t.Fatalf("session reset time leaked from week section: %q", status.DailyResetTime)
}
// The week section, starting at its header, gets the next available
// "% used / Resets" pair, which is the week's own.
if status.WeeklyLimit != "5%" {
t.Fatalf("WeeklyLimit: got %q want %q", status.WeeklyLimit, "5%")
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Fatalf("WeeklyResetTime: got %q want %q", status.WeeklyResetTime, "May 16, 6pm (Asia/Seoul)")
}
if status.Metadata["weekly_label"] != "Current week" {
t.Fatalf("weekly_label: got %q want %q", status.Metadata["weekly_label"], "Current week")
}
}
func TestParseStatusOutput_ClaudeUsageFromVisibleScreenRepaint(t *testing.T) {
// Reproduces the cursor-addressed repaint that caused the G08 review run-2
// failure: the panel writes a loading state ("Last 24h", "Esc to cancel"),
// then uses CSI cursor positioning to overwrite specific rows with the
// session/week payload. In the raw append stream the corrupted loading
// text remains interleaved with the payload runes ("No hing ov r",
// "21% of y ur usage..."), so a line-oriented cleanANSI parser cannot
// reassemble a coherent "Current session ... X% used\nResets Y". A
// terminal screen model must apply the cursor moves and erases to recover
// the actually-visible text.
//
// Sequence:
// 1. Clear screen, write garbage loading lines on rows 5..10.
// 2. CUP back to row 5 col 1, EL (erase line), write "Current session".
// 3. Same for row 6 with the bar + "4% used".
// 4. Same for row 7 with "Resets 10am (Asia/Seoul)".
// 5. Same for rows 9..11 with the week block.
const ESC = "\x1b"
// First write a "Current session" header and a misleading early payload
// ("21% of your usage Resets garbage time") that — if left intact in the
// stream — would be picked up by a section-aware regex as the session's
// payload. Then cursor back, erase the misleading lines, and write the
// real " 4% used / Resets 10am" payload at those same rows. The same
// pattern is applied for the week block.
raw := ESC + "[2J" + ESC + "[H" +
ESC + "[5;1H" + " Current session" +
ESC + "[6;1H" + " 21% of your usage in last 24h" +
ESC + "[7;1H" + " Resets garbage time" +
ESC + "[8;1H" + "" +
ESC + "[9;1H" + " Current week (all models)" +
ESC + "[10;1H" + " 47% of week things here" +
ESC + "[11;1H" + " Resets garbage week time" +
// Repaint rows 6,7 with real session payload.
ESC + "[6;1H" + ESC + "[2K" + " #### 4% used" +
ESC + "[7;1H" + ESC + "[2K" + " Resets 10am (Asia/Seoul)" +
// Repaint rows 10,11 with real week payload.
ESC + "[10;1H" + ESC + "[2K" + " 0% used" +
ESC + "[11;1H" + ESC + "[2K" + " Resets May 16, 6pm (Asia/Seoul)" +
// Trailing panel chrome that timeout tails would otherwise reveal.
ESC + "[13;1H" + " Last 24h" +
ESC + "[14;1H" + " d to day · w to week" +
ESC + "[15;1H" + " Extra usage" +
ESC + "[16;1H" + " Esc to cancel"
// Sanity check: the line-oriented cleanANSI alone must NOT be able to find
// a coherent payload — corrupt text leaks through and a section-aware
// regex on raw-clean ends up with empty or wrong fields. We assert that
// directly so the regression catches anyone reverting to a line-only
// parser.
lineOnly := &UsageStatus{}
lineOnly.parseClaudeUsage(cleanANSI(raw))
if lineOnly.DailyResetTime == "10am (Asia/Seoul)" && lineOnly.WeeklyResetTime == "May 16, 6pm (Asia/Seoul)" {
t.Fatalf("line-only parser must fail on cursor-repainted raw; got daily=%q weekly=%q", lineOnly.DailyResetTime, lineOnly.WeeklyResetTime)
}
status, err := ParseStatusOutput(raw)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "96%" {
t.Errorf("DailyLimit: got %q want 96%%", status.DailyLimit)
}
if status.DailyResetTime != "10am (Asia/Seoul)" {
t.Errorf("DailyResetTime: got %q want %q", status.DailyResetTime, "10am (Asia/Seoul)")
}
if status.WeeklyLimit != "100%" {
t.Errorf("WeeklyLimit: got %q want 100%%", status.WeeklyLimit)
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Errorf("WeeklyResetTime: got %q want %q", status.WeeklyResetTime, "May 16, 6pm (Asia/Seoul)")
}
if status.Metadata["daily_label"] != "Current session" {
t.Errorf("daily_label: got %q want %q", status.Metadata["daily_label"], "Current session")
}
if status.Metadata["weekly_label"] != "Current week" {
t.Errorf("weekly_label: got %q want %q", status.Metadata["weekly_label"], "Current week")
}
}
func TestParseStatusOutput_ClaudeUsageWithTerminalControls(t *testing.T) {
rawOutput := "Current \x1b[1C \x1b[1Csession:\x1b[10C [#\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\xb8\x1b[5C\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80] \x1b[1C2%\x1b[1C used\rResets \x1b[1C10am (Asia/Seoul)\r\n\n" +
"Current week (all models): [#\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\xb8\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80\xe2\x96\x80] 0% used\rResets May 16, 6pm (Asia/Seoul)"
status, err := ParseStatusOutput(rawOutput)
if err != nil {
t.Fatalf("ParseStatusOutput failed: %v", err)
}
if status.DailyLimit != "98%" {
t.Errorf("expected daily limit 98%%, got %q", status.DailyLimit)
}
if status.DailyResetTime != "10am (Asia/Seoul)" {
t.Errorf("expected daily reset 10am (Asia/Seoul), got %q", status.DailyResetTime)
}
if status.WeeklyLimit != "100%" {
t.Errorf("expected weekly limit 100%%, got %q", status.WeeklyLimit)
}
if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" {
t.Errorf("expected weekly reset May 16, 6pm (Asia/Seoul), got %q", status.WeeklyResetTime)
}
if status.Metadata["daily_label"] != "Current session" {
t.Errorf("expected daily_label Current session, got %q", status.Metadata["daily_label"])
}
if status.Metadata["weekly_label"] != "Current week" {
t.Errorf("expected weekly_label Current week, got %q", status.Metadata["weekly_label"])
}
}