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
23 changes: 14 additions & 9 deletions cli_output/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ def _display_credit_line(plan_credits: dict) -> None:
console.print(f" {plan_label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")


def _display_purchased_credit_line(bucket: dict) -> None:
"""Display a purchased credit line with progress bar."""
def _display_bucket_credit_line(bucket: dict, label: str) -> None:
"""Display a credit bucket line (purchased or promo) with progress bar."""
remaining = bucket["remaining"]
total = bucket["total"]
expiry_date = bucket["expiry_date"]
Expand All @@ -71,12 +71,12 @@ def _display_purchased_credit_line(bucket: dict) -> None:
bar = _create_progress_bar(remaining, total, width=30)

if is_expired:
console.print(f" Purchased {bar} expired {formatted_date}")
console.print(f" {label:10} {bar} expired {formatted_date}")
else:
console.print(f" Purchased {bar} {remaining:2} of {total} remaining expires {formatted_date}")
console.print(f" {label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")


def _display_status_message(plan_credits: Optional[dict], purchased_credits: list) -> None:
def _display_status_message(plan_credits: Optional[dict], purchased_credits: list, promo_credits: list) -> None:
"""Display appropriate status message based on credit state."""
has_remaining = False

Expand All @@ -93,8 +93,8 @@ def _display_status_message(plan_credits: Optional[dict], purchased_credits: lis
if remaining > 0 and period_end > now:
has_remaining = True

# Check if any purchased credits have remaining balance and not expired
for bucket in purchased_credits:
# Check if any purchased or promo credits have remaining balance and not expired
for bucket in [*purchased_credits, *promo_credits]:
dt_str = bucket["expiry_date"].replace("Z", "+00:00")
expiry_date = datetime.fromisoformat(dt_str)
# If naive datetime, assume UTC
Expand Down Expand Up @@ -127,6 +127,7 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
org_owner = response.get("organization_owner_email")
plan_credits = response.get("plan_credits")
purchased_credits = response.get("purchased_credits", [])
promo_credits = response.get("promo_credits", [])

# Display header information
if client_version_valid:
Expand All @@ -153,8 +154,12 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:

# Display purchased credits
for bucket in purchased_credits:
_display_purchased_credit_line(bucket)
_display_bucket_credit_line(bucket, "Purchased")

# Display promo credits
for bucket in promo_credits:
_display_bucket_credit_line(bucket, "Promo")

# Display status messages and management link
_display_status_message(plan_credits, purchased_credits)
_display_status_message(plan_credits, purchased_credits, promo_credits)
console.print("\nTo manage your plan navigate to https://platform.codeplain.ai/plans")
130 changes: 118 additions & 12 deletions tests/test_cli_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from cli_output.status import (
_create_progress_bar,
_display_bucket_credit_line,
_display_credit_line,
_display_purchased_credit_line,
_display_status_message,
print_status,
)
Expand Down Expand Up @@ -132,8 +132,8 @@ def test_timezone_naive_datetime(self, mock_console):
mock_console.print.assert_called_once()


class TestDisplayPurchasedCreditLine:
"""Tests for _display_purchased_credit_line function."""
class TestDisplayBucketCreditLine:
"""Tests for _display_bucket_credit_line function."""

@patch("cli_output.status.console")
def test_display_active_purchased_credits(self, mock_console):
Expand All @@ -143,13 +143,28 @@ def test_display_active_purchased_credits(self, mock_console):
"remaining": 20,
"expiry_date": "2028-12-12T00:00:00+00:00",
}
_display_purchased_credit_line(bucket)
_display_bucket_credit_line(bucket, "Purchased")

call_args = mock_console.print.call_args[0][0]
assert "Purchased" in call_args
assert "20 of 100 remaining" in call_args
assert "expires Dec 12, 2028" in call_args

@patch("cli_output.status.console")
def test_display_active_promo_credits(self, mock_console):
"""Test displaying active promo credits."""
bucket = {
"total": 100,
"remaining": 20,
"expiry_date": "2028-12-12T00:00:00+00:00",
}
_display_bucket_credit_line(bucket, "Promo")

call_args = mock_console.print.call_args[0][0]
assert "Promo" in call_args
assert "20 of 100 remaining" in call_args
assert "expires Dec 12, 2028" in call_args

@patch("cli_output.status.console")
def test_display_expired_purchased_credits(self, mock_console):
"""Test displaying expired purchased credits."""
Expand All @@ -158,7 +173,7 @@ def test_display_expired_purchased_credits(self, mock_console):
"remaining": 20,
"expiry_date": "2024-01-01T00:00:00+00:00",
}
_display_purchased_credit_line(bucket)
_display_bucket_credit_line(bucket, "Purchased")

