490 lines
16 KiB
Python
490 lines
16 KiB
Python
"""Unit tests for the quest system.
|
|
|
|
Tests quest definitions, progress tracking, completion detection,
|
|
and token rewards.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from timmy.quest_system import (
|
|
QuestDefinition,
|
|
QuestProgress,
|
|
QuestStatus,
|
|
QuestType,
|
|
_is_on_cooldown,
|
|
claim_quest_reward,
|
|
evaluate_quest_progress,
|
|
get_or_create_progress,
|
|
get_quest_definition,
|
|
get_quest_leaderboard,
|
|
load_quest_config,
|
|
reset_quest_progress,
|
|
update_quest_progress,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_quest_state():
|
|
"""Reset quest progress between tests."""
|
|
reset_quest_progress()
|
|
yield
|
|
reset_quest_progress()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_issue_count_quest():
|
|
"""Create a sample issue_count quest definition."""
|
|
return QuestDefinition(
|
|
id="test_close_issues",
|
|
name="Test Issue Closer",
|
|
description="Close 3 test issues",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=False,
|
|
cooldown_hours=0,
|
|
criteria={"target_count": 3, "issue_labels": ["test"]},
|
|
notification_message="Test quest complete! Earned {tokens} tokens.",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_daily_run_quest():
|
|
"""Create a sample daily_run quest definition."""
|
|
return QuestDefinition(
|
|
id="test_daily_run",
|
|
name="Test Daily Runner",
|
|
description="Complete 5 sessions",
|
|
reward_tokens=250,
|
|
quest_type=QuestType.DAILY_RUN,
|
|
enabled=True,
|
|
repeatable=True,
|
|
cooldown_hours=24,
|
|
criteria={"min_sessions": 5},
|
|
notification_message="Daily run quest complete! Earned {tokens} tokens.",
|
|
)
|
|
|
|
|
|
# ── Quest Definition Tests ───────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestDefinition:
|
|
def test_from_dict_minimal(self):
|
|
data = {"id": "test_quest", "name": "Test Quest"}
|
|
quest = QuestDefinition.from_dict(data)
|
|
assert quest.id == "test_quest"
|
|
assert quest.name == "Test Quest"
|
|
assert quest.quest_type == QuestType.CUSTOM
|
|
assert quest.enabled is True
|
|
|
|
def test_from_dict_full(self):
|
|
data = {
|
|
"id": "full_quest",
|
|
"name": "Full Quest",
|
|
"description": "A test quest",
|
|
"reward_tokens": 500,
|
|
"type": "issue_count",
|
|
"enabled": False,
|
|
"repeatable": True,
|
|
"cooldown_hours": 12,
|
|
"criteria": {"target_count": 5},
|
|
"notification_message": "Done!",
|
|
}
|
|
quest = QuestDefinition.from_dict(data)
|
|
assert quest.id == "full_quest"
|
|
assert quest.reward_tokens == 500
|
|
assert quest.quest_type == QuestType.ISSUE_COUNT
|
|
assert quest.enabled is False
|
|
assert quest.repeatable is True
|
|
assert quest.cooldown_hours == 12
|
|
|
|
|
|
# ── Quest Progress Tests ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestProgress:
|
|
def test_progress_creation(self):
|
|
progress = QuestProgress(
|
|
quest_id="test_quest",
|
|
agent_id="test_agent",
|
|
status=QuestStatus.NOT_STARTED,
|
|
)
|
|
assert progress.quest_id == "test_quest"
|
|
assert progress.agent_id == "test_agent"
|
|
assert progress.current_value == 0
|
|
|
|
def test_progress_to_dict(self):
|
|
progress = QuestProgress(
|
|
quest_id="test_quest",
|
|
agent_id="test_agent",
|
|
status=QuestStatus.IN_PROGRESS,
|
|
current_value=2,
|
|
target_value=5,
|
|
)
|
|
data = progress.to_dict()
|
|
assert data["quest_id"] == "test_quest"
|
|
assert data["status"] == "in_progress"
|
|
assert data["current_value"] == 2
|
|
|
|
|
|
# ── Quest Loading Tests ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestLoading:
|
|
def test_load_quest_config(self):
|
|
definitions, settings = load_quest_config()
|
|
assert isinstance(definitions, dict)
|
|
assert isinstance(settings, dict)
|
|
|
|
def test_get_quest_definition_exists(self):
|
|
# Should return None for non-existent quest in fresh state
|
|
quest = get_quest_definition("nonexistent")
|
|
# The function returns from loaded config, which may have quests
|
|
# or be empty if config doesn't exist
|
|
assert quest is None or isinstance(quest, QuestDefinition)
|
|
|
|
def test_get_quest_definition_not_found(self):
|
|
quest = get_quest_definition("definitely_not_a_real_quest_12345")
|
|
assert quest is None
|
|
|
|
|
|
# ── Quest Progress Management Tests ─────────────────────────────────────
|
|
|
|
|
|
class TestQuestProgressManagement:
|
|
def test_get_or_create_progress_new(self):
|
|
# First create a quest definition
|
|
quest = QuestDefinition(
|
|
id="progress_test",
|
|
name="Progress Test",
|
|
description="Test quest",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=False,
|
|
cooldown_hours=0,
|
|
criteria={"target_count": 3},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
# Need to inject into the definitions dict
|
|
from timmy.quest_system import _quest_definitions
|
|
|
|
_quest_definitions["progress_test"] = quest
|
|
|
|
progress = get_or_create_progress("progress_test", "agent1")
|
|
assert progress.quest_id == "progress_test"
|
|
assert progress.agent_id == "agent1"
|
|
assert progress.status == QuestStatus.NOT_STARTED
|
|
assert progress.target_value == 3
|
|
|
|
del _quest_definitions["progress_test"]
|
|
|
|
def test_update_quest_progress(self):
|
|
quest = QuestDefinition(
|
|
id="update_test",
|
|
name="Update Test",
|
|
description="Test quest",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=False,
|
|
cooldown_hours=0,
|
|
criteria={"target_count": 3},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
from timmy.quest_system import _quest_definitions
|
|
|
|
_quest_definitions["update_test"] = quest
|
|
|
|
# Create initial progress
|
|
progress = get_or_create_progress("update_test", "agent1")
|
|
assert progress.current_value == 0
|
|
|
|
# Update progress
|
|
updated = update_quest_progress("update_test", "agent1", 2)
|
|
assert updated.current_value == 2
|
|
assert updated.status == QuestStatus.NOT_STARTED
|
|
|
|
# Complete the quest
|
|
completed = update_quest_progress("update_test", "agent1", 3)
|
|
assert completed.current_value == 3
|
|
assert completed.status == QuestStatus.COMPLETED
|
|
assert completed.completed_at != ""
|
|
|
|
del _quest_definitions["update_test"]
|
|
|
|
|
|
# ── Quest Evaluation Tests ───────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestEvaluation:
|
|
def test_evaluate_issue_count_quest(self):
|
|
quest = QuestDefinition(
|
|
id="eval_test",
|
|
name="Eval Test",
|
|
description="Test quest",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=False,
|
|
cooldown_hours=0,
|
|
criteria={"target_count": 2, "issue_labels": ["test"]},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
from timmy.quest_system import _quest_definitions
|
|
|
|
_quest_definitions["eval_test"] = quest
|
|
|
|
# Simulate closed issues
|
|
closed_issues = [
|
|
{"id": 1, "labels": [{"name": "test"}]},
|
|
{"id": 2, "labels": [{"name": "test"}, {"name": "bug"}]},
|
|
{"id": 3, "labels": [{"name": "other"}]},
|
|
]
|
|
|
|
context = {"closed_issues": closed_issues}
|
|
progress = evaluate_quest_progress("eval_test", "agent1", context)
|
|
|
|
assert progress is not None
|
|
assert progress.current_value == 2 # Two issues with 'test' label
|
|
|
|
del _quest_definitions["eval_test"]
|
|
|
|
def test_evaluate_issue_reduce_quest(self):
|
|
quest = QuestDefinition(
|
|
id="reduce_test",
|
|
name="Reduce Test",
|
|
description="Test quest",
|
|
reward_tokens=200,
|
|
quest_type=QuestType.ISSUE_REDUCE,
|
|
enabled=True,
|
|
repeatable=False,
|
|
cooldown_hours=0,
|
|
criteria={"target_reduction": 2},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
from timmy.quest_system import _quest_definitions
|
|
|
|
_quest_definitions["reduce_test"] = quest
|
|
|
|
context = {"previous_issue_count": 10, "current_issue_count": 7}
|
|
progress = evaluate_quest_progress("reduce_test", "agent1", context)
|
|
|
|
assert progress is not None
|
|
assert progress.current_value == 3 # Reduced by 3
|
|
|
|
del _quest_definitions["reduce_test"]
|
|
|
|
def test_evaluate_daily_run_quest(self):
|
|
quest = QuestDefinition(
|
|
id="daily_test",
|
|
name="Daily Test",
|
|
description="Test quest",
|
|
reward_tokens=250,
|
|
quest_type=QuestType.DAILY_RUN,
|
|
enabled=True,
|
|
repeatable=True,
|
|
cooldown_hours=24,
|
|
criteria={"min_sessions": 5},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
from timmy.quest_system import _quest_definitions
|
|
|
|
_quest_definitions["daily_test"] = quest
|
|
|
|
context = {"sessions_completed": 5}
|
|
progress = evaluate_quest_progress("daily_test", "agent1", context)
|
|
|
|
assert progress is not None
|
|
assert progress.current_value == 5
|
|
assert progress.status == QuestStatus.COMPLETED
|
|
|
|
del _quest_definitions["daily_test"]
|
|
|
|
|
|
# ── Quest Cooldown Tests ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestCooldown:
|
|
def test_is_on_cooldown_no_cooldown(self):
|
|
quest = QuestDefinition(
|
|
id="cooldown_test",
|
|
name="Cooldown Test",
|
|
description="Test quest",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=True,
|
|
cooldown_hours=24,
|
|
criteria={},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
progress = QuestProgress(
|
|
quest_id="cooldown_test",
|
|
agent_id="agent1",
|
|
status=QuestStatus.CLAIMED,
|
|
)
|
|
|
|
# No last_completed_at means no cooldown
|
|
assert _is_on_cooldown(progress, quest) is False
|
|
|
|
|
|
# ── Quest Reward Tests ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestReward:
|
|
def test_claim_quest_reward_not_completed(self):
|
|
quest = QuestDefinition(
|
|
id="reward_test",
|
|
name="Reward Test",
|
|
description="Test quest",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=False,
|
|
cooldown_hours=0,
|
|
criteria={"target_count": 3},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
from timmy.quest_system import _quest_definitions, _quest_progress
|
|
|
|
_quest_definitions["reward_test"] = quest
|
|
|
|
# Create progress but don't complete
|
|
progress = get_or_create_progress("reward_test", "agent1")
|
|
_quest_progress["agent1:reward_test"] = progress
|
|
|
|
# Try to claim - should fail
|
|
reward = claim_quest_reward("reward_test", "agent1")
|
|
assert reward is None
|
|
|
|
del _quest_definitions["reward_test"]
|
|
|
|
|
|
# ── Leaderboard Tests ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestLeaderboard:
|
|
def test_get_quest_leaderboard_empty(self):
|
|
reset_quest_progress()
|
|
leaderboard = get_quest_leaderboard()
|
|
assert leaderboard == []
|
|
|
|
def test_get_quest_leaderboard_with_data(self):
|
|
# Create and complete a quest for two agents
|
|
quest = QuestDefinition(
|
|
id="leaderboard_test",
|
|
name="Leaderboard Test",
|
|
description="Test quest",
|
|
reward_tokens=100,
|
|
quest_type=QuestType.ISSUE_COUNT,
|
|
enabled=True,
|
|
repeatable=True,
|
|
cooldown_hours=0,
|
|
criteria={"target_count": 1},
|
|
notification_message="Done!",
|
|
)
|
|
|
|
from timmy.quest_system import _quest_definitions, _quest_progress
|
|
|
|
_quest_definitions["leaderboard_test"] = quest
|
|
|
|
# Create progress for agent1 with 2 completions
|
|
progress1 = QuestProgress(
|
|
quest_id="leaderboard_test",
|
|
agent_id="agent1",
|
|
status=QuestStatus.NOT_STARTED,
|
|
completion_count=2,
|
|
)
|
|
_quest_progress["agent1:leaderboard_test"] = progress1
|
|
|
|
# Create progress for agent2 with 1 completion
|
|
progress2 = QuestProgress(
|
|
quest_id="leaderboard_test",
|
|
agent_id="agent2",
|
|
status=QuestStatus.NOT_STARTED,
|
|
completion_count=1,
|
|
)
|
|
_quest_progress["agent2:leaderboard_test"] = progress2
|
|
|
|
leaderboard = get_quest_leaderboard()
|
|
|
|
assert len(leaderboard) == 2
|
|
# agent1 should be first (more tokens)
|
|
assert leaderboard[0]["agent_id"] == "agent1"
|
|
assert leaderboard[0]["total_tokens"] == 200
|
|
assert leaderboard[1]["agent_id"] == "agent2"
|
|
assert leaderboard[1]["total_tokens"] == 100
|
|
|
|
del _quest_definitions["leaderboard_test"]
|
|
|
|
|
|
# ── Quest Reset Tests ─────────────────────────────────────────────────────
|
|
|
|
|
|
class TestQuestReset:
|
|
def test_reset_quest_progress_all(self):
|
|
# Create some progress entries
|
|
progress1 = QuestProgress(
|
|
quest_id="quest1", agent_id="agent1", status=QuestStatus.NOT_STARTED
|
|
)
|
|
progress2 = QuestProgress(
|
|
quest_id="quest2", agent_id="agent2", status=QuestStatus.NOT_STARTED
|
|
)
|
|
|
|
from timmy.quest_system import _quest_progress
|
|
|
|
_quest_progress["agent1:quest1"] = progress1
|
|
_quest_progress["agent2:quest2"] = progress2
|
|
|
|
assert len(_quest_progress) == 2
|
|
|
|
count = reset_quest_progress()
|
|
assert count == 2
|
|
assert len(_quest_progress) == 0
|
|
|
|
def test_reset_quest_progress_specific_quest(self):
|
|
progress1 = QuestProgress(
|
|
quest_id="quest1", agent_id="agent1", status=QuestStatus.NOT_STARTED
|
|
)
|
|
progress2 = QuestProgress(
|
|
quest_id="quest2", agent_id="agent1", status=QuestStatus.NOT_STARTED
|
|
)
|
|
|
|
from timmy.quest_system import _quest_progress
|
|
|
|
_quest_progress["agent1:quest1"] = progress1
|
|
_quest_progress["agent1:quest2"] = progress2
|
|
|
|
count = reset_quest_progress(quest_id="quest1")
|
|
assert count == 1
|
|
assert "agent1:quest1" not in _quest_progress
|
|
assert "agent1:quest2" in _quest_progress
|
|
|
|
def test_reset_quest_progress_specific_agent(self):
|
|
progress1 = QuestProgress(
|
|
quest_id="quest1", agent_id="agent1", status=QuestStatus.NOT_STARTED
|
|
)
|
|
progress2 = QuestProgress(
|
|
quest_id="quest1", agent_id="agent2", status=QuestStatus.NOT_STARTED
|
|
)
|
|
|
|
from timmy.quest_system import _quest_progress
|
|
|
|
_quest_progress["agent1:quest1"] = progress1
|
|
_quest_progress["agent2:quest1"] = progress2
|
|
|
|
count = reset_quest_progress(agent_id="agent1")
|
|
assert count == 1
|
|
assert "agent1:quest1" not in _quest_progress
|
|
assert "agent2:quest1" in _quest_progress
|