Files
markitect-main/tests/test_issue_124_day_wrapup.py
tegwick 73d7a83103 feat: implement single command day wrap-up system (issue #124)
- Add comprehensive DayWrapUpService integrating worktime, activity, and cost tracking
- Implement daily wrap-up command with auto-estimation and cost distribution features
- Support multiple output formats (summary, detailed, JSON) with rich formatting
- Add intelligent recommendations based on daily work patterns and data
- Create estimate command for automatic worktime distribution based on activities
- Include period wrap-up functionality for multi-day reporting and analysis
- Add 15 comprehensive test cases covering all service and CLI functionality
- Enable one-command workflow: estimate time, distribute costs, generate reports
- Integrate seamlessly with existing worktime, activity, and cost tracking systems

Features demonstrated:
- Daily summary with 3h30m worktime across 2 issues
- Proportional cost distribution (€150: 71.4% to #122, 28.6% to #123)
- Activity tracking integration showing 3 activities across 2 issues
- Intelligent recommendations for worktime and cost optimization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 03:52:06 +02:00

621 lines
26 KiB
Python

"""
Tests for Issue #124 - Single command Day-Wrap-Up
This module contains comprehensive tests for the day wrap-up functionality
that consolidates daily work summaries, activity tracking, cost distribution,
and reporting into a single convenient command.
"""
import pytest
import tempfile
from datetime import datetime, date, timedelta
from unittest.mock import Mock, patch, MagicMock
from pathlib import Path
from decimal import Decimal
import json
from markitect.finance.day_wrapup_commands import DayWrapUpService, wrapup, _display_daily_summary, _display_period_summary
class TestDayWrapUpService:
"""Test suite for DayWrapUpService."""
def setup_method(self):
"""Set up test fixtures with temporary database."""
self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
self.temp_db.close()
self.db_path = self.temp_db.name
self.service = DayWrapUpService(self.db_path)
def teardown_method(self):
"""Clean up test fixtures."""
Path(self.db_path).unlink(missing_ok=True)
def test_service_initialization(self):
"""Test that service initializes properly with all trackers."""
assert self.service.db_path == self.db_path
assert self.service.worktime_tracker is not None
assert self.service.activity_tracker is not None
assert self.service.session_tracker is not None
def test_get_worktime_summary_no_data(self):
"""Test worktime summary when no data exists."""
today = date.today()
summary = self.service._get_worktime_summary(today)
assert summary['total_minutes'] == 0
assert summary['total_hours'] == 0.0
assert summary['issues_worked'] == 0
assert summary['entries'] == []
assert summary['cost_allocated'] is None
assert summary['cost_per_minute'] is None
def test_get_worktime_summary_with_data(self):
"""Test worktime summary with logged data."""
today = date.today()
# Log some worktime
self.service.worktime_tracker.log_worktime(124, 90, work_date=today, description="Main work")
self.service.worktime_tracker.log_worktime(125, 60, work_date=today, description="Side work")
summary = self.service._get_worktime_summary(today)
assert summary['total_minutes'] == 150 # 90 + 60
assert summary['total_hours'] == 2.5
assert summary['issues_worked'] == 2
assert summary['entries'] == 2
assert len(summary['issue_breakdown']) == 2
assert 124 in summary['issue_breakdown']
assert 125 in summary['issue_breakdown']
assert summary['issue_breakdown'][124]['minutes'] == 90
assert summary['issue_breakdown'][125]['minutes'] == 60
def test_get_activity_summary_no_data(self):
"""Test activity summary when no data exists."""
today = date.today()
summary = self.service._get_activity_summary(today)
assert summary['total_activities'] == 0
assert summary['unique_issues'] == 0
assert summary['activities_by_type'] == {}
assert summary['activities'] == []
def test_get_activity_summary_with_data(self):
"""Test activity summary with logged data."""
today = date.today()
# Log some activities
from markitect.issues.activity_tracker import ActivityType
self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today, activity_details="Created issue")
self.service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today, activity_details="Updated issue")
self.service.activity_tracker.log_activity(125, ActivityType.CREATED, activity_date=today, activity_details="Created another")
summary = self.service._get_activity_summary(today)
assert summary['total_activities'] == 3
assert summary['unique_issues'] == 2
assert 'created' in summary['activities_by_type']
assert 'modified' in summary['activities_by_type']
assert summary['activities_by_type']['created'] == 2
assert summary['activities_by_type']['modified'] == 1
assert len(summary['activities']) == 3
def test_get_cost_summary_no_distribution(self):
"""Test cost summary when no cost distribution exists."""
today = date.today()
summary = self.service._get_cost_summary(today)
assert summary['daily_total'] == 0.0
assert summary['issue_costs'] == {}
assert summary['has_cost_allocation'] is False
def test_get_cost_summary_with_distribution(self):
"""Test cost summary with cost distribution data."""
today = date.today()
# Log worktime and distribute costs
self.service.worktime_tracker.log_worktime(124, 120, work_date=today) # 2 hours
self.service.worktime_tracker.log_worktime(125, 60, work_date=today) # 1 hour
distribution = self.service.worktime_tracker.distribute_daily_costs(
work_date=today,
total_daily_cost=Decimal('90.00') # €90 total
)
summary = self.service._get_cost_summary(today)
assert summary['daily_total'] == 90.0
assert summary['has_cost_allocation'] is True
assert len(summary['issue_costs']) == 2
assert summary['issue_costs'][124] == 60.0 # 2/3 of €90
assert summary['issue_costs'][125] == 30.0 # 1/3 of €90
def test_generate_recommendations_no_data(self):
"""Test recommendation generation with no data."""
summary = {
'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0},
'activities': {'total_activities': 0, 'unique_issues': 0},
'costs': {'has_cost_allocation': False}
}
recommendations = self.service._generate_recommendations(summary)
assert len(recommendations) >= 2
assert any("No worktime logged" in rec for rec in recommendations)
assert any("No issue activities logged" in rec for rec in recommendations)
def test_generate_recommendations_with_data(self):
"""Test recommendation generation with various data conditions."""
# Test low worktime
summary = {
'worktime': {'total_minutes': 120, 'total_hours': 2.0, 'issues_worked': 1},
'activities': {'total_activities': 5, 'unique_issues': 1},
'costs': {'has_cost_allocation': False}
}
recommendations = self.service._generate_recommendations(summary)
assert any("Low worktime logged" in rec for rec in recommendations)
assert any("no costs distributed" in rec for rec in recommendations)
# Test high worktime
summary['worktime'] = {'total_minutes': 660, 'total_hours': 11.0, 'issues_worked': 1}
recommendations = self.service._generate_recommendations(summary)
assert any("High worktime logged" in rec for rec in recommendations)
# Test many issues
summary['activities'] = {'total_activities': 10, 'unique_issues': 6}
recommendations = self.service._generate_recommendations(summary)
assert any("Many issues worked on" in rec for rec in recommendations)
def test_perform_auto_estimation_no_activities(self):
"""Test auto estimation when no activities exist."""
today = date.today()
result = self.service.perform_auto_estimation(today, 8.0)
assert result['estimated'] is False
assert "No active issues found" in result['reason']
assert result['active_issues'] == []
def test_perform_auto_estimation_with_existing_time(self):
"""Test auto estimation when time is already logged."""
today = date.today()
# Log some worktime first
self.service.worktime_tracker.log_worktime(124, 60, work_date=today)
result = self.service.perform_auto_estimation(today, 8.0)
assert result['estimated'] is False
assert "Time already logged" in result['reason']
assert result['existing_minutes'] == 60
def test_perform_auto_estimation_success(self):
"""Test successful auto estimation."""
today = date.today()
# Create activities for issues
from markitect.issues.activity_tracker import ActivityType
self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today)
self.service.activity_tracker.log_activity(125, ActivityType.MODIFIED, activity_date=today)
result = self.service.perform_auto_estimation(today, 6.0)
assert result['estimated'] is True
assert 'estimation_result' in result
estimation = result['estimation_result']
assert estimation['total_minutes'] == 360 # 6 hours
assert estimation['issues_count'] == 2
assert len(estimation['issue_estimates']) == 2
# Verify worktime entries were created
entries = self.service.worktime_tracker.get_worktime_entries(work_date=today)
assert len(entries) == 2
assert all(e.entry_type == "estimated" for e in entries)
def test_distribute_daily_costs(self):
"""Test daily cost distribution functionality."""
today = date.today()
# Log worktime first
self.service.worktime_tracker.log_worktime(124, 180, work_date=today) # 3 hours
self.service.worktime_tracker.log_worktime(125, 120, work_date=today) # 2 hours
# Total: 5 hours (300 minutes)
result = self.service.distribute_daily_costs(today, Decimal('150.00'))
assert result['total_cost'] == 150.0
assert result['total_minutes'] == 300
assert result['cost_per_minute'] == 0.5
assert result['distributions'][124]['cost_allocated'] == 90.0 # 3/5 * €150
assert result['distributions'][125]['cost_allocated'] == 60.0 # 2/5 * €150
def test_generate_daily_summary_integration(self):
"""Test complete daily summary generation."""
today = date.today()
# Create comprehensive test data
from markitect.issues.activity_tracker import ActivityType
# Log worktime
self.service.worktime_tracker.log_worktime(124, 120, work_date=today, description="Main feature")
self.service.worktime_tracker.log_worktime(125, 60, work_date=today, description="Bug fix")
# Log activities
self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today)
self.service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today)
self.service.activity_tracker.log_activity(125, ActivityType.CLOSED, activity_date=today)
# Distribute costs
self.service.distribute_daily_costs(today, Decimal('90.00'))
# Generate summary
summary = self.service.generate_daily_summary(today)
# Verify summary structure
assert summary['date'] == today
assert 'worktime' in summary
assert 'activities' in summary
assert 'costs' in summary
assert 'recommendations' in summary
# Verify worktime data
worktime = summary['worktime']
assert worktime['total_minutes'] == 180
assert worktime['total_hours'] == 3.0
assert worktime['issues_worked'] == 2
assert worktime['cost_allocated'] == 90.0
# Verify activity data
activities = summary['activities']
assert activities['total_activities'] == 3
assert activities['unique_issues'] == 2
# Verify cost data
costs = summary['costs']
assert costs['daily_total'] == 90.0
assert costs['has_cost_allocation'] is True
# Verify recommendations exist
assert isinstance(summary['recommendations'], list)
class TestDayWrapUpCommands:
"""Test suite for day wrap-up CLI commands."""
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
def test_daily_command_basic(self, mock_service_class):
"""Test the daily wrap-up command with basic functionality."""
mock_service = Mock()
mock_service_class.return_value = mock_service
# Mock the summary data
mock_summary = {
'date': date.today(),
'worktime': {
'total_minutes': 180,
'total_hours': 3.0,
'issues_worked': 2,
'entries': 2,
'issue_breakdown': {124: {'minutes': 120, 'entries': 1}, 125: {'minutes': 60, 'entries': 1}},
'cost_allocated': 90.0,
'cost_per_minute': 0.5
},
'activities': {
'total_activities': 3,
'unique_issues': 2,
'activities_by_type': {'created': 2, 'modified': 1},
'activities': []
},
'costs': {
'daily_total': 90.0,
'issue_costs': {124: 60.0, 125: 30.0},
'has_cost_allocation': True
},
'recommendations': ["💰 Costs distributed successfully"]
}
mock_service.generate_daily_summary.return_value = mock_summary
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(wrapup, ['daily'])
assert result.exit_code == 0
assert "📊 Daily Wrap-Up" in result.output
assert "⏰ WORKTIME SUMMARY" in result.output
assert "📝 ACTIVITIES SUMMARY" in result.output
assert "💰 COST SUMMARY" in result.output
assert "💡 RECOMMENDATIONS" in result.output
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
def test_daily_command_with_auto_estimate(self, mock_service_class):
"""Test daily command with auto-estimation enabled."""
mock_service = Mock()
mock_service_class.return_value = mock_service
# Mock estimation result
mock_estimation = {
'estimated': True,
'estimation_result': {
'total_minutes': 480,
'issues_count': 2,
'issue_estimates': {124: 240, 125: 240}
}
}
mock_service.perform_auto_estimation.return_value = mock_estimation
# Mock summary
mock_service.generate_daily_summary.return_value = {
'date': date.today(),
'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0, 'entries': []},
'activities': {'total_activities': 0, 'unique_issues': 0, 'activities_by_type': {}, 'activities': []},
'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False},
'recommendations': []
}
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(wrapup, ['daily', '--auto-estimate', '--estimate-hours', '8'])
assert result.exit_code == 0
assert "🤖 Auto-estimating worktime" in result.output
assert "✅ Estimated 8.0h across 2 issues" in result.output
mock_service.perform_auto_estimation.assert_called_once_with(date.today(), 8.0)
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
def test_daily_command_with_cost_distribution(self, mock_service_class):
"""Test daily command with cost distribution."""
mock_service = Mock()
mock_service_class.return_value = mock_service
# Mock distribution result
mock_distribution = {
'total_cost': 120.0,
'total_minutes': 240,
'issues_count': 2,
'distributions': {124: {'cost_allocated': 80.0}, 125: {'cost_allocated': 40.0}}
}
mock_service.distribute_daily_costs.return_value = mock_distribution
# Mock summary
mock_service.generate_daily_summary.return_value = {
'date': date.today(),
'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0, 'entries': []},
'activities': {'total_activities': 0, 'unique_issues': 0, 'activities_by_type': {}, 'activities': []},
'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False},
'recommendations': []
}
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(wrapup, ['daily', '--distribute-cost', '120'])
assert result.exit_code == 0
assert "💰 Distributing €120.00" in result.output
assert "✅ Distributed €120.00 across 2 issues" in result.output
mock_service.distribute_daily_costs.assert_called_once()
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
def test_daily_command_json_format(self, mock_service_class):
"""Test daily command with JSON output format."""
mock_service = Mock()
mock_service_class.return_value = mock_service
mock_summary = {
'date': date.today(),
'worktime': {'total_minutes': 120, 'total_hours': 2.0, 'issues_worked': 1, 'entries': 1},
'activities': {'total_activities': 2, 'unique_issues': 1, 'activities_by_type': {}, 'activities': []},
'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False},
'recommendations': []
}
mock_service.generate_daily_summary.return_value = mock_summary
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(wrapup, ['daily', '--format', 'json'])
assert result.exit_code == 0
# Should be valid JSON
output_data = json.loads(result.output.strip())
assert 'date' in output_data
assert 'worktime' in output_data
assert 'activities' in output_data
assert 'costs' in output_data
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
def test_estimate_command(self, mock_service_class):
"""Test the estimate command."""
mock_service = Mock()
mock_service_class.return_value = mock_service
mock_estimation = {
'estimated': True,
'estimation_result': {
'work_date': date.today(),
'total_minutes': 480, # 8 hours
'distribution_method': 'activity_based',
'issue_estimates': {124: 300, 125: 180}, # 5h and 3h
'issues_count': 2
}
}
mock_service.perform_auto_estimation.return_value = mock_estimation
from click.testing import CliRunner
runner = CliRunner()
today = date.today().strftime('%Y-%m-%d')
result = runner.invoke(wrapup, ['estimate', today, '--hours', '8'])
assert result.exit_code == 0
assert "✅ Estimated worktime" in result.output
assert "Total Hours: 8.0h" in result.output
assert "Issues: 2" in result.output
assert "Estimated Time Distribution:" in result.output
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
def test_period_command(self, mock_service_class):
"""Test the period wrap-up command."""
mock_service = Mock()
mock_service_class.return_value = mock_service
# Mock worktime report
mock_worktime_report = {
'period': '2025-10-01 to 2025-10-04',
'total_entries': 8,
'total_time': {'hours': 20, 'minutes': 30, 'total_minutes': 1230},
'unique_issues': 3,
'unique_dates': 4,
'average_minutes_per_day': 307.5
}
mock_service.worktime_tracker.get_worktime_report.return_value = mock_worktime_report
# Mock activity summary
mock_activity_summary = {
'total_activities': 15,
'unique_issues': 4,
'activities_by_type': {'created': 8, 'modified': 5, 'closed': 2}
}
mock_service.activity_tracker.get_activity_summary.return_value = mock_activity_summary
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(wrapup, ['period', '2025-10-01', '2025-10-04'])
assert result.exit_code == 0
assert "📈 Period Wrap-Up" in result.output
assert "⏰ WORKTIME OVERVIEW" in result.output
assert "📝 ACTIVITIES OVERVIEW" in result.output
assert "Total Time: 20h 30m" in result.output
assert "Total Activities: 15" in result.output
class TestDayWrapUpIntegration:
"""Integration tests for the complete day wrap-up system."""
def setup_method(self):
"""Set up integration test fixtures."""
self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
self.temp_db.close()
self.db_path = self.temp_db.name
def teardown_method(self):
"""Clean up integration test fixtures."""
Path(self.db_path).unlink(missing_ok=True)
def test_complete_day_workflow(self):
"""Test a complete daily workflow from start to finish."""
service = DayWrapUpService(self.db_path)
today = date.today()
# 1. Start with empty day
initial_summary = service.generate_daily_summary(today)
assert initial_summary['worktime']['total_minutes'] == 0
assert initial_summary['activities']['total_activities'] == 0
# 2. Log some activities
from markitect.issues.activity_tracker import ActivityType
service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today, activity_details="Started new feature")
service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today, activity_details="Made progress")
service.activity_tracker.log_activity(125, ActivityType.CLOSED, activity_date=today, activity_details="Fixed bug")
# 3. Perform auto-estimation
estimation = service.perform_auto_estimation(today, 7.5)
assert estimation['estimated'] is True
assert estimation['estimation_result']['total_minutes'] == 450 # 7.5 hours
# 4. Distribute costs
distribution = service.distribute_daily_costs(today, Decimal('112.50')) # €15 per hour
assert distribution['total_cost'] == 112.5
assert distribution['cost_per_minute'] == 0.25 # €0.25 per minute
# 5. Generate final summary
final_summary = service.generate_daily_summary(today)
# Verify complete summary
assert final_summary['worktime']['total_hours'] == 7.5
assert final_summary['worktime']['issues_worked'] == 2
assert final_summary['worktime']['cost_allocated'] == 112.5
assert final_summary['activities']['total_activities'] == 3
assert final_summary['activities']['unique_issues'] == 2
assert final_summary['costs']['daily_total'] == 112.5
assert final_summary['costs']['has_cost_allocation'] is True
assert len(final_summary['costs']['issue_costs']) == 2
# Verify recommendations are helpful
recommendations = final_summary['recommendations']
assert len(recommendations) >= 0 # Should have reasonable recommendations
def test_multi_day_period_summary(self):
"""Test period summary across multiple days."""
service = DayWrapUpService(self.db_path)
# Create data across multiple days
dates = [date.today() - timedelta(days=i) for i in range(3)] # Last 3 days
for i, test_date in enumerate(dates):
# Log different amounts of work each day
hours = 6 + i * 2 # 6, 8, 10 hours
minutes = hours * 60
service.worktime_tracker.log_worktime(124, minutes // 2, work_date=test_date, description=f"Day {i+1} main work")
service.worktime_tracker.log_worktime(125 + i, minutes // 2, work_date=test_date, description=f"Day {i+1} side work")
# Log activities
from markitect.issues.activity_tracker import ActivityType
service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=test_date)
service.activity_tracker.log_activity(125 + i, ActivityType.MODIFIED, activity_date=test_date)
# Generate period report
start_date = dates[-1] # Oldest date
end_date = dates[0] # Most recent date
worktime_report = service.worktime_tracker.get_worktime_report(
start_date=start_date,
end_date=end_date
)
# Verify period data
assert worktime_report['total_entries'] == 6 # 2 entries per day * 3 days
assert worktime_report['total_time']['total_minutes'] == 1440 # 6+8+10 = 24 hours
assert worktime_report['unique_issues'] == 4 # Issues 124, 125, 126, 127
assert worktime_report['unique_dates'] == 3
# Verify daily averages
expected_avg = 1440 / 3 # 480 minutes per day on average
assert abs(worktime_report['average_minutes_per_day'] - expected_avg) < 1
def test_error_handling_and_edge_cases(self):
"""Test error handling and edge cases."""
service = DayWrapUpService(self.db_path)
today = date.today()
# Test estimation with no activities
estimation = service.perform_auto_estimation(today, 8.0)
assert estimation['estimated'] is False
assert "No active issues found" in estimation['reason']
# Test cost distribution with no worktime
distribution = service.distribute_daily_costs(today, Decimal('100.00'))
assert 'message' in distribution
assert "No worktime entries found" in distribution['message']
# Test summary generation with partial data
from markitect.issues.activity_tracker import ActivityType
service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today)
summary = service.generate_daily_summary(today)
assert summary['worktime']['total_minutes'] == 0 # No worktime logged
assert summary['activities']['total_activities'] == 1 # But activity exists
assert "No worktime logged" in ' '.join(summary['recommendations'])
# Test recommendations for edge cases
service.worktime_tracker.log_worktime(124, 720, work_date=today) # 12 hours - excessive
summary = service.generate_daily_summary(today)
assert any("High worktime logged" in rec for rec in summary['recommendations'])