Add state-transition methods to ActivityRepository for advancing activity stages (mark_downloaded, mark_converted, mark_imported, mark_duplicate, mark_failed) with proper retention of last_completed_stage on failure. Add list_pending_for_user to filter activities for processing. Implement SyncRunRepository for creating and finalizing sync runs with counts and summary errors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
from app.db.models import ActivityStatus, SyncRunStatus
|
|
|
|
|
|
def test_failure_retains_last_completed_stage(activity_repository, seeded_activity) -> None:
|
|
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
|
|
activity_repository.mark_failed(seeded_activity.id, "Garmin timeout", retryable=True)
|
|
activity = activity_repository.get(seeded_activity.id)
|
|
|
|
assert activity.status == ActivityStatus.FAILED
|
|
assert activity.last_completed_stage == ActivityStatus.DOWNLOADED
|
|
assert activity.retryable is True
|
|
|
|
|
|
def test_converted_activity_is_pending_until_terminal(activity_repository, seeded_activity) -> None:
|
|
activity_repository.mark_converted(seeded_activity.id, "/data/activities/1/a/converted.fit")
|
|
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
|
|
assert seeded_activity.id in ids
|
|
|
|
|
|
def test_list_pending_excludes_imported(activity_repository, seeded_activity) -> None:
|
|
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
|
|
activity_repository.mark_converted(seeded_activity.id, "/data/activities/1/a/converted.fit")
|
|
activity_repository.mark_imported(seeded_activity.id, "garmin-123")
|
|
|
|
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
|
|
assert seeded_activity.id not in ids
|
|
|
|
|
|
def test_list_pending_excludes_duplicate(activity_repository, seeded_activity) -> None:
|
|
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
|
|
activity_repository.mark_duplicate(seeded_activity.id)
|
|
|
|
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
|
|
assert seeded_activity.id not in ids
|
|
|
|
|
|
def test_list_pending_excludes_non_retryable_failed(activity_repository, seeded_activity) -> None:
|
|
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
|
|
activity_repository.mark_failed(seeded_activity.id, "Cannot retry", retryable=False)
|
|
|
|
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
|
|
assert seeded_activity.id not in ids
|
|
|
|
|
|
def test_list_pending_includes_retryable_failed(activity_repository, seeded_activity) -> None:
|
|
activity_repository.mark_downloaded(seeded_activity.id, "/data/activities/1/a/source.fit")
|
|
activity_repository.mark_failed(seeded_activity.id, "Temporary error", retryable=True)
|
|
|
|
ids = [item.id for item in activity_repository.list_pending_for_user(seeded_activity.user_id)]
|
|
assert seeded_activity.id in ids
|
|
|
|
|
|
def test_sync_run_start_and_finish(sync_run_repository, user_repository) -> None:
|
|
user = user_repository.create(
|
|
name="Test User",
|
|
enabled=True,
|
|
health_state="healthy",
|
|
mywhoosh_email_enc="test@example.com",
|
|
mywhoosh_password_enc="password",
|
|
garmin_email_enc="test@garmin.com",
|
|
garmin_password_enc="garmin_password",
|
|
)
|
|
|
|
sync_run = sync_run_repository.start(user.id)
|
|
assert sync_run.status == SyncRunStatus.RUNNING
|
|
assert sync_run.discovered_count == 0
|
|
assert sync_run.imported_count == 0
|
|
assert sync_run.skipped_count == 0
|
|
assert sync_run.failed_count == 0
|
|
|
|
finished = sync_run_repository.finish(
|
|
sync_run.id,
|
|
status=SyncRunStatus.SUCCESS,
|
|
discovered=5,
|
|
imported=3,
|
|
skipped=1,
|
|
failed=1,
|
|
)
|
|
|
|
assert finished.status == SyncRunStatus.SUCCESS
|
|
assert finished.discovered_count == 5
|
|
assert finished.imported_count == 3
|
|
assert finished.skipped_count == 1
|
|
assert finished.failed_count == 1
|
|
assert finished.finished_at is not None
|
|
|
|
# Reload from DB to verify persisted
|
|
reloaded = sync_run_repository.get(sync_run.id)
|
|
assert reloaded.status == SyncRunStatus.SUCCESS
|
|
assert reloaded.discovered_count == 5
|
|
assert reloaded.imported_count == 3
|
|
|
|
|
|
def test_sync_run_finish_with_error(sync_run_repository, user_repository) -> None:
|
|
user = user_repository.create(
|
|
name="Test User",
|
|
enabled=True,
|
|
health_state="healthy",
|
|
mywhoosh_email_enc="test@example.com",
|
|
mywhoosh_password_enc="password",
|
|
garmin_email_enc="test@garmin.com",
|
|
garmin_password_enc="garmin_password",
|
|
)
|
|
|
|
sync_run = sync_run_repository.start(user.id)
|
|
error_msg = "Connection timeout"
|
|
|
|
finished = sync_run_repository.finish(
|
|
sync_run.id,
|
|
status=SyncRunStatus.FAILED,
|
|
discovered=0,
|
|
imported=0,
|
|
skipped=0,
|
|
failed=0,
|
|
summary_error=error_msg,
|
|
)
|
|
|
|
assert finished.summary_error == error_msg
|
|
|
|
# Verify truncation works
|
|
long_error = "x" * 5000
|
|
finished_long = sync_run_repository.finish(
|
|
sync_run.id,
|
|
status=SyncRunStatus.FAILED,
|
|
discovered=0,
|
|
imported=0,
|
|
skipped=0,
|
|
failed=0,
|
|
summary_error=long_error,
|
|
)
|
|
assert len(finished_long.summary_error) == 2000
|
|
assert finished_long.summary_error == long_error[:2000]
|