Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/claude-code-plugin/hooks/lib/tdd_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""TDD cycle mini progress bar for status display (#1035).

Builds a visual indicator showing current TDD phase progress:
[RED ● GREEN ○ REFACTOR ○]

Reads phase from CODINGBUDDY_TDD_PHASE env var or explicit argument.
"""
import os
from typing import Optional

# TDD phases in sequential order
TDD_PHASES = ["RED", "GREEN", "REFACTOR"]

# Display symbols
ACTIVE = "\u25cf" # ●
INACTIVE = "\u25cb" # ○


def build_tdd_indicator(
phase: Optional[str] = None,
cycle_count: Optional[int] = None,
) -> Optional[str]:
"""Build a one-line TDD cycle progress indicator.

Args:
phase: Current TDD phase (RED, GREEN, REFACTOR).
If None, reads from CODINGBUDDY_TDD_PHASE env var.
cycle_count: Number of completed TDD cycles. Appended as '#N'
when > 0.

Returns:
Formatted string like '[RED ● GREEN ○ REFACTOR ○]' or None
if no valid phase is active.
"""
if phase is None:
phase = os.environ.get("CODINGBUDDY_TDD_PHASE", "")

if not phase:
return None

phase = phase.upper().strip()
if phase not in TDD_PHASES:
return None

phase_index = TDD_PHASES.index(phase)
parts = []
for i, p in enumerate(TDD_PHASES):
symbol = ACTIVE if i <= phase_index else INACTIVE
parts.append(f"{p} {symbol}")

indicator = f"[{' '.join(parts)}]"

if cycle_count and cycle_count > 0:
indicator = f"{indicator} #{cycle_count}"

return indicator
9 changes: 9 additions & 0 deletions packages/claude-code-plugin/hooks/pre-tool-use.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from config import get_config
from agent_status import build_status_message
from adaptive_perf import get_monitor
from tdd_progress import build_tdd_indicator

# Pattern to detect git commit in a command string
_GIT_COMMIT_RE = re.compile(r"\bgit\s+commit\b")
Expand Down Expand Up @@ -179,6 +180,14 @@ def _handle(data: dict) -> Optional[dict]:
# Build agent status message for spinner (#974) — applies to ALL tools
status_msg = build_status_message()

# Append TDD cycle progress indicator (#1035)
tdd_indicator = build_tdd_indicator()
if tdd_indicator:
if status_msg:
status_msg = f"{status_msg} {tdd_indicator}"
else:
status_msg = tdd_indicator

tool_name = data.get("tool_name", "")
contexts = []

Expand Down
87 changes: 87 additions & 0 deletions packages/claude-code-plugin/hooks/tests/test_tdd_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Tests for tdd_progress module (#1035)."""
import os
import sys

import pytest

# Add lib to path
_hooks_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_lib_dir = os.path.join(_hooks_dir, "lib")
if _lib_dir not in sys.path:
sys.path.insert(0, _lib_dir)

from tdd_progress import build_tdd_indicator


class TestBuildTddIndicator:
"""Test build_tdd_indicator function."""

def test_red_phase(self):
result = build_tdd_indicator("RED")
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"

def test_green_phase(self):
result = build_tdd_indicator("GREEN")
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb]"

def test_refactor_phase(self):
result = build_tdd_indicator("REFACTOR")
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cf]"

def test_none_returns_none(self):
assert build_tdd_indicator(None) is None

def test_empty_string_returns_none(self):
assert build_tdd_indicator("") is None

def test_invalid_phase_returns_none(self):
assert build_tdd_indicator("INVALID") is None

def test_lowercase_phase(self):
result = build_tdd_indicator("red")
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"

def test_mixed_case_phase(self):
result = build_tdd_indicator("Green")
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb]"

def test_whitespace_trimmed(self):
result = build_tdd_indicator(" RED ")
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"


class TestBuildTddIndicatorFromEnv:
"""Test build_tdd_indicator reads from environment when no arg given."""

def test_reads_env_var(self, monkeypatch):
monkeypatch.setenv("CODINGBUDDY_TDD_PHASE", "GREEN")
result = build_tdd_indicator()
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb]"

def test_no_env_var_returns_none(self, monkeypatch):
monkeypatch.delenv("CODINGBUDDY_TDD_PHASE", raising=False)
assert build_tdd_indicator() is None

def test_empty_env_var_returns_none(self, monkeypatch):
monkeypatch.setenv("CODINGBUDDY_TDD_PHASE", "")
assert build_tdd_indicator() is None


class TestBuildTddIndicatorWithCycleCount:
"""Test TDD indicator with cycle count display."""

def test_red_with_cycle_count(self):
result = build_tdd_indicator("RED", cycle_count=3)
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb] #3"

def test_green_with_cycle_count(self):
result = build_tdd_indicator("GREEN", cycle_count=1)
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb] #1"

def test_zero_cycle_count_omitted(self):
result = build_tdd_indicator("RED", cycle_count=0)
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"

def test_none_cycle_count_omitted(self):
result = build_tdd_indicator("RED", cycle_count=None)
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"
Loading