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
36 changes: 36 additions & 0 deletions packages/claude-code-plugin/hooks/lib/achievement_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,27 @@
"en": {
"unlocked": "Achievement Unlocked!",
"congrats": "Congratulations!",
"more_unlocked": "+{n} more achievements unlocked!",
},
"ko": {
"unlocked": "업적 달성!",
"congrats": "축하합니다!",
"more_unlocked": "+{n}개의 업적을 추가 달성!",
},
"ja": {
"unlocked": "実績解除!",
"congrats": "おめでとうございます!",
"more_unlocked": "+{n}件の実績を解除!",
},
"zh": {
"unlocked": "成就解锁!",
"congrats": "恭喜!",
"more_unlocked": "+{n}个成就已解锁!",
},
"es": {
"unlocked": "Logro desbloqueado!",
"congrats": "Felicitaciones!",
"more_unlocked": "+{n} logros más desbloqueados!",
},
}

Expand Down Expand Up @@ -383,6 +388,37 @@ def render_achievement_celebration(
return "\n".join(lines)


def render_batch_celebration(
newly_unlocked: List[Dict[str, Any]],
language: str = "en",
) -> str:
"""Render a batch celebration for one or more newly unlocked achievements.

When a single achievement is unlocked, delegates to render_achievement_celebration.
When multiple are unlocked, shows the first in detail and summarises the rest.

Args:
newly_unlocked: List of achievement definition dicts just unlocked.
language: Language code.

Returns:
Formatted celebration string, or empty string if list is empty.
"""
if not newly_unlocked:
return ""

if len(newly_unlocked) == 1:
return render_achievement_celebration(newly_unlocked[0], language)

# Multiple: show first achievement in detail, summarise the rest
top = newly_unlocked[0]
detail = render_achievement_celebration(top, language)
msgs = CELEBRATION_MESSAGES.get(language, CELEBRATION_MESSAGES["en"])
remaining = len(newly_unlocked) - 1
summary_line = f" {msgs['more_unlocked'].format(n=remaining)}"
return f"{detail}\n{summary_line}"


def render_achievement_badges(
unlocked: List[Dict[str, Any]],
language: str = "en",
Expand Down
10 changes: 5 additions & 5 deletions packages/claude-code-plugin/hooks/stop.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def handle_stop(data: dict):
try:
from achievement_tracker import (
AchievementTracker,
render_achievement_celebration,
render_batch_celebration,
render_achievement_badges,
)

Expand All @@ -145,10 +145,10 @@ def handle_stop(data: dict):

newly_unlocked = tracker.check_achievements()
if newly_unlocked:
for achievement in newly_unlocked:
celebration = render_achievement_celebration(
achievement, language
)
celebration = render_batch_celebration(
newly_unlocked, language
)
if celebration:
print(celebration, file=sys.stderr)

# Show badge summary if any unlocked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
get_achievement_definitions,
render_achievement_badges,
render_achievement_celebration,
render_batch_celebration,
)


Expand Down Expand Up @@ -350,3 +351,54 @@ def test_render_unknown_id(self):
unlocked = [{"id": "unknown_xyz", "name": "Mystery"}]
result = render_achievement_badges(unlocked, "en")
assert "Mystery" in result


class TestRenderBatchCelebration:
"""Test batch celebration rendering (#1042)."""

def test_empty_list(self):
result = render_batch_celebration([], "en")
assert result == ""

def test_single_achievement(self):
defn = get_achievement_by_id("tdd_champion")
result = render_batch_celebration([defn], "en")
expected = render_achievement_celebration(defn, "en")
assert result == expected

def test_three_achievements(self):
ids = ["tdd_champion", "agent_master", "streak"]
achievements = []
for aid in ids:
defn = get_achievement_by_id(aid)
if defn:
achievements.append(defn)
# Pad with fallback dicts if not enough real definitions
while len(achievements) < 3:
achievements.append({"name": f"Test{len(achievements)}", "icon": "\U0001f3c6"})

result = render_batch_celebration(achievements, "en")
# First achievement should be detailed
assert achievements[0]["name"] in result
assert "Achievement Unlocked!" in result
# Summary line for remaining
assert "+2 more achievements unlocked!" in result

def test_three_achievements_korean(self):
achievements = [
{"name": "First", "icon": "\U0001f3c6", "face": "\u2605\u203f\u2605"},
{"name": "Second", "icon": "\U0001f3c6"},
{"name": "Third", "icon": "\U0001f3c6"},
]
result = render_batch_celebration(achievements, "ko")
assert "업적 달성!" in result
assert "+2개의 업적을 추가 달성!" in result

def test_two_achievements(self):
achievements = [
{"name": "Alpha", "icon": "\U0001f3c6", "face": "\u2605\u203f\u2605"},
{"name": "Beta", "icon": "\U0001f3c6"},
]
result = render_batch_celebration(achievements, "en")
assert "Alpha" in result
assert "+1 more achievements unlocked!" in result
Loading