{"record":{"id":"96cddb7e9e3b23bb","repo":"666ghj/MiroFish","slug":"clock-must-return-a-utc-datetime","errorCode":null,"errorMessage":"clock must return a UTC datetime","messagePattern":"clock must return a UTC datetime","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":334,"sourceCode":"\ndef _parse_state_timestamp(value: Any, label: str) -> datetime:\n    if not isinstance(value, str) or not STATE_TIMESTAMP_RE.fullmatch(value):\n        raise StarHistoryError(f\"{label} must use YYYY-MM-DDTHH:MM:SSZ\")\n    try:\n        parsed = datetime.strptime(value, \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=UTC)\n    except ValueError as exc:\n        raise StarHistoryError(f\"{label} is not a valid UTC timestamp\") from exc\n    return parsed\n\n\ndef _format_state_timestamp(value: datetime) -> str:\n    normalized = _normalize_now(value)\n    return normalized.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\n\ndef _normalize_now(value: datetime) -> datetime:\n    if value.tzinfo is None or value.utcoffset() != timedelta(0):\n        raise StarHistoryError(\"clock must return a UTC datetime\")\n    return value.astimezone(UTC).replace(microsecond=0)\n\n\ndef _expect_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:\n    actual = set(value)\n    if actual != expected:\n        raise StarHistoryError(f\"{label} contains missing or unknown fields\")\n\n\ndef validate_state(state: Any) -> None:\n    if not isinstance(state, dict):\n        raise StarHistoryError(\"history state must be a JSON object\")\n    _expect_keys(\n        state,\n        {\n            \"schema_version\",\n            \"repository\",\n            \"timezone\",","sourceCodeStart":316,"sourceCodeEnd":352,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L316-L352","documentation":"_normalize_now (also used by _format_state_timestamp) received a datetime from the injected clock that was naive (tzinfo None) or had a non-zero UTC offset, and raised StarHistoryError('clock must return a UTC datetime'). Every timestamp the script writes into state goes through this, so the clock callable must supply tz-aware UTC.","triggerScenarios":"A clock function returning datetime.utcnow() (naive, deprecated but common), datetime.now() without tz, or datetime.now(ZoneInfo('Europe/Berlin')).","commonSituations":"Calling the script's internals from tests with datetime.now() (naive local time) as the clock; overriding the clock with datetime.now(timezone(timedelta(hours=2))); refactors that changed the clock's return type.","solutions":["Use a UTC-aware clock: datetime.now(timezone.utc) or datetime.now(UTC)","In tests, inject a fixed UTC datetime like datetime(2024, 1, 15, tzinfo=timezone.utc)","Audit every place the clock is constructed or injected and centralize it"],"exampleFix":"# before\nclock = lambda: datetime.now()\n\n# after\nfrom datetime import datetime, timezone\nclock = lambda: datetime.now(timezone.utc)","handlingStrategy":"type-guard","validationCode":"from datetime import datetime, timezone\n\ndef clock() -> datetime:\n    now = datetime.now(timezone.utc)\n    assert now.tzinfo is timezone.utc and now.utcoffset() == timezone.utc.utcoffset(None)\n    return now","typeGuard":"from datetime import datetime, timedelta, timezone\n\ndef is_utc_datetime(value: object) -> TypeGuard[datetime]:\n    return (\n        isinstance(value, datetime)\n        and value.tzinfo is not None\n        and value.utcoffset() == timedelta(0)\n    )","tryCatchPattern":"try:\n    stamp = _format_state_timestamp(clock())\nexcept StarHistoryError as exc:\n    if \"clock must return a UTC datetime\" in str(exc):\n        raise RuntimeError(\"fix the injected clock: use datetime.now(timezone.utc)\") from exc\n    raise","preventionTips":["Standardize on datetime.now(timezone.utc); avoid naive utcnow()","Inject one shared clock in tests and assert it is tz-aware UTC before use"],"tags":["datetime","utc","clock","validation","star-history"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}