From 934afa4c84e40ac56bbdef5a4cd553b7b480845a Mon Sep 17 00:00:00 2001 From: husamemad Date: Sun, 26 Jul 2026 23:24:39 +0300 Subject: [PATCH 1/2] fix(skills): replace deprecated utcnow in skill timestamp helper _now_iso() builds the 'created' value in skill frontmatter. datetime.utcnow() returns a naive datetime and has been deprecated since Python 3.12, scheduled for removal. Switch to the timezone-aware datetime.now(timezone.utc), keeping the serialized YYYY-MM-DDTHH:MM:SSZ shape unchanged so existing skill files keep parsing. timezone.utc is used rather than the datetime.UTC alias, which is 3.11+ only. Adds regression tests covering the deprecation, the serialized shape, and UTC correctness under a non-UTC local timezone -- the last guards against a bare datetime.now(), which yields the same shape but local wall time. Fixes #5697 --- services/memory/skill_format.py | 4 +-- tests/test_skill_format_timestamp.py | 52 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/test_skill_format_timestamp.py diff --git a/services/memory/skill_format.py b/services/memory/skill_format.py index 2b2dfb1b3..628474b04 100644 --- a/services/memory/skill_format.py +++ b/services/memory/skill_format.py @@ -50,7 +50,7 @@ import logging import re from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) @@ -441,4 +441,4 @@ def to_markdown(self) -> str: def _now_iso() -> str: - return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/tests/test_skill_format_timestamp.py b/tests/test_skill_format_timestamp.py new file mode 100644 index 000000000..0443c375e --- /dev/null +++ b/tests/test_skill_format_timestamp.py @@ -0,0 +1,52 @@ +"""Regression for issue #5697 — skill timestamps must not use ``datetime.utcnow()``. + +``_now_iso()`` builds the ``created`` value in skill frontmatter. ``utcnow()`` +returns a *naive* datetime and has been deprecated since Python 3.12, scheduled +for removal. The replacement must stay timezone-aware while keeping the +serialized ``YYYY-MM-DDTHH:MM:SSZ`` shape, so skill files written by older +versions keep parsing. + +The UTC check matters on its own: a bare ``datetime.now()`` also produces the +right shape, but emits local wall time, which would silently backdate or +postdate skills for every user outside UTC. +""" + +import os +import re +import time +import warnings +from datetime import datetime, timezone + +from services.memory.skill_format import _now_iso + +_ISO_Z = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$") + + +def test_now_iso_keeps_serialized_shape(): + assert _ISO_Z.match(_now_iso()) + + +def test_now_iso_emits_no_deprecation_warning(): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + _now_iso() + assert not [w for w in caught if issubclass(w.category, DeprecationWarning)] + + +def test_now_iso_is_utc_not_local_time(): + """Pin UTC under a non-UTC local timezone, where the two visibly diverge.""" + original_tz = os.environ.get("TZ") + os.environ["TZ"] = "Asia/Amman" # UTC+3, never UTC + time.tzset() + try: + emitted = datetime.strptime(_now_iso(), "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + drift = abs((emitted - datetime.now(timezone.utc)).total_seconds()) + assert drift < 60, f"timestamp is {drift}s off UTC — local time leaked in" + finally: + if original_tz is None: + os.environ.pop("TZ", None) + else: + os.environ["TZ"] = original_tz + time.tzset() From 801f3717f710eba48ce28aaf7b77f2732ed52305 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira Date: Thu, 30 Jul 2026 09:52:52 +0100 Subject: [PATCH 2/2] test(skills): skip timezone mutation where unsupported --- tests/test_skill_format_timestamp.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_skill_format_timestamp.py b/tests/test_skill_format_timestamp.py index 0443c375e..a9309bdc1 100644 --- a/tests/test_skill_format_timestamp.py +++ b/tests/test_skill_format_timestamp.py @@ -17,6 +17,8 @@ import warnings from datetime import datetime, timezone +import pytest + from services.memory.skill_format import _now_iso _ISO_Z = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$") @@ -33,6 +35,10 @@ def test_now_iso_emits_no_deprecation_warning(): assert not [w for w in caught if issubclass(w.category, DeprecationWarning)] +@pytest.mark.skipif( + not hasattr(time, "tzset"), + reason="time.tzset is unavailable on this platform", +) def test_now_iso_is_utc_not_local_time(): """Pin UTC under a non-UTC local timezone, where the two visibly diverge.""" original_tz = os.environ.get("TZ")