package status import ( "context" "os" "path/filepath" "strings" "testing" "time" ) func TestClaudeCheckerRequestsStatus(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") script := `#!/usr/bin/env sh # Wait for startup readiness printf 'Claude Code v2.1.142\n' printf 'Try /help\n' sleep 0.1 # Read command from stdin IFS= read -r command # If not /status (strip Ctrl+S prefix \x15 if present) cmd="${command#?}" if [ "$cmd" != "/status" ] && [ "$command" != "/status" ]; then printf 'unexpected command: %%s\n' "$command" exit 3 fi # Print Claude /status output printf 'Current session: [████████░░░░░░░░░░░░] 2%% used\n' printf 'Resets 10am (Asia/Seoul)\n' printf '\n' printf 'Current week (all models): [███████████████░░░░░] 50%% used\n' printf 'Resets May 16, 6pm (Asia/Seoul)\n' # Read exit command IFS= read -r exit_command # Cleanup pkill -P $$ 2>/dev/null || true exit 0 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) status, err := checker.Check(ctx) if err != nil { t.Fatalf("Check failed: %v", err) } if status.DailyLimit != "98%" { t.Fatalf("DailyLimit: got %q want %q", status.DailyLimit, "98%") } if status.DailyResetTime != "10am (Asia/Seoul)" { t.Fatalf("DailyResetTime: got %q want %q", status.DailyResetTime, "10am (Asia/Seoul)") } if status.WeeklyLimit != "50%" { t.Fatalf("WeeklyLimit: got %q want %q", status.WeeklyLimit, "50%") } if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" { t.Fatalf("WeeklyResetTime: got %q want %q", status.WeeklyResetTime, "May 16, 6pm (Asia/Seoul)") } } func TestClaudeCheckerWaitsForDelayedUsageWeek(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") script := `#!/usr/bin/env sh printf 'Claude Code v2.1.142\n' printf 'Try /help\n' sleep 0.1 IFS= read -r command cmd="${command#?}" if [ "$cmd" != "/status" ] && [ "$command" != "/status" ]; then printf 'unexpected command: %%s\n' "$command" exit 3 fi # Print Current session first, then sleep before week output printf 'Current session: [████████░░░░░░░░░░░░] 2%% used\n' printf 'Resets 10am (Asia/Seoul)\n' sleep 1 # Print Current week after delay printf '\n' printf 'Current week (all models): [███████████████░░░░░] 50%% used\n' printf 'Resets May 16, 6pm (Asia/Seoul)\n' IFS= read -r exit_command pkill -P $$ 2>/dev/null || true exit 0 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) status, err := checker.Check(ctx) if err != nil { t.Fatalf("Check failed: %v", err) } if status.DailyLimit != "98%" { t.Fatalf("DailyLimit: got %q want %q", status.DailyLimit, "98%") } if status.DailyResetTime != "10am (Asia/Seoul)" { t.Fatalf("DailyResetTime: got %q want %q", status.DailyResetTime, "10am (Asia/Seoul)") } if status.WeeklyLimit != "50%" { t.Fatalf("WeeklyLimit: got %q want %q", status.WeeklyLimit, "50%") } if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" { t.Fatalf("WeeklyResetTime: got %q want %q", status.WeeklyResetTime, "May 16, 6pm (Asia/Seoul)") } } func TestClaudeCheckerReturnsRawFallbackWhenUsageHasNoLimitBlock(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") script := `#!/usr/bin/env sh printf 'Claude Code v2.1.142\n' printf '? for shortcuts\n' sleep 0.1 IFS= read -r command cmd="${command#?}" if [ "$cmd" != "/status" ] && [ "$command" != "/status" ]; then printf 'unexpected command: %%s\n' "$command" exit 3 fi printf 'Last 24h · these are independent characteristics of your usage, not a breakdown\n' printf 'Nothing over 10%% in this period — try the other window.\n' printf 'd to day · w to week\n' printf 'Esc to cancel\n' exit 0 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) status, err := checker.Check(ctx) if err != nil { t.Fatalf("Check failed: %v", err) } if status.DailyLimit != "" || status.WeeklyLimit != "" { t.Fatalf("expected no parsed limits, got daily=%q weekly=%q", status.DailyLimit, status.WeeklyLimit) } if !strings.Contains(status.RawOutput, "Nothing over 10%") { t.Fatalf("expected raw fallback output, got %q", status.RawOutput) } } func TestClaudeCheckerSelectsUsageAndParsesRepaintedScreen(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") // Mimics the real TUI: startup banner, then /status panel that first paints // a loading state ("Scanning local sessions...", "Esc to cancel") via cursor // repaints, then later renders the actual Current session / Current week // content. The parser must wait through the loading state rather than // timing out on the partial repaint. script := `#!/usr/bin/env sh printf 'Claude Code v2.1.142\n' printf '? for shortcuts\n' sleep 0.1 IFS= read -r command cmd="${command#?}" if [ "$cmd" != "/status" ] && [ "$command" != "/status" ]; then printf 'unexpected command: %%s\n' "$command" exit 3 fi # Loading state with cursor repositioning escapes and "Esc to cancel" tail. printf '\033[2J\033[H' printf ' Usage\n' printf ' Scanning local sessions...\n' printf ' Refreshing...\n' printf ' Esc to cancel\n' sleep 1 # Repaint with real data after data fetch. printf '\033[2J\033[H' printf ' Settings Status Config Usage Stats\n' printf '\n' printf ' Session\n' printf '\n' printf ' Current session\n' printf ' \033[48;5;102m\033[38;5;153m## \033[49m\033[39m 4%% used\n' printf ' Resets 10am (Asia/Seoul)\n' printf '\n' printf ' Current week (all models)\n' printf ' 0%% used\n' printf ' Resets May 16, 6pm (Asia/Seoul)\n' printf '\n' printf ' Esc to cancel\n' IFS= read -r exit_command pkill -P $$ 2>/dev/null || true exit 0 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) status, err := checker.Check(ctx) if err != nil { t.Fatalf("Check failed: %v", err) } if status.DailyLimit != "96%" { t.Fatalf("DailyLimit: got %q want %q", status.DailyLimit, "96%") } if status.DailyResetTime != "10am (Asia/Seoul)" { t.Fatalf("DailyResetTime: got %q want %q", status.DailyResetTime, "10am (Asia/Seoul)") } if status.WeeklyLimit != "100%" { t.Fatalf("WeeklyLimit: got %q want %q", status.WeeklyLimit, "100%") } 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["daily_label"] != "Current session" { t.Fatalf("daily_label: got %q want %q", status.Metadata["daily_label"], "Current session") } if status.Metadata["weekly_label"] != "Current week" { t.Fatalf("weekly_label: got %q want %q", status.Metadata["weekly_label"], "Current week") } } func TestClaudeCheckerParsesCursorRepaintedUsageScreen(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") // Fake TUI that paints the /status panel via cursor-addressed writes, // matching how real Claude renders. We first write misleading payload // text ("21% of your usage" / "Resets garbage time") at rows 6/7, then // reposition the cursor and use EL (erase line) to overwrite those rows // with the actual data. In the raw stream both the garbage and the real // payload remain interleaved; only a terminal screen model that applies // the cursor moves and erases sees the final visible payload. // // Implementations that parse the line-oriented cleanANSI output without // rendering the screen will either pick up "21%" / "garbage time" as the // session payload or fail to find a coherent section at all, so they will // not satisfy the final assertions. script := `#!/usr/bin/env bash printf 'Claude Code v2.1.143\n' printf '? for shortcuts\n' sleep 0.1 IFS= read -r command cmd="${command#?}" if [ "$cmd" != "/status" ] && [ "$command" != "/status" ]; then printf 'unexpected command: %s\n' "$command" exit 3 fi ESC=$'\x1b' # Clear screen, paint loading state with misleading numbers. printf '%s[2J%s[H' "$ESC" "$ESC" printf '%s[5;1H Current session' "$ESC" printf '%s[6;1H 21%% of your usage in last 24h' "$ESC" printf '%s[7;1H Resets garbage time' "$ESC" printf '%s[9;1H Current week (all models)' "$ESC" printf '%s[10;1H 47%% of week things here' "$ESC" printf '%s[11;1H Resets garbage week time' "$ESC" printf '%s[13;1H Last 24h' "$ESC" printf '%s[14;1H d to day · w to week' "$ESC" printf '%s[15;1H Extra usage' "$ESC" printf '%s[16;1H Esc to cancel' "$ESC" sleep 0.3 # Repaint with real payload by erasing and rewriting the same rows. printf '%s[6;1H%s[2K #### 4%% used' "$ESC" "$ESC" printf '%s[7;1H%s[2K Resets 10am (Asia/Seoul)' "$ESC" "$ESC" printf '%s[10;1H%s[2K 0%% used' "$ESC" "$ESC" printf '%s[11;1H%s[2K Resets May 16, 6pm (Asia/Seoul)' "$ESC" "$ESC" # Idle until the checker tears down the PTY. sleep 30 exit 0 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) status, err := checker.Check(ctx) if err != nil { t.Fatalf("Check failed: %v", err) } if status.DailyLimit != "96%" { t.Fatalf("DailyLimit: got %q want %q", status.DailyLimit, "96%") } if status.DailyResetTime != "10am (Asia/Seoul)" { t.Fatalf("DailyResetTime: got %q want %q", status.DailyResetTime, "10am (Asia/Seoul)") } if status.WeeklyLimit != "100%" { t.Fatalf("WeeklyLimit: got %q want %q", status.WeeklyLimit, "100%") } 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["daily_label"] != "Current session" { t.Fatalf("daily_label: got %q want %q", status.Metadata["daily_label"], "Current session") } if status.Metadata["weekly_label"] != "Current week" { t.Fatalf("weekly_label: got %q want %q", status.Metadata["weekly_label"], "Current week") } } func TestClaudeCheckerNotStatusExits(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") script := `#!/usr/bin/env sh printf 'Claude Code v2.1.142\n' printf 'Welcome back\n' IFS= read -r command # This is not /status, exit with failure exit 3 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) _, err := checker.Check(ctx) // This test expects the checker to NOT find expected usage output // so it should fail or timeout if err == nil { // The faker exited early so pty Read will return EOF // This is acceptable - we just want to verify non-/status input // causes the exit code 3 behavior t.Log("fake claude exited before /status as expected") } } func TestClaudeCheckerUsesEmptyCommandAsDefault(t *testing.T) { checker := NewClaudeChecker("") if checker.command != "claude" { t.Errorf("NewClaudeChecker(\"\") = %q; want %q", checker.command, "claude") } } func TestClaudeCheckerPreservesCommand(t *testing.T) { checker := NewClaudeChecker("/custom/path/claude") if checker.command != "/custom/path/claude" { t.Errorf("NewClaudeChecker(\"/custom/path/claude\") = %q; want %q", checker.command, "/custom/path/claude") } } func TestClaudeCheckerParsesCarriageReturnUsageScreen(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") script := `#!/usr/bin/env sh printf 'Claude Code v2.1.142\n' printf 'Try /help\r' sleep 0.1 IFS= read -r command cmd="${command#?}" if [ "$cmd" != "/status" ] && [ "$command" != "/status" ]; then printf 'unexpected command: %%s\n' "$command" exit 3 fi # Print usage screen with carriage returns like real TUI printf 'Current session: [████████░░░░░░░░░░░░] 2%% used\r' printf 'Resets 10am (Asia/Seoul)\r' printf '\r' printf 'Current week (all models): [███████████████░░░░░] 50%% used\r' printf 'Resets May 16, 6pm (Asia/Seoul)\r' printf '\r' IFS= read -r exit_command kill -P $$ 2>/dev/null || true exit 0 ` if err := os.WriteFile(fakeClaude, []byte(script), 0o755); err != nil { t.Fatalf("write fake claude: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() checker := NewClaudeChecker(fakeClaude) status, err := checker.Check(ctx) if err != nil { t.Fatalf("Check failed: %v", err) } if status.DailyLimit != "98%" { t.Fatalf("DailyLimit: got %q want %q", status.DailyLimit, "98%") } if status.DailyResetTime != "10am (Asia/Seoul)" { t.Fatalf("DailyResetTime: got %q want %q", status.DailyResetTime, "10am (Asia/Seoul)") } if status.WeeklyLimit != "50%" { t.Fatalf("WeeklyLimit: got %q want %q", status.WeeklyLimit, "50%") } if status.WeeklyResetTime != "May 16, 6pm (Asia/Seoul)" { t.Fatalf("WeeklyResetTime: got %q want %q", status.WeeklyResetTime, "May 16, 6pm (Asia/Seoul)") } }