test_stats_reconciled_duration_2592.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """Regression test for reconnect-reconciliation inflating Total Print Time (#2592).
  2. On a farm, a connected-edge reconcile closes out every stale ``status="printing"``
  3. archive as an aborted run whose duration was computed ``completed_at - started_at``
  4. — i.e. the whole multi-day disconnect gap — and the Stats endpoint's fallback
  5. recomputed the same value even when the stored duration was 0/NULL. A single
  6. reconnect could add hundreds of fictitious print hours (reporter @Jostxxl saw
  7. Total Print Time jump from ~1,500h to 3,215h).
  8. The fix stores an explicit 0 for reconciled entries and makes the Stats total
  9. trust that 0 instead of recomputing from the stale timestamps. This test drives
  10. the ``/archives/stats`` endpoint with hand-crafted rows covering the reporter's
  11. scenarios.
  12. """
  13. from __future__ import annotations
  14. from datetime import datetime, timedelta
  15. import pytest
  16. from httpx import AsyncClient
  17. from backend.app.models.print_log import PrintLogEntry
  18. @pytest.mark.asyncio
  19. @pytest.mark.integration
  20. async def test_stats_total_time_ignores_reconciled_but_keeps_real_runtime(async_client: AsyncClient, db_session):
  21. base = datetime(2026, 7, 15, 10, 0, 0)
  22. reconnect = base + timedelta(days=2, hours=4) # multi-day gap → ~52h each if recomputed
  23. rows = [
  24. # Two reconciled aborts for the same printer: duration logged as 0 on
  25. # purpose (unknown real end time). Their timestamps span days, so the
  26. # bug would have banked ~52h each (~104h total) via the fallback.
  27. PrintLogEntry(printer_id=1, status="aborted", started_at=base, completed_at=reconnect, duration_seconds=0),
  28. PrintLogEntry(printer_id=1, status="aborted", started_at=base, completed_at=reconnect, duration_seconds=0),
  29. # A genuine >24h print — must be retained in full (no cap, no zeroing).
  30. PrintLogEntry(
  31. printer_id=1,
  32. status="completed",
  33. started_at=base,
  34. completed_at=base + timedelta(hours=30),
  35. duration_seconds=30 * 3600,
  36. ),
  37. # A legacy row that never stored a duration — must still fall back to
  38. # its own (short, legitimate) 2h span.
  39. PrintLogEntry(
  40. printer_id=1,
  41. status="completed",
  42. started_at=base,
  43. completed_at=base + timedelta(hours=2),
  44. duration_seconds=None,
  45. ),
  46. ]
  47. for r in rows:
  48. db_session.add(r)
  49. await db_session.commit()
  50. resp = await async_client.get("/api/v1/archives/stats")
  51. assert resp.status_code == 200, resp.text
  52. data = resp.json()
  53. # 30h (genuine) + 2h (legacy fallback) + 0 + 0 (reconciled) = 32.0h.
  54. # Pre-fix the two reconciled rows would have added ~104h from their stamps.
  55. assert data["total_print_time_hours"] == 32.0