Files
bambuddy/backend/tests/integration/test_stats_reconciled_duration_2592.py
maziggy 8d618678a2 fix(stats): don't bank the disconnect gap as print time on reconnect (#2592)
reconcile_stale_active_prints closes out stale status="printing" archives by
synthesising an aborted on_print_complete, which logged a PrintLogEntry whose
duration was completed_at - started_at — the whole multi-day disconnect gap,
since a reconciled archive's real end time is unknown. Across a farm of stale
rows this inflated Total Print Time by hundreds of hours, and the Stats total
recomputed the same value from the timestamps even when duration was NULL/0.

Reconciled completions now log duration_seconds=0, the two Stats time paths
trust a stored 0 instead of recomputing, and reconciled aborts get an honest
"Stale - reconciled ..." failure_reason instead of "User cancelled". Genuine
long prints are untouched (no cap; still-running >24h prints aren't stale).
2026-07-18 09:58:21 +02:00

67 lines
2.7 KiB
Python

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