call_args = mock_console.print.call_args[0][0]
assert "expired Jan 1, 2024" in call_args
Expand All @@ -173,7 +188,7 @@ def test_timezone_naive_datetime(self, mock_console):
"expiry_date": "2026-06-12T00:00:00", # No timezone
}
# Should not raise exception
_display_purchased_credit_line(bucket)
_display_bucket_credit_line(bucket, "Purchased")
mock_console.print.assert_called_once()


Expand All @@ -187,7 +202,7 @@ def test_has_active_plan_credits(self, mock_console):
"remaining": 10,
"period_end": "2028-12-01T00:00:00+00:00",
}
_display_status_message(plan_credits, [])
_display_status_message(plan_credits, [], [])

# Should not print warning message
mock_console.print.assert_not_called()
Expand All @@ -201,7 +216,21 @@ def test_has_active_purchased_credits(self, mock_console):
"expiry_date": "2030-06-12T00:00:00+00:00",
}
]
_display_status_message(None, purchased_credits)
_display_status_message(None, purchased_credits, [])

# Should not print warning message
mock_console.print.assert_not_called()

@patch("cli_output.status.console")
def test_has_active_promo_credits(self, mock_console):
"""Test when user only has active promo credits."""
promo_credits = [
{
"remaining": 20,
"expiry_date": "2030-06-12T00:00:00+00:00",
}
]
_display_status_message(None, [], promo_credits)

# Should not print warning message
mock_console.print.assert_not_called()
Expand All @@ -213,7 +242,7 @@ def test_no_credits_remaining(self, mock_console):
"remaining": 0,
"period_end": "2028-12-01T00:00:00+00:00",
}
_display_status_message(plan_credits, [])
_display_status_message(plan_credits, [], [])

mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
Expand All @@ -226,16 +255,16 @@ def test_expired_plan_credits(self, mock_console):
"remaining": 10,
"period_end": "2024-01-01T00:00:00+00:00",
}
_display_status_message(plan_credits, [])
_display_status_message(plan_credits, [], [])

mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
assert "No rendering credits remaining" in call_args

@patch("cli_output.status.console")
def test_null_plan_credits_and_empty_purchased(self, mock_console):
"""Test when plan_credits is None and purchased_credits is empty."""
_display_status_message(None, [])
"""Test when plan_credits is None and purchased/promo credits are empty."""
_display_status_message(None, [], [])

mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
Expand Down Expand Up @@ -384,6 +413,83 @@ def test_multiple_purchased_credit_buckets(self, mock_console, mock_api_class):
purchased_calls = [c for c in calls if "Purchased" in c]
assert len(purchased_calls) == 2

@patch("cli_output.status.codeplain_api.CodeplainAPI")
@patch("cli_output.status.console")
def test_promo_credit_buckets(self, mock_console, mock_api_class):
"""Test status display includes promo credit buckets."""
mock_api = Mock()
mock_api_class.return_value = mock_api
mock_api.connection_check.return_value = {
"client_version_valid": True,
"min_client_version": "0.3.0",
}
mock_api.status.return_value = {
"user": {
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
},
"api_key_label": "test-key",
"organization_owner_email": "owner@example.com",
"plan_credits": None,
"purchased_credits": [
{
"total": 100,
"remaining": 50,
"expiry_date": "2030-06-12T00:00:00+00:00",
},
],
"promo_credits": [
{
"total": 30,
"remaining": 15,
"expiry_date": "2030-12-31T00:00:00+00:00",
},
],
}

print_status("fake-key", "http://localhost:5000", "0.3.0")

calls = [str(call) for call in mock_console.print.call_args_list]
promo_calls = [c for c in calls if "Promo" in c]
assert len(promo_calls) == 1
# Active credits remain, so no "no credits remaining" warning
assert not any("No rendering credits remaining" in c for c in calls)

@patch("cli_output.status.codeplain_api.CodeplainAPI")
@patch("cli_output.status.console")
def test_missing_promo_credits_key_is_backward_compatible(self, mock_console, mock_api_class):
"""Test status display when API response omits promo_credits (older API)."""
mock_api = Mock()
mock_api_class.return_value = mock_api
mock_api.connection_check.return_value = {
"client_version_valid": True,
"min_client_version": "0.3.0",
}
mock_api.status.return_value = {
"user": {
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
},
"api_key_label": "test-key",
"organization_owner_email": "owner@example.com",
"plan_credits": {
"type": "free",
"total": 50,
"remaining": 10,
"period_end": "2030-12-01T00:00:00+00:00",
},
"purchased_credits": [],
# promo_credits intentionally omitted
}

# Should not raise
print_status("fake-key", "http://localhost:5000", "0.3.0")

calls = [str(call) for call in mock_console.print.call_args_list]
assert not any("Promo" in c for c in calls)


class TestVersionFlag:
"""Tests for --version flag."""
Expand Down
Loading