From bdd6a5cbe404c42de6f9ed3d956a3a5d932e4e83 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 19 Oct 2026 22:32:18 -0700 Subject: [PATCH] fix(library): an out-of-range last use reads as unknown, not a failed list --- database/library.py | 15 ++++++++++----- tests/test_api_library.py | 32 ++++++++++++++++++++++++++++++++ tests/test_library.py | 10 ++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/database/library.py b/database/library.py index 232e4a1..a54ffd2 100644 --- a/database/library.py +++ b/database/library.py @@ -51,7 +51,12 @@ def utc_iso(value: object) -> str | None: """A stored timestamp as UTC ISO text, or None when absent or unreadable. Usage writes are already UTC ISO; a naive value (SQLite's CURRENT_TIMESTAMP - form) is read as UTC, and an offset is converted to UTC. + form) is read as UTC, and an offset is converted to UTC. One bad row must + not fail a whole Library read, so a value past the datetime range after + conversion (OverflowError) is unreadable too. Python 3.11+ fromisoformat + accepts more ISO forms than 3.10 (e.g. a 2-digit fraction); every form the + app writes (utc_now_iso, the 3.2.4 backfill, CURRENT_TIMESTAMP, Z) parses + the same on both, so only hand-edited values can differ by version. """ if not isinstance(value, str): return None @@ -60,11 +65,11 @@ def utc_iso(value: object) -> str | None: text = text[:-1] + "+00:00" try: stamp = datetime.datetime.fromisoformat(text) - except ValueError: + if stamp.tzinfo is None: + stamp = stamp.replace(tzinfo=datetime.timezone.utc) + return stamp.astimezone(datetime.timezone.utc).isoformat() + except (ValueError, OverflowError): return None - if stamp.tzinfo is None: - return stamp.replace(tzinfo=datetime.timezone.utc).isoformat() - return stamp.astimezone(datetime.timezone.utc).isoformat() _NAME = ( diff --git a/tests/test_api_library.py b/tests/test_api_library.py index e8c3f63..6cc6329 100644 --- a/tests/test_api_library.py +++ b/tests/test_api_library.py @@ -468,6 +468,38 @@ class TestLibraryAPI(ImageAPITestCase): ) return response, await response.json() + async def test_unconvertible_last_use_does_not_fail_list_detail_or_receipt(self): + positive = self._save_prompt("Cat") + other = self._save_prompt("Dog") + with self.api.db.model.get_connection() as connection: + connection.execute( + "UPDATE prompts SET last_used_at=? WHERE id=?", + ("9999-12-31T23:59:59-01:00", positive), + ) + connection.execute( + "UPDATE prompts SET last_used_at=? WHERE id=?", + ("0001-01-01T00:00:00+01:00", other), + ) + response, listed = await self.get() + self.assertEqual(response.status, 200, listed) + self.assertEqual( + [item["last_used_at"] for item in listed["items"]], [None, None] + ) + response, detail = await self.get(f"/{positive}") + self.assertEqual(response.status, 200, detail) + self.assertIsNone(detail["positive"]["last_used_at"]) + response, receipt = await self.patch_parent( + positive, + { + "expected_epoch": detail["library_epoch"], + "if_match": detail["positive"]["edit_token"], + "metadata": {"title": "Portrait"}, + }, + ) + self.assertEqual(response.status, 200, receipt) + self.assertEqual(receipt["positive"]["title"], "Portrait") + self.assertIsNone(receipt["positive"]["last_used_at"]) + async def test_native_parent_patch_strict_validation(self): from database.execution_history_schema import rotate_history_epoch diff --git a/tests/test_library.py b/tests/test_library.py index f204061..96fbf54 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -498,10 +498,15 @@ class TestLibrary(unittest.TestCase): offset = self.save("Offset") broken = self.save("Broken") zulu = self.save("Zulu") + # Parse but cannot convert to UTC: past the datetime range either way. + floor = self.save("Floor") + ceiling = self.save("Ceiling") with self.db.model.get_connection() as connection: for prompt_id, value in ( (legacy, "2026-10-07 18:39:06"), (zulu, "2026-10-07T18:39:06Z"), + (floor, "0001-01-01T00:00:00+01:00"), + (ceiling, "9999-12-31T23:59:59-01:00"), (offset, "2026-10-07T20:39:06.250+02:00"), (broken, "yesterday"), ): @@ -525,6 +530,11 @@ class TestLibrary(unittest.TestCase): ) self.assertIsNone(items[broken]["last_used_at"]) self.assertEqual(items[zulu]["last_used_at"], "2026-10-07T18:39:06+00:00") + self.assertIsNone(items[floor]["last_used_at"]) + self.assertIsNone(items[ceiling]["last_used_at"]) + self.assertIsNone( + self.db.get_library_positive(floor)["positive"]["last_used_at"] + ) self.assertEqual(type(items[broken]["run_count"]), int) detail = self.db.get_library_positive(used)["positive"] self.assertEqual(detail["run_count"], 3)