mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
feat(code): show last update check time in dcode doctor (#4307)
The Updates section of `dcode doctor` reported status but not *when* the last PyPI update check happened. This adds a "Last checked" item (e.g. `2h ago`, or `never`), read from the offline cache so the command stays network-free. Made by [Open SWE](https://openswe.vercel.app/agents/5810207b-bdbb-6bee-54cf-3b4bcf31a3e4) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -218,11 +218,35 @@ def _collect_updates() -> DiagnosticSection:
|
||||
update_status = f"v{latest} available"
|
||||
else:
|
||||
update_status = "up to date"
|
||||
items.append(DiagnosticItem("Latest version", update_status))
|
||||
items.extend(
|
||||
(
|
||||
DiagnosticItem("Latest version", update_status),
|
||||
DiagnosticItem("Last checked", _format_last_checked()),
|
||||
)
|
||||
)
|
||||
|
||||
return DiagnosticSection(title="Updates", items=items)
|
||||
|
||||
|
||||
def _format_last_checked() -> str:
|
||||
"""Return a relative description of the last update check, or `never`.
|
||||
|
||||
`never` covers both the no-check-recorded case and, defensively, a stamp
|
||||
that cannot be formatted. `get_last_update_check_time` only returns finite,
|
||||
in-range epochs, so the formatting path does not raise here.
|
||||
"""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from deepagents_code.sessions import format_relative_timestamp
|
||||
from deepagents_code.update_check import get_last_update_check_time
|
||||
|
||||
checked_at = get_last_update_check_time()
|
||||
if checked_at is None:
|
||||
return "never"
|
||||
iso = datetime.fromtimestamp(checked_at, tz=UTC).isoformat()
|
||||
return format_relative_timestamp(iso) or "never"
|
||||
|
||||
|
||||
def _sanitize_endpoint(endpoint: str) -> str:
|
||||
"""Return a paste-safe custom endpoint identifier.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
@@ -240,7 +241,8 @@ def get_cached_update_available() -> tuple[bool, str | None]:
|
||||
if not isinstance(data, dict):
|
||||
return False, None
|
||||
checked_at = data.get("checked_at")
|
||||
if not isinstance(checked_at, (int, float)):
|
||||
checked_at = _coerce_checked_at(checked_at)
|
||||
if checked_at is None:
|
||||
return False, None
|
||||
if time.time() - checked_at >= CACHE_TTL:
|
||||
return False, None
|
||||
@@ -253,6 +255,42 @@ def get_cached_update_available() -> tuple[bool, str | None]:
|
||||
return False, None
|
||||
|
||||
|
||||
def _coerce_checked_at(value: object) -> float | None:
|
||||
"""Return a valid epoch timestamp from cached state, or `None`."""
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
return None
|
||||
checked_at = float(value)
|
||||
if not math.isfinite(checked_at):
|
||||
return None
|
||||
try:
|
||||
datetime.fromtimestamp(checked_at, tz=UTC)
|
||||
except (OverflowError, OSError, ValueError):
|
||||
return None
|
||||
return checked_at
|
||||
|
||||
|
||||
def get_last_update_check_time() -> float | None:
|
||||
"""Return the epoch time of the last PyPI update check, or `None`.
|
||||
|
||||
Reads the `checked_at` stamp recorded in `CACHE_FILE` when the update cache
|
||||
is written (primarily by `get_latest_version`; also seeded by
|
||||
`_write_release_requires_prereleases`). Missing, corrupt, or non-numeric
|
||||
data fail-soft to `None` so callers can render an "unknown" state without
|
||||
contacting the network.
|
||||
"""
|
||||
try:
|
||||
if not CACHE_FILE.exists():
|
||||
return None
|
||||
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
checked_at = data.get("checked_at")
|
||||
except (OSError, json.JSONDecodeError, TypeError):
|
||||
logger.debug("Failed to read last update check time", exc_info=True)
|
||||
return None
|
||||
return _coerce_checked_at(checked_at)
|
||||
|
||||
|
||||
def _requires_prerelease_dependency(requirements: Sequence[object] | None) -> bool:
|
||||
"""Return whether any requirement specifier names a pre-release version.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import argparse
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
@@ -194,6 +195,64 @@ class TestCollectTracing:
|
||||
assert labels["Replica project"] == "replica"
|
||||
|
||||
|
||||
class TestCollectUpdates:
|
||||
"""Tests for the Updates diagnostic section."""
|
||||
|
||||
def _labels(self, cache_file: Path) -> dict[str, str]:
|
||||
"""Collect the Updates labels, reading `checked_at` from `cache_file`.
|
||||
|
||||
Patches `CACHE_FILE` rather than `get_last_update_check_time` so the
|
||||
section flows through the genuine reader and the epoch -> ISO ->
|
||||
relative-time conversion, not a stub.
|
||||
"""
|
||||
from deepagents_code.doctor import _collect_updates
|
||||
|
||||
with (
|
||||
patch("deepagents_code.config._is_editable_install", return_value=False),
|
||||
patch(
|
||||
"deepagents_code.update_check.is_update_check_enabled",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.update_check.is_auto_update_enabled",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.update_check.get_cached_update_available",
|
||||
return_value=(False, "1.0.0"),
|
||||
),
|
||||
patch("deepagents_code.update_check.CACHE_FILE", cache_file),
|
||||
):
|
||||
section = _collect_updates()
|
||||
return {item.label: item.value for item in section.items}
|
||||
|
||||
def test_last_checked_shows_relative_time(self, tmp_path: Path) -> None:
|
||||
"""A check stamped an hour ago renders as `1h ago` via the real read."""
|
||||
cache = tmp_path / "latest_version.json"
|
||||
cache.write_text(
|
||||
json.dumps({"checked_at": time.time() - 3600}), encoding="utf-8"
|
||||
)
|
||||
assert self._labels(cache)["Last checked"] == "1h ago"
|
||||
|
||||
def test_last_checked_just_now_on_future_stamp(self, tmp_path: Path) -> None:
|
||||
"""A future stamp (clock skew) renders as `just now`, not a crash."""
|
||||
cache = tmp_path / "latest_version.json"
|
||||
cache.write_text(
|
||||
json.dumps({"checked_at": time.time() + 3600}), encoding="utf-8"
|
||||
)
|
||||
assert self._labels(cache)["Last checked"] == "just now"
|
||||
|
||||
def test_last_checked_never_without_cache(self, tmp_path: Path) -> None:
|
||||
"""An absent cache reports `never` rather than crashing."""
|
||||
assert self._labels(tmp_path / "latest_version.json")["Last checked"] == "never"
|
||||
|
||||
def test_last_checked_never_on_corrupt_stamp(self, tmp_path: Path) -> None:
|
||||
"""A non-finite stamp fails soft to `never` instead of crashing doctor."""
|
||||
cache = tmp_path / "latest_version.json"
|
||||
cache.write_text(json.dumps({"checked_at": float("nan")}), encoding="utf-8")
|
||||
assert self._labels(cache)["Last checked"] == "never"
|
||||
|
||||
|
||||
class TestCommitHash:
|
||||
"""Tests for git commit hash detection."""
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ from deepagents_code.update_check import (
|
||||
format_shadowed_dcode_fix_command,
|
||||
format_shadowed_dcode_warning,
|
||||
get_cached_update_available,
|
||||
get_last_update_check_time,
|
||||
get_latest_version,
|
||||
get_release_time,
|
||||
get_sdk_release_time,
|
||||
@@ -273,6 +274,21 @@ class TestCachedUpdateAvailable:
|
||||
|
||||
mock_get.assert_not_called()
|
||||
|
||||
@pytest.mark.parametrize("checked_at", [float("nan"), float("inf"), 1e100])
|
||||
def test_invalid_numeric_checked_at_returns_no_answer_without_http(
|
||||
self, cache_file, checked_at: float
|
||||
) -> None:
|
||||
"""Corrupt numeric timestamps must not be treated as fresh cache."""
|
||||
cache_file.write_text(
|
||||
json.dumps({"version": "99.0.0", "checked_at": checked_at}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch("requests.get") as mock_get:
|
||||
assert get_cached_update_available() == (False, None)
|
||||
|
||||
mock_get.assert_not_called()
|
||||
|
||||
def test_missing_cache_returns_no_answer_without_http(self, cache_file) -> None:
|
||||
"""Missing cache should not block startup on a network request."""
|
||||
assert not cache_file.exists()
|
||||
@@ -291,6 +307,38 @@ class TestCachedUpdateAvailable:
|
||||
assert get_cached_update_available() == (False, __version__)
|
||||
|
||||
|
||||
class TestGetLastUpdateCheckTime:
|
||||
def test_reads_checked_at(self, cache_file) -> None:
|
||||
"""The stored `checked_at` epoch is returned as a float."""
|
||||
now = time.time()
|
||||
cache_file.write_text(json.dumps({"checked_at": now}), encoding="utf-8")
|
||||
assert get_last_update_check_time() == pytest.approx(now)
|
||||
|
||||
def test_missing_cache_returns_none(self, cache_file) -> None: # noqa: ARG002
|
||||
"""An absent cache yields `None`."""
|
||||
assert get_last_update_check_time() is None
|
||||
|
||||
def test_corrupt_cache_returns_none(self, cache_file) -> None:
|
||||
"""Unparseable cache data fails soft to `None`."""
|
||||
cache_file.write_text("{not valid json", encoding="utf-8")
|
||||
assert get_last_update_check_time() is None
|
||||
|
||||
def test_non_numeric_checked_at_returns_none(self, cache_file) -> None:
|
||||
"""A non-numeric (or boolean) `checked_at` is ignored."""
|
||||
cache_file.write_text(json.dumps({"checked_at": "soon"}), encoding="utf-8")
|
||||
assert get_last_update_check_time() is None
|
||||
cache_file.write_text(json.dumps({"checked_at": True}), encoding="utf-8")
|
||||
assert get_last_update_check_time() is None
|
||||
|
||||
@pytest.mark.parametrize("checked_at", [float("nan"), float("inf"), 1e100])
|
||||
def test_invalid_numeric_checked_at_returns_none(
|
||||
self, cache_file, checked_at: float
|
||||
) -> None:
|
||||
"""Invalid numeric `checked_at` values are ignored."""
|
||||
cache_file.write_text(json.dumps({"checked_at": checked_at}), encoding="utf-8")
|
||||
assert get_last_update_check_time() is None
|
||||
|
||||
|
||||
class TestGetLatestVersion:
|
||||
def test_fresh_fetch(self, cache_file) -> None:
|
||||
"""Successful PyPI fetch writes cache and returns version."""
|
||||
|
||||
Reference in New Issue
Block a user