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_ClaudeStatusPanel(t *testing.T) { rawOutput := "ClaudeCode v2.1.144\n\n" + "Version: 2.1.144\n" + "SessionID: e26e764d-dd46-4c7e-9509-5a9b964ce993\n" + "cwd: /config/workspace/iop\n" + "Loginmethod: Claude Pro account\n" + "Model: opus (claude-opus-4-7)\n" status, err := ParseStatusOutput(rawOutput) if err != nil { t.Fatalf("ParseStatusOutput failed: %v", err) } if status.Metadata["status_kind"] != "claude" { t.Fatalf("status_kind: got %q", status.Metadata["status_kind"]) } if status.Metadata["claude_status_version"] != "2.1.144" { t.Errorf("version: got %q", status.Metadata["claude_status_version"]) } if status.Metadata["claude_status_login_method"] != "Claude Pro account" { t.Errorf("login_method: got %q", status.Metadata["claude_status_login_method"]) } if status.Metadata["claude_status_model"] != "opus (claude-opus-4-7)" { t.Errorf("model: got %q", status.Metadata["claude_status_model"]) } } 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 TestRenderVisibleScreenTreatsHangulAsWide(t *testing.T) { const ESC = "\x1b" got := RenderVisibleScreen("한글"+ESC+"[2DX", 1, 20) want := "한X\n" if got != want { t.Fatalf("RenderVisibleScreen() = %q, want %q", got, want) } } func TestRenderVisibleScreenClearsWideContinuation(t *testing.T) { const ESC = "\x1b" got := RenderVisibleScreen("한글"+ESC+"[2D"+ESC+"[K", 1, 20) want := "한\n" if got != want { t.Fatalf("RenderVisibleScreen() = %q, want %q", got, want) } } func TestParseStatusOutput_GeminiStatsQuota(t *testing.T) { rawOutput := "Auto (Gemini 3) Stats For Nerds\n" + "0% used (Limit resets in 24h)\n" + "Usage limit: 200\n" + "Usage limits span all sessions and reset daily.\n" status, err := ParseStatusOutput(rawOutput) if err != nil { t.Fatalf("ParseStatusOutput failed: %v", err) } if status.DailyLimit != "100%" { t.Errorf("DailyLimit: got %q want %q", status.DailyLimit, "100%") } if status.DailyResetTime != "24h" { t.Errorf("DailyResetTime: got %q want %q", status.DailyResetTime, "24h") } if status.WeeklyLimit != "" { t.Errorf("WeeklyLimit should be empty, got %q", status.WeeklyLimit) } if status.Metadata["daily_label"] != "Daily quota" { t.Errorf("daily_label: got %q want %q", status.Metadata["daily_label"], "Daily quota") } if status.Metadata["used_percent"] != "0%" { t.Errorf("used_percent: got %q want %q", status.Metadata["used_percent"], "0%") } if status.Metadata["usage_limit"] != "200" { t.Errorf("usage_limit: got %q want %q", status.Metadata["usage_limit"], "200") } } func TestParseStatusOutput_GeminiLimitReached(t *testing.T) { rawOutput := "Auto (Gemini 3) Stats For Nerds\n" + "Limit reached, resets in 5h\n" + "Usage limit: 200\n" status, err := ParseStatusOutput(rawOutput) if err != nil { t.Fatalf("ParseStatusOutput failed: %v", err) } if status.DailyLimit != "0%" { t.Errorf("DailyLimit: got %q want %q", status.DailyLimit, "0%") } if status.DailyResetTime != "5h" { t.Errorf("DailyResetTime: got %q want %q", status.DailyResetTime, "5h") } if status.Metadata["used_percent"] != "100%" { t.Errorf("used_percent: got %q want %q", status.Metadata["used_percent"], "100%") } if status.Metadata["usage_limit"] != "200" { t.Errorf("usage_limit: got %q want %q", status.Metadata["usage_limit"], "200") } } func TestParseStatusOutput_GeminiScreenReaderQuotaWithoutReset(t *testing.T) { rawOutput := "workspace (/directory)\n" + "~/workspace/iop branch\n" + "main sandbox\n" + "no sandbox /model\n" + "Auto (Gemini 3) quota\n" + "0% used\n" + "No API calls have been made in this session.\n" status, err := ParseStatusOutput(rawOutput) if err != nil { t.Fatalf("ParseStatusOutput failed: %v", err) } if status.DailyLimit != "100%" { t.Errorf("DailyLimit: got %q want %q", status.DailyLimit, "100%") } if status.DailyResetTime != "" { t.Errorf("DailyResetTime should be empty, got %q", status.DailyResetTime) } if status.Metadata["daily_label"] != "Daily quota" { t.Errorf("daily_label: got %q want %q", status.Metadata["daily_label"], "Daily quota") } if status.Metadata["used_percent"] != "0%" { t.Errorf("used_percent: got %q want %q", status.Metadata["used_percent"], "0%") } if status.Metadata["usage_limit"] != "" { t.Errorf("usage_limit should be empty, got %q", status.Metadata["usage_limit"]) } } func TestParseStatusOutput_GeminiModelUsage(t *testing.T) { rawOutput := "Select Model\n" + "(checked) 1. Auto (Gemini 3)\n" + "Model usage\n" + "Flash ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 0% Resets: 2:15 PM (24h)\n" + "Flash Lite ▬ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 1% Resets: 12:48 PM (22h 34m)\n" + "\n" + "Pro ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 2% Resets: 2:15 PM (24h)\n" + "gemini-3.1-flash-lite ▬ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 3% Resets: 12:48 PM\n" + "(22h 34m)\n" + "(Press Esc to close)\n" status, err := ParseStatusOutput(rawOutput) if err != nil { t.Fatalf("ParseStatusOutput failed: %v", err) } if status.Metadata["model_usage_count"] != "4" { t.Fatalf("model_usage_count: got %q want 4", status.Metadata["model_usage_count"]) } tests := []struct { idx string name string used string reset string }{ {"0", "Flash", "0%", "2:15 PM (24h)"}, {"1", "Flash Lite", "1%", "12:48 PM (22h 34m)"}, {"2", "Pro", "2%", "2:15 PM (24h)"}, {"3", "gemini-3.1-flash-lite", "3%", "12:48 PM (22h 34m)"}, } for _, tt := range tests { prefix := "model_usage_" + tt.idx if status.Metadata[prefix+"_name"] != tt.name { t.Errorf("%s_name: got %q want %q", prefix, status.Metadata[prefix+"_name"], tt.name) } if status.Metadata[prefix+"_used_percent"] != tt.used { t.Errorf("%s_used_percent: got %q want %q", prefix, status.Metadata[prefix+"_used_percent"], tt.used) } if status.Metadata[prefix+"_reset"] != tt.reset { t.Errorf("%s_reset: got %q want %q", prefix, status.Metadata[prefix+"_reset"], tt.reset) } } } func TestParseStatusOutput_GeminiNoAPICallsLeavesRawOnly(t *testing.T) { rawOutput := "Auto (Gemini 3) Stats For Nerds\n" + "No API calls have been made in this session.\n" status, err := ParseStatusOutput(rawOutput) if err != nil { t.Fatalf("ParseStatusOutput failed: %v", err) } if status.DailyLimit != "" { t.Errorf("DailyLimit should be empty, got %q", status.DailyLimit) } if status.WeeklyLimit != "" { t.Errorf("WeeklyLimit should be empty, got %q", status.WeeklyLimit) } if status.RawOutput == "" { t.Error("RawOutput should not be empty") } } 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"]) } }