fix(skills): replace deprecated utcnow in skill timestamp helper (#5777)

* 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

* test(skills): skip timezone mutation where unsupported

---------

Co-authored-by: Alexandre Teixeira <alexandremagteixeira@gmail.com>
This commit is contained in:
Husam
2026-07-30 11:54:59 +03:00
committed by GitHub
parent 6a84398e75
commit f23221420f
2 changed files with 60 additions and 2 deletions
+2 -2
View File
@@ -50,7 +50,7 @@ import json
import logging import logging
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime, timezone
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -441,4 +441,4 @@ class Skill:
def _now_iso() -> 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")
+58
View File
@@ -0,0 +1,58 @@
"""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
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$")
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)]
@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")
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()