From b649ea7168d2ef17e5359583dbd9303a3380561d Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 21 Aug 2026 18:01:02 +0200 Subject: [PATCH] fix: canonicalize numeric legacy timestamps --- hub_core/runtime/migration.py | 2 ++ tests/test_migration.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/hub_core/runtime/migration.py b/hub_core/runtime/migration.py index cb390b1..6dd3cfa 100644 --- a/hub_core/runtime/migration.py +++ b/hub_core/runtime/migration.py @@ -60,6 +60,8 @@ def _datetime(value: Any, default: datetime | None = None) -> datetime: return value if value.tzinfo else value.replace(tzinfo=timezone.utc) if isinstance(value, str) and value: return datetime.fromisoformat(value.replace("Z", "+00:00")) + if isinstance(value, (int, float)) and not isinstance(value, bool): + return datetime.fromtimestamp(value, tz=timezone.utc) return default or _now() diff --git a/tests/test_migration.py b/tests/test_migration.py index 0694a31..f29dc61 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -52,6 +52,22 @@ def test_validation_rejects_secret_material_and_digest_drift(): assert any("bundleSha256" in error for error in report["errors"]) +def test_numeric_legacy_timestamps_have_deterministic_content_hashes(): + numeric = bundle() + numeric["records"]["interactionEvents"][0]["metadata"]["recordedAt"] = 1 + numeric["bundleSha256"] = bundle_digest(numeric) + textual = bundle() + textual["records"]["interactionEvents"][0]["metadata"]["recordedAt"] = ( + "1970-01-01T00:00:01Z" + ) + textual["bundleSha256"] = bundle_digest(textual) + + assert ( + validate_bundle(numeric)["contentHashes"]["interactionEvents"] + == validate_bundle(textual)["contentHashes"]["interactionEvents"] + ) + + def test_import_is_idempotent_and_exports_all_seven_collections(tmp_path): async def run(): store = await make_store(tmp_path)