Files
clawd/dashboard/test_habits_check.py

274 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""
Tests for POST /api/habits/{id}/check endpoint
"""
import json
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from http.client import HTTPConnection
# Test against local server
HOST = 'localhost'
PORT = 8088
HABITS_FILE = Path(__file__).parent / 'habits.json'
def cleanup_test_habits():
"""Reset habits.json to empty state for testing."""
data = {
'lastUpdated': datetime.now().isoformat(),
'habits': []
}
HABITS_FILE.write_text(json.dumps(data, indent=2))
def create_test_habit(name='Test Habit', frequency='daily'):
"""Helper to create a test habit and return its ID."""
conn = HTTPConnection(HOST, PORT)
payload = json.dumps({'name': name, 'frequency': frequency})
headers = {'Content-Type': 'application/json'}
conn.request('POST', '/api/habits', payload, headers)
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
return data['id']
def test_check_habit_success():
"""Test successfully checking a habit for today."""
cleanup_test_habits()
habit_id = create_test_habit('Morning Run', 'daily')
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
assert response.status == 200, f"Expected 200, got {response.status}"
assert data['id'] == habit_id, "Habit ID should match"
assert 'completions' in data, "Response should include completions"
assert len(data['completions']) == 1, "Should have exactly 1 completion"
# Check that completion date is today (YYYY-MM-DD format)
today = datetime.now().date().isoformat()
assert data['completions'][0] == today, f"Completion should be today's date: {today}"
# Check that streak is calculated and included
assert 'streak' in data, "Response should include streak"
assert data['streak'] == 1, "Streak should be 1 after first check"
print("✓ test_check_habit_success")
def test_check_habit_already_checked():
"""Test checking a habit that was already checked today."""
cleanup_test_habits()
habit_id = create_test_habit('Reading', 'daily')
# Check it once
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
conn.getresponse().read()
conn.close()
# Try to check again
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
assert response.status == 400, f"Expected 400, got {response.status}"
assert 'error' in data, "Response should include error"
assert 'already checked' in data['error'].lower(), "Error should mention already checked"
print("✓ test_check_habit_already_checked")
def test_check_habit_not_found():
"""Test checking a non-existent habit."""
cleanup_test_habits()
conn = HTTPConnection(HOST, PORT)
conn.request('POST', '/api/habits/nonexistent-id/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
assert response.status == 404, f"Expected 404, got {response.status}"
assert 'error' in data, "Response should include error"
print("✓ test_check_habit_not_found")
def test_check_habit_persistence():
"""Test that completions are persisted to habits.json."""
cleanup_test_habits()
habit_id = create_test_habit('Meditation', 'daily')
# Check the habit
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
conn.getresponse().read()
conn.close()
# Read habits.json directly
habits_data = json.loads(HABITS_FILE.read_text())
habit = next((h for h in habits_data['habits'] if h['id'] == habit_id), None)
assert habit is not None, "Habit should exist in file"
assert len(habit['completions']) == 1, "Should have 1 completion in file"
today = datetime.now().date().isoformat()
assert habit['completions'][0] == today, "Completion date should be today"
print("✓ test_check_habit_persistence")
def test_check_habit_sorted_completions():
"""Test that completions array is sorted chronologically."""
cleanup_test_habits()
# Create a habit and manually add out-of-order completions
habit_id = create_test_habit('Workout', 'daily')
# Manually add past completions in reverse order
habits_data = json.loads(HABITS_FILE.read_text())
habit = next((h for h in habits_data['habits'] if h['id'] == habit_id), None)
today = datetime.now().date()
habit['completions'] = [
(today - timedelta(days=2)).isoformat(), # 2 days ago
(today - timedelta(days=4)).isoformat(), # 4 days ago
(today - timedelta(days=1)).isoformat(), # yesterday
]
HABITS_FILE.write_text(json.dumps(habits_data, indent=2))
# Check today
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
# Verify completions are sorted oldest first
expected_order = [
(today - timedelta(days=4)).isoformat(),
(today - timedelta(days=2)).isoformat(),
(today - timedelta(days=1)).isoformat(),
today.isoformat()
]
assert data['completions'] == expected_order, f"Completions should be sorted. Got: {data['completions']}"
print("✓ test_check_habit_sorted_completions")
def test_check_habit_streak_calculation():
"""Test that streak is calculated correctly after checking."""
cleanup_test_habits()
habit_id = create_test_habit('Journaling', 'daily')
# Add consecutive past completions
today = datetime.now().date()
habits_data = json.loads(HABITS_FILE.read_text())
habit = next((h for h in habits_data['habits'] if h['id'] == habit_id), None)
habit['completions'] = [
(today - timedelta(days=2)).isoformat(),
(today - timedelta(days=1)).isoformat(),
]
HABITS_FILE.write_text(json.dumps(habits_data, indent=2))
# Check today
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
# Streak should be 3 (today + yesterday + day before)
assert data['streak'] == 3, f"Expected streak 3, got {data['streak']}"
print("✓ test_check_habit_streak_calculation")
def test_check_weekly_habit():
"""Test checking a weekly habit."""
cleanup_test_habits()
habit_id = create_test_habit('Team Meeting', 'weekly')
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
assert response.status == 200, f"Expected 200, got {response.status}"
assert len(data['completions']) == 1, "Should have 1 completion"
assert data['streak'] == 1, "Weekly habit should have streak of 1"
print("✓ test_check_weekly_habit")
def test_check_habit_iso_date_format():
"""Test that completion dates use ISO YYYY-MM-DD format (not timestamps)."""
cleanup_test_habits()
habit_id = create_test_habit('Water Plants', 'daily')
conn = HTTPConnection(HOST, PORT)
conn.request('POST', f'/api/habits/{habit_id}/check')
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
completion = data['completions'][0]
# Verify format is YYYY-MM-DD (exactly 10 chars, 2 dashes)
assert len(completion) == 10, f"Date should be 10 chars, got {len(completion)}"
assert completion.count('-') == 2, "Date should have 2 dashes"
assert 'T' not in completion, "Date should not include time (no T)"
# Verify it parses as a valid date
try:
datetime.fromisoformat(completion)
except ValueError:
assert False, f"Completion date should be valid ISO date: {completion}"
print("✓ test_check_habit_iso_date_format")
if __name__ == '__main__':
print("Running tests for POST /api/habits/{id}/check...")
print()
try:
test_check_habit_success()
test_check_habit_already_checked()
test_check_habit_not_found()
test_check_habit_persistence()
test_check_habit_sorted_completions()
test_check_habit_streak_calculation()
test_check_weekly_habit()
test_check_habit_iso_date_format()
print()
print("✅ All tests passed!")
sys.exit(0)
except AssertionError as e:
print()
print(f"❌ Test failed: {e}")
sys.exit(1)
except Exception as e:
print()
print(f"❌ Error running tests: {e}")
import traceback
traceback.print_exc()
sys.exit(1)