feat: habit tracker with gamification + KB updates
Merge feature/habit-tracker into master (squashed): ✨ Habit Tracker Features: - Bead chain visualization (30-day history) - Weekly lives recovery system (+1 life/week) - Lucide icons (zap, shield) replacing emoji - Responsive layout (mobile-optimized) - Navigation links added to all dashboard pages 📚 Knowledge Base: - 40+ trading basics articles with metadata - Daily notes (2026-02-10, 2026-02-11) - Health & insights content - KB index restructuring 🧪 Tests: - Comprehensive test suite (4 test files) - Integration tests for lives recovery - 28/29 tests passing Commits squashed: - feat(habits): bead chain visualization + weekly lives recovery + nav integration - docs(memory): update KB content + daily notes - chore(data): update habits and status data Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
1129
dashboard/tests/test_habits_api.py
Normal file
1129
dashboard/tests/test_habits_api.py
Normal file
File diff suppressed because it is too large
Load Diff
2868
dashboard/tests/test_habits_frontend.py
Normal file
2868
dashboard/tests/test_habits_frontend.py
Normal file
File diff suppressed because it is too large
Load Diff
573
dashboard/tests/test_habits_helpers.py
Normal file
573
dashboard/tests/test_habits_helpers.py
Normal file
@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Tests for habits_helpers.py
|
||||
|
||||
Tests cover all helper functions for habit tracking including:
|
||||
- calculate_streak for all 6 frequency types
|
||||
- should_check_today for all frequency types
|
||||
- get_completion_rate
|
||||
- get_weekly_summary
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Add parent directory to path to import habits_helpers
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from habits_helpers import (
|
||||
calculate_streak,
|
||||
should_check_today,
|
||||
get_completion_rate,
|
||||
get_weekly_summary,
|
||||
check_and_award_weekly_lives
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_streak_daily_consecutive():
|
||||
"""Test daily streak with consecutive days."""
|
||||
today = datetime.now().date()
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=1)).isoformat()},
|
||||
{"date": (today - timedelta(days=2)).isoformat()},
|
||||
]
|
||||
}
|
||||
assert calculate_streak(habit) == 3
|
||||
|
||||
|
||||
def test_calculate_streak_daily_with_gap():
|
||||
"""Test daily streak breaks on gap."""
|
||||
today = datetime.now().date()
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=1)).isoformat()},
|
||||
# Gap here (day 2 missing)
|
||||
{"date": (today - timedelta(days=3)).isoformat()},
|
||||
]
|
||||
}
|
||||
assert calculate_streak(habit) == 2
|
||||
|
||||
|
||||
def test_calculate_streak_daily_empty():
|
||||
"""Test daily streak with no completions."""
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": []
|
||||
}
|
||||
assert calculate_streak(habit) == 0
|
||||
|
||||
|
||||
def test_calculate_streak_specific_days():
|
||||
"""Test specific_days streak (Mon, Wed, Fri)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# Find the most recent Monday
|
||||
days_since_monday = today.weekday()
|
||||
last_monday = today - timedelta(days=days_since_monday)
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [0, 2, 4] # Mon, Wed, Fri (0=Mon in Python weekday)
|
||||
},
|
||||
"completions": [
|
||||
{"date": last_monday.isoformat()}, # Mon
|
||||
{"date": (last_monday - timedelta(days=2)).isoformat()}, # Fri previous week
|
||||
{"date": (last_monday - timedelta(days=4)).isoformat()}, # Wed previous week
|
||||
]
|
||||
}
|
||||
|
||||
# Should count 3 consecutive relevant days
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 1 # At least the most recent relevant day
|
||||
|
||||
|
||||
def test_calculate_streak_x_per_week():
|
||||
"""Test x_per_week streak (3 times per week)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# Find Monday of current week
|
||||
days_since_monday = today.weekday()
|
||||
monday = today - timedelta(days=days_since_monday)
|
||||
|
||||
# Current week: 3 completions (Mon, Tue, Wed)
|
||||
# Previous week: 3 completions (Mon, Tue, Wed)
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "x_per_week",
|
||||
"count": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": monday.isoformat()}, # This week Mon
|
||||
{"date": (monday + timedelta(days=1)).isoformat()}, # This week Tue
|
||||
{"date": (monday + timedelta(days=2)).isoformat()}, # This week Wed
|
||||
# Previous week
|
||||
{"date": (monday - timedelta(days=7)).isoformat()}, # Last week Mon
|
||||
{"date": (monday - timedelta(days=6)).isoformat()}, # Last week Tue
|
||||
{"date": (monday - timedelta(days=5)).isoformat()}, # Last week Wed
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 2 # Both weeks meet the target
|
||||
|
||||
|
||||
def test_calculate_streak_weekly():
|
||||
"""Test weekly streak (at least 1 per week)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "weekly"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()}, # This week
|
||||
{"date": (today - timedelta(days=7)).isoformat()}, # Last week
|
||||
{"date": (today - timedelta(days=14)).isoformat()}, # 2 weeks ago
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 1
|
||||
|
||||
|
||||
def test_calculate_streak_monthly():
|
||||
"""Test monthly streak (at least 1 per month)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
# This month
|
||||
habit = {
|
||||
"frequency": {"type": "monthly"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak >= 1
|
||||
|
||||
|
||||
def test_calculate_streak_custom_interval():
|
||||
"""Test custom interval streak (every 3 days)."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "custom",
|
||||
"interval": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=3)).isoformat()},
|
||||
{"date": (today - timedelta(days=6)).isoformat()},
|
||||
]
|
||||
}
|
||||
|
||||
streak = calculate_streak(habit)
|
||||
assert streak == 3
|
||||
|
||||
|
||||
def test_should_check_today_daily():
|
||||
"""Test should_check_today for daily habit."""
|
||||
habit = {"frequency": {"type": "daily"}}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_specific_days():
|
||||
"""Test should_check_today for specific_days habit."""
|
||||
today_weekday = datetime.now().date().weekday()
|
||||
|
||||
# Habit relevant today
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [today_weekday]
|
||||
}
|
||||
}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
# Habit not relevant today
|
||||
other_day = (today_weekday + 1) % 7
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [other_day]
|
||||
}
|
||||
}
|
||||
assert should_check_today(habit) is False
|
||||
|
||||
|
||||
def test_should_check_today_x_per_week():
|
||||
"""Test should_check_today for x_per_week habit."""
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "x_per_week",
|
||||
"count": 3
|
||||
}
|
||||
}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_weekly():
|
||||
"""Test should_check_today for weekly habit."""
|
||||
habit = {"frequency": {"type": "weekly"}}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_monthly():
|
||||
"""Test should_check_today for monthly habit."""
|
||||
habit = {"frequency": {"type": "monthly"}}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_custom_ready():
|
||||
"""Test should_check_today for custom interval when ready."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "custom",
|
||||
"interval": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": (today - timedelta(days=3)).isoformat()}
|
||||
]
|
||||
}
|
||||
assert should_check_today(habit) is True
|
||||
|
||||
|
||||
def test_should_check_today_custom_not_ready():
|
||||
"""Test should_check_today for custom interval when not ready."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "custom",
|
||||
"interval": 3
|
||||
},
|
||||
"completions": [
|
||||
{"date": (today - timedelta(days=1)).isoformat()}
|
||||
]
|
||||
}
|
||||
assert should_check_today(habit) is False
|
||||
|
||||
|
||||
def test_get_completion_rate_daily_perfect():
|
||||
"""Test completion rate for daily habit with 100%."""
|
||||
today = datetime.now().date()
|
||||
|
||||
completions = []
|
||||
for i in range(30):
|
||||
completions.append({"date": (today - timedelta(days=i)).isoformat()})
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": completions
|
||||
}
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert rate == 100.0
|
||||
|
||||
|
||||
def test_get_completion_rate_daily_half():
|
||||
"""Test completion rate for daily habit with 50%."""
|
||||
today = datetime.now().date()
|
||||
|
||||
completions = []
|
||||
for i in range(0, 30, 2): # Every other day
|
||||
completions.append({"date": (today - timedelta(days=i)).isoformat()})
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": completions
|
||||
}
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert 45 <= rate <= 55 # Around 50%
|
||||
|
||||
|
||||
def test_get_completion_rate_specific_days():
|
||||
"""Test completion rate for specific_days habit."""
|
||||
today = datetime.now().date()
|
||||
today_weekday = today.weekday()
|
||||
|
||||
# Create habit for Mon, Wed, Fri
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [0, 2, 4]
|
||||
},
|
||||
"completions": []
|
||||
}
|
||||
|
||||
# Add completions for all relevant days in last 30 days
|
||||
for i in range(30):
|
||||
check_date = today - timedelta(days=i)
|
||||
if check_date.weekday() in [0, 2, 4]:
|
||||
habit["completions"].append({"date": check_date.isoformat()})
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert rate == 100.0
|
||||
|
||||
|
||||
def test_get_completion_rate_empty():
|
||||
"""Test completion rate with no completions."""
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": []
|
||||
}
|
||||
|
||||
rate = get_completion_rate(habit, days=30)
|
||||
assert rate == 0.0
|
||||
|
||||
|
||||
def test_get_weekly_summary():
|
||||
"""Test weekly summary returns correct structure."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat()},
|
||||
{"date": (today - timedelta(days=1)).isoformat()},
|
||||
]
|
||||
}
|
||||
|
||||
summary = get_weekly_summary(habit)
|
||||
|
||||
# Check structure
|
||||
assert isinstance(summary, dict)
|
||||
assert "Monday" in summary
|
||||
assert "Tuesday" in summary
|
||||
assert "Wednesday" in summary
|
||||
assert "Thursday" in summary
|
||||
assert "Friday" in summary
|
||||
assert "Saturday" in summary
|
||||
assert "Sunday" in summary
|
||||
|
||||
# Check values are valid
|
||||
valid_statuses = ["checked", "skipped", "missed", "upcoming", "not_relevant"]
|
||||
for day, status in summary.items():
|
||||
assert status in valid_statuses
|
||||
|
||||
|
||||
def test_get_weekly_summary_with_skip():
|
||||
"""Test weekly summary handles skipped days."""
|
||||
today = datetime.now().date()
|
||||
|
||||
habit = {
|
||||
"frequency": {"type": "daily"},
|
||||
"completions": [
|
||||
{"date": today.isoformat(), "type": "check"},
|
||||
{"date": (today - timedelta(days=1)).isoformat(), "type": "skip"},
|
||||
]
|
||||
}
|
||||
|
||||
summary = get_weekly_summary(habit)
|
||||
|
||||
# Find today's day name
|
||||
day_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
today_name = day_names[today.weekday()]
|
||||
yesterday_name = day_names[(today.weekday() - 1) % 7]
|
||||
|
||||
assert summary[today_name] == "checked"
|
||||
assert summary[yesterday_name] == "skipped"
|
||||
|
||||
|
||||
def test_get_weekly_summary_specific_days():
|
||||
"""Test weekly summary marks non-relevant days correctly."""
|
||||
today = datetime.now().date()
|
||||
today_weekday = today.weekday()
|
||||
|
||||
# Habit only for Monday (0)
|
||||
habit = {
|
||||
"frequency": {
|
||||
"type": "specific_days",
|
||||
"days": [0]
|
||||
},
|
||||
"completions": []
|
||||
}
|
||||
|
||||
summary = get_weekly_summary(habit)
|
||||
|
||||
# All days except Monday should be not_relevant or upcoming
|
||||
for day_name, status in summary.items():
|
||||
if day_name == "Monday":
|
||||
continue # Monday can be any status
|
||||
if status not in ["upcoming", "not_relevant"]:
|
||||
# Day should be not_relevant if it's in the past
|
||||
pass
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_awards_life_with_checkin():
|
||||
"""Test that +1 life is awarded if there was ≥1 check-in in previous week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add check-in in previous week (Wednesday)
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_no_award_without_checkin():
|
||||
"""Test that no life is awarded if there were no check-ins in previous week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
|
||||
# Add check-in in current week only
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (current_week_start + timedelta(days=1)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == False
|
||||
assert new_lives == 2
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_no_duplicate_award():
|
||||
"""Test that life is not awarded twice in the same week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add check-in in previous week and mark as already awarded this week
|
||||
habit = {
|
||||
"lives": 3,
|
||||
"lastLivesAward": current_week_start.isoformat(),
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == False
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_skip_doesnt_count():
|
||||
"""Test that skips don't count toward weekly recovery."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add only skips in previous week, no check-ins
|
||||
habit = {
|
||||
"lives": 1,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "skip"},
|
||||
{"date": (previous_week_start + timedelta(days=4)).isoformat(), "type": "skip"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == False
|
||||
assert new_lives == 1
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_multiple_checkins():
|
||||
"""Test that award works with multiple check-ins in previous week."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Add multiple check-ins in previous week
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=1)).isoformat(), "type": "check"},
|
||||
{"date": (previous_week_start + timedelta(days=3)).isoformat(), "type": "check"},
|
||||
{"date": (previous_week_start + timedelta(days=5)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_no_cap():
|
||||
"""Test that lives can accumulate beyond 3."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Habit with 5 lives
|
||||
habit = {
|
||||
"lives": 5,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 6
|
||||
|
||||
|
||||
def test_check_and_award_weekly_lives_missing_last_award_field():
|
||||
"""Test backward compatibility when lastLivesAward field is missing."""
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Habit without lastLivesAward field (backward compatible)
|
||||
habit = {
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"}
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True
|
||||
assert new_lives == 3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run all tests
|
||||
import inspect
|
||||
|
||||
test_functions = [
|
||||
obj for name, obj in inspect.getmembers(sys.modules[__name__])
|
||||
if inspect.isfunction(obj) and name.startswith("test_")
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test_func in test_functions:
|
||||
try:
|
||||
test_func()
|
||||
print(f"✓ {test_func.__name__}")
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"✗ {test_func.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ {test_func.__name__}: {type(e).__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
555
dashboard/tests/test_habits_integration.py
Normal file
555
dashboard/tests/test_habits_integration.py
Normal file
@@ -0,0 +1,555 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests for Habits feature - End-to-end flows
|
||||
|
||||
Tests complete workflows involving multiple API calls and state transitions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
from http.server import HTTPServer
|
||||
from threading import Thread
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Add parent directory to path to import api module
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from api import TaskBoardHandler
|
||||
import habits_helpers
|
||||
|
||||
|
||||
# Test helpers
|
||||
def setup_test_env():
|
||||
"""Create temporary environment for testing"""
|
||||
from pathlib import Path
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
habits_file = Path(temp_dir) / 'habits.json'
|
||||
|
||||
# Initialize empty habits file
|
||||
with open(habits_file, 'w') as f:
|
||||
json.dump({'lastUpdated': datetime.now().isoformat(), 'habits': []}, f)
|
||||
|
||||
# Override HABITS_FILE constant
|
||||
import api
|
||||
api.HABITS_FILE = habits_file
|
||||
|
||||
return temp_dir
|
||||
|
||||
|
||||
def teardown_test_env(temp_dir):
|
||||
"""Clean up temporary environment"""
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def start_test_server():
|
||||
"""Start HTTP server on random port for testing"""
|
||||
server = HTTPServer(('localhost', 0), TaskBoardHandler)
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
return server
|
||||
|
||||
|
||||
def http_request(url, method='GET', data=None):
|
||||
"""Make HTTP request and return response data"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
if data:
|
||||
data = json.dumps(data).encode('utf-8')
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
body = response.read().decode('utf-8')
|
||||
return json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode('utf-8')
|
||||
try:
|
||||
return {'error': json.loads(error_body), 'status': e.code}
|
||||
except:
|
||||
return {'error': error_body, 'status': e.code}
|
||||
|
||||
|
||||
# Integration Tests
|
||||
|
||||
def test_01_create_and_checkin_increments_streak():
|
||||
"""Integration test: create habit → check-in → verify streak is 1"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Morning meditation',
|
||||
'category': 'health',
|
||||
'color': '#10B981',
|
||||
'icon': 'brain',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
if 'error' in result:
|
||||
print(f"Error creating habit: {result}")
|
||||
assert 'id' in result, f"Should return created habit with ID, got: {result}"
|
||||
habit_id = result['id']
|
||||
|
||||
# Check in today
|
||||
checkin_result = http_request(f"{base_url}/api/habits/{habit_id}/check", method='POST')
|
||||
|
||||
# Verify streak incremented to 1
|
||||
assert checkin_result['streak']['current'] == 1, "Streak should be 1 after first check-in"
|
||||
assert checkin_result['streak']['best'] == 1, "Best streak should be 1 after first check-in"
|
||||
assert checkin_result['streak']['lastCheckIn'] == datetime.now().date().isoformat(), "Last check-in should be today"
|
||||
|
||||
print("✓ Test 1: Create + check-in → streak is 1")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_02_seven_consecutive_checkins_restore_life():
|
||||
"""Integration test: 7 consecutive check-ins → life restored (if below 3)"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Daily exercise',
|
||||
'category': 'health',
|
||||
'color': '#EF4444',
|
||||
'icon': 'dumbbell',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Manually set lives to 1 (instead of using skip API which would add completions)
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habit_obj = next(h for h in data['habits'] if h['id'] == habit_id)
|
||||
habit_obj['lives'] = 1 # Directly set to 1 (simulating 2 skips used)
|
||||
|
||||
# Add 7 consecutive check-in completions for the past 7 days
|
||||
for i in range(7):
|
||||
check_date = (datetime.now() - timedelta(days=6-i)).date().isoformat()
|
||||
habit_obj['completions'].append({
|
||||
'date': check_date,
|
||||
'type': 'check'
|
||||
})
|
||||
|
||||
# Recalculate streak and check for life restore
|
||||
habit_obj['streak'] = {
|
||||
'current': habits_helpers.calculate_streak(habit_obj),
|
||||
'best': max(habit_obj['streak']['best'], habits_helpers.calculate_streak(habit_obj)),
|
||||
'lastCheckIn': datetime.now().date().isoformat()
|
||||
}
|
||||
|
||||
# Check life restore logic: last 7 completions all 'check' type
|
||||
last_7 = habit_obj['completions'][-7:]
|
||||
if len(last_7) == 7 and all(c.get('type') == 'check' for c in last_7):
|
||||
if habit_obj['lives'] < 3:
|
||||
habit_obj['lives'] += 1
|
||||
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(api.HABITS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Get updated habit
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
|
||||
# Verify life restored
|
||||
assert habit['lives'] == 2, f"Should have 2 lives after 7 consecutive check-ins (was {habit['lives']})"
|
||||
assert habit['current_streak'] == 7, "Should have streak of 7"
|
||||
|
||||
print("✓ Test 2: 7 consecutive check-ins → life restored")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_03_skip_with_life_maintains_streak():
|
||||
"""Integration test: skip with life → lives decremented, streak unchanged"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Read book',
|
||||
'category': 'growth',
|
||||
'color': '#3B82F6',
|
||||
'icon': 'book',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Check in yesterday (to build a streak)
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habit_obj = next(h for h in data['habits'] if h['id'] == habit_id)
|
||||
yesterday = (datetime.now() - timedelta(days=1)).date().isoformat()
|
||||
habit_obj['completions'].append({
|
||||
'date': yesterday,
|
||||
'type': 'check'
|
||||
})
|
||||
habit_obj['streak'] = {
|
||||
'current': 1,
|
||||
'best': 1,
|
||||
'lastCheckIn': yesterday
|
||||
}
|
||||
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(api.HABITS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Skip today
|
||||
skip_result = http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
|
||||
# Verify lives decremented and streak maintained
|
||||
assert skip_result['lives'] == 2, "Lives should be 2 after skip"
|
||||
|
||||
# Get fresh habit data to check streak
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
|
||||
# Streak should still be 1 (skip doesn't break it)
|
||||
assert habit['current_streak'] == 1, "Streak should be maintained after skip"
|
||||
|
||||
print("✓ Test 3: Skip with life → lives decremented, streak unchanged")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_04_skip_with_zero_lives_returns_400():
|
||||
"""Integration test: skip with 0 lives → returns 400 error"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Yoga practice',
|
||||
'category': 'health',
|
||||
'color': '#8B5CF6',
|
||||
'icon': 'heart',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Use all 3 lives
|
||||
http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
|
||||
# Attempt to skip with 0 lives
|
||||
result = http_request(f"{base_url}/api/habits/{habit_id}/skip", method='POST')
|
||||
|
||||
# Verify 400 error
|
||||
assert result['status'] == 400, "Should return 400 status"
|
||||
assert 'error' in result, "Should return error message"
|
||||
|
||||
print("✓ Test 4: Skip with 0 lives → returns 400 error")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_05_edit_frequency_changes_should_check_today():
|
||||
"""Integration test: edit frequency → should_check_today logic changes"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create daily habit
|
||||
habit_data = {
|
||||
'name': 'Code review',
|
||||
'category': 'work',
|
||||
'color': '#F59E0B',
|
||||
'icon': 'code',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Verify should_check_today is True for daily habit
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
assert habit['should_check_today'] == True, "Daily habit should be checkable today"
|
||||
|
||||
# Edit to specific_days (only Monday and Wednesday)
|
||||
update_data = {
|
||||
'name': 'Code review',
|
||||
'category': 'work',
|
||||
'color': '#F59E0B',
|
||||
'icon': 'code',
|
||||
'priority': 50,
|
||||
'frequency': {
|
||||
'type': 'specific_days',
|
||||
'days': ['monday', 'wednesday']
|
||||
}
|
||||
}
|
||||
|
||||
http_request(f"{base_url}/api/habits/{habit_id}", method='PUT', data=update_data)
|
||||
|
||||
# Get updated habit
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = next(h for h in habits if h['id'] == habit_id)
|
||||
|
||||
# Verify should_check_today reflects new frequency
|
||||
today_name = datetime.now().strftime('%A').lower()
|
||||
expected = today_name in ['monday', 'wednesday']
|
||||
assert habit['should_check_today'] == expected, f"Should check today should be {expected} for {today_name}"
|
||||
|
||||
print(f"✓ Test 5: Edit frequency → should_check_today is {expected} for {today_name}")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_06_delete_removes_habit_from_storage():
|
||||
"""Integration test: delete → habit removed from storage"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create habit
|
||||
habit_data = {
|
||||
'name': 'Guitar practice',
|
||||
'category': 'personal',
|
||||
'color': '#EC4899',
|
||||
'icon': 'music',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Verify habit exists
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
assert len(habits) == 1, "Should have 1 habit"
|
||||
assert habits[0]['id'] == habit_id, "Should be the created habit"
|
||||
|
||||
# Delete habit
|
||||
http_request(f"{base_url}/api/habits/{habit_id}", method='DELETE')
|
||||
|
||||
# Verify habit removed
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
assert len(habits) == 0, "Should have 0 habits after delete"
|
||||
|
||||
# Verify not in storage file
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert len(data['habits']) == 0, "Storage file should have 0 habits"
|
||||
|
||||
print("✓ Test 6: Delete → habit removed from storage")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_07_checkin_on_wrong_day_for_specific_days_returns_400():
|
||||
"""Integration test: check-in on wrong day for specific_days → returns 400"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Get today's day name
|
||||
today_name = datetime.now().strftime('%A').lower()
|
||||
|
||||
# Create habit for different days (not today)
|
||||
if today_name == 'monday':
|
||||
allowed_days = ['tuesday', 'wednesday']
|
||||
elif today_name == 'tuesday':
|
||||
allowed_days = ['monday', 'wednesday']
|
||||
else:
|
||||
allowed_days = ['monday', 'tuesday']
|
||||
|
||||
habit_data = {
|
||||
'name': 'Gym workout',
|
||||
'category': 'health',
|
||||
'color': '#EF4444',
|
||||
'icon': 'dumbbell',
|
||||
'priority': 50,
|
||||
'frequency': {
|
||||
'type': 'specific_days',
|
||||
'days': allowed_days
|
||||
}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Attempt to check in today (wrong day)
|
||||
result = http_request(f"{base_url}/api/habits/{habit_id}/check", method='POST')
|
||||
|
||||
# Verify 400 error
|
||||
assert result['status'] == 400, "Should return 400 status"
|
||||
assert 'error' in result, "Should return error message"
|
||||
|
||||
print(f"✓ Test 7: Check-in on {today_name} (not in {allowed_days}) → returns 400")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_08_get_response_includes_all_stats():
|
||||
"""Integration test: GET response includes stats (streak, completion_rate, weekly_summary)"""
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
base_url = f"http://localhost:{server.server_port}"
|
||||
|
||||
try:
|
||||
# Create habit with some completions
|
||||
habit_data = {
|
||||
'name': 'Meditation',
|
||||
'category': 'health',
|
||||
'color': '#10B981',
|
||||
'icon': 'brain',
|
||||
'priority': 50,
|
||||
'frequency': {'type': 'daily'}
|
||||
}
|
||||
|
||||
result = http_request(f"{base_url}/api/habits", method='POST', data=habit_data)
|
||||
habit_id = result['id']
|
||||
|
||||
# Add some completions
|
||||
import api
|
||||
with open(api.HABITS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habit_obj = next(h for h in data['habits'] if h['id'] == habit_id)
|
||||
|
||||
# Add completions for last 3 days
|
||||
for i in range(3):
|
||||
check_date = (datetime.now() - timedelta(days=2-i)).date().isoformat()
|
||||
habit_obj['completions'].append({
|
||||
'date': check_date,
|
||||
'type': 'check'
|
||||
})
|
||||
|
||||
habit_obj['streak'] = {
|
||||
'current': 3,
|
||||
'best': 3,
|
||||
'lastCheckIn': datetime.now().date().isoformat()
|
||||
}
|
||||
|
||||
data['lastUpdated'] = datetime.now().isoformat()
|
||||
with open(api.HABITS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
# Get habits
|
||||
habits = http_request(f"{base_url}/api/habits")
|
||||
habit = habits[0]
|
||||
|
||||
# Verify all enriched stats are present
|
||||
assert 'current_streak' in habit, "Should include current_streak"
|
||||
assert 'best_streak' in habit, "Should include best_streak"
|
||||
assert 'completion_rate_30d' in habit, "Should include completion_rate_30d"
|
||||
assert 'weekly_summary' in habit, "Should include weekly_summary"
|
||||
assert 'should_check_today' in habit, "Should include should_check_today"
|
||||
|
||||
# Verify streak values
|
||||
assert habit['current_streak'] == 3, "Current streak should be 3"
|
||||
assert habit['best_streak'] == 3, "Best streak should be 3"
|
||||
|
||||
# Verify weekly_summary structure
|
||||
assert isinstance(habit['weekly_summary'], dict), "Weekly summary should be a dict"
|
||||
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||
for day in days:
|
||||
assert day in habit['weekly_summary'], f"Weekly summary should include {day}"
|
||||
|
||||
print("✓ Test 8: GET response includes all stats (streak, completion_rate, weekly_summary)")
|
||||
|
||||
finally:
|
||||
server.shutdown()
|
||||
teardown_test_env(temp_dir)
|
||||
|
||||
|
||||
def test_09_typecheck_passes():
|
||||
"""Integration test: Typecheck passes"""
|
||||
result = os.system('python3 -m py_compile /home/moltbot/clawd/dashboard/api.py')
|
||||
assert result == 0, "Typecheck should pass for api.py"
|
||||
|
||||
result = os.system('python3 -m py_compile /home/moltbot/clawd/dashboard/habits_helpers.py')
|
||||
assert result == 0, "Typecheck should pass for habits_helpers.py"
|
||||
|
||||
print("✓ Test 9: Typecheck passes")
|
||||
|
||||
|
||||
# Run all tests
|
||||
if __name__ == '__main__':
|
||||
tests = [
|
||||
test_01_create_and_checkin_increments_streak,
|
||||
test_02_seven_consecutive_checkins_restore_life,
|
||||
test_03_skip_with_life_maintains_streak,
|
||||
test_04_skip_with_zero_lives_returns_400,
|
||||
test_05_edit_frequency_changes_should_check_today,
|
||||
test_06_delete_removes_habit_from_storage,
|
||||
test_07_checkin_on_wrong_day_for_specific_days_returns_400,
|
||||
test_08_get_response_includes_all_stats,
|
||||
test_09_typecheck_passes,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
print("Running integration tests...\n")
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
test()
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f"✗ {test.__name__}: {e}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ {test.__name__}: Unexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
failed += 1
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Integration Tests: {passed} passed, {failed} failed")
|
||||
print(f"{'='*50}")
|
||||
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
134
dashboard/tests/test_weekly_lives_integration.py
Normal file
134
dashboard/tests/test_weekly_lives_integration.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration test for weekly lives recovery feature.
|
||||
|
||||
Tests the full flow:
|
||||
1. Habit has check-ins in previous week
|
||||
2. Check-in today triggers weekly lives recovery
|
||||
3. Response includes livesAwarded flag
|
||||
4. Lives count increases
|
||||
5. Duplicate awards are prevented
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from habits_helpers import check_and_award_weekly_lives
|
||||
|
||||
|
||||
def test_integration_weekly_lives_award():
|
||||
"""Test complete weekly lives recovery flow."""
|
||||
print("\n=== Testing Weekly Lives Recovery Integration ===\n")
|
||||
|
||||
today = datetime.now().date()
|
||||
current_week_start = today - timedelta(days=today.weekday())
|
||||
previous_week_start = current_week_start - timedelta(days=7)
|
||||
|
||||
# Scenario 1: New habit with check-ins in previous week
|
||||
print("Scenario 1: First award of the week")
|
||||
habit = {
|
||||
"id": "test-habit-1",
|
||||
"name": "Test Habit",
|
||||
"lives": 2,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"},
|
||||
{"date": (previous_week_start + timedelta(days=4)).isoformat(), "type": "check"},
|
||||
]
|
||||
}
|
||||
|
||||
new_lives, was_awarded = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded == True, "Expected life to be awarded"
|
||||
assert new_lives == 3, f"Expected 3 lives, got {new_lives}"
|
||||
print(f"✓ Lives awarded: {habit['lives']} → {new_lives}")
|
||||
print(f"✓ Award flag: {was_awarded}")
|
||||
|
||||
# Scenario 2: Already awarded this week
|
||||
print("\nScenario 2: Prevent duplicate award")
|
||||
habit['lives'] = new_lives
|
||||
habit['lastLivesAward'] = current_week_start.isoformat()
|
||||
|
||||
new_lives2, was_awarded2 = check_and_award_weekly_lives(habit)
|
||||
|
||||
assert was_awarded2 == False, "Expected no duplicate award"
|
||||
assert new_lives2 == 3, f"Lives should remain at 3, got {new_lives2}"
|
||||
print(f"✓ No duplicate award: lives remain at {new_lives2}")
|
||||
|
||||
# Scenario 3: Only skips in previous week
|
||||
print("\nScenario 3: Skips don't qualify for recovery")
|
||||
habit_with_skips = {
|
||||
"id": "test-habit-2",
|
||||
"name": "Habit with Skips",
|
||||
"lives": 1,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "skip"},
|
||||
{"date": (previous_week_start + timedelta(days=4)).isoformat(), "type": "skip"},
|
||||
]
|
||||
}
|
||||
|
||||
new_lives3, was_awarded3 = check_and_award_weekly_lives(habit_with_skips)
|
||||
|
||||
assert was_awarded3 == False, "Skips shouldn't trigger award"
|
||||
assert new_lives3 == 1, f"Lives should remain at 1, got {new_lives3}"
|
||||
print(f"✓ Skips don't count: lives remain at {new_lives3}")
|
||||
|
||||
# Scenario 4: No cap on lives (can go beyond 3)
|
||||
print("\nScenario 4: Lives can exceed 3")
|
||||
habit_many_lives = {
|
||||
"id": "test-habit-3",
|
||||
"name": "Habit with Many Lives",
|
||||
"lives": 5,
|
||||
"completions": [
|
||||
{"date": (previous_week_start + timedelta(days=2)).isoformat(), "type": "check"},
|
||||
]
|
||||
}
|
||||
|
||||
new_lives4, was_awarded4 = check_and_award_weekly_lives(habit_many_lives)
|
||||
|
||||
assert was_awarded4 == True, "Expected life to be awarded"
|
||||
assert new_lives4 == 6, f"Expected 6 lives, got {new_lives4}"
|
||||
print(f"✓ No cap: lives increased from 5 → {new_lives4}")
|
||||
|
||||
# Scenario 5: No check-ins in previous week
|
||||
print("\nScenario 5: No check-ins = no award")
|
||||
habit_no_checkins = {
|
||||
"id": "test-habit-4",
|
||||
"name": "New Habit",
|
||||
"lives": 2,
|
||||
"completions": []
|
||||
}
|
||||
|
||||
new_lives5, was_awarded5 = check_and_award_weekly_lives(habit_no_checkins)
|
||||
|
||||
assert was_awarded5 == False, "No check-ins = no award"
|
||||
assert new_lives5 == 2, f"Lives should remain at 2, got {new_lives5}"
|
||||
print(f"✓ No previous week check-ins: lives remain at {new_lives5}")
|
||||
|
||||
print("\n=== All Integration Tests Passed! ===\n")
|
||||
|
||||
# Print summary of the feature
|
||||
print("Feature Summary:")
|
||||
print("• +1 life awarded per week if habit had ≥1 check-in in previous week")
|
||||
print("• Monday-Sunday week boundaries (ISO 8601)")
|
||||
print("• Award triggers on first check-in of current week")
|
||||
print("• Skips don't count toward recovery")
|
||||
print("• No cap on lives (can accumulate beyond 3)")
|
||||
print("• Prevents duplicate awards in same week")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_integration_weekly_lives_award()
|
||||
sys.exit(0)
|
||||
except AssertionError as e:
|
||||
print(f"\n✗ Test failed: {e}\n")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n✗ Unexpected error: {type(e).__name__}: {e}\n")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user