Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ def _compute_repo_mtime(repo_root: Path) -> float:
return newest


def _loaded_modules_missing_expected_exports() -> bool:
"""Return True if loaded ``hermes_cli.config`` or ``utils`` lack expected attributes."""
cfg_mod = sys.modules.get("hermes_cli.config")
if cfg_mod is not None and not hasattr(cfg_mod, "cfg_get"):
return True
utils_mod = sys.modules.get("utils")
if utils_mod is not None:
if not hasattr(utils_mod, "atomic_replace"):
return True
if not hasattr(utils_mod, "base_url_host_matches"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixed list is not a complete stale-module contract: current regression coverage at tests/test_stale_utils_module_import.py:52-63 reproduces the same ImportError when stale utils lacks env_float, which this probe does not check. The linked June 24 report also identifies missing exports outside utils.

return True
return False


def _coerce_gateway_timestamp(value: Any) -> Optional[float]:
"""Best-effort conversion of stored gateway timestamps to epoch seconds.

Expand Down Expand Up @@ -1019,6 +1033,13 @@ def __init__(self, config: Optional[GatewayConfig] = None):
self._boot_repo_mtime: float = _compute_repo_mtime(
self._repo_root_for_staleness,
)
if self._boot_repo_mtime <= 0.0 and self._boot_wall_time:
logger.warning(
"Stale-code mtime baseline unavailable (no readable sentinel "
"files under %s). Export-based detection still applies. "
"See Issue #17648.",
self._repo_root_for_staleness,
)
except Exception:
self._boot_wall_time = 0.0
self._repo_root_for_staleness = Path(".")
Expand Down Expand Up @@ -2637,6 +2658,8 @@ def _detect_stale_code(self) -> bool:
sentinel file is readable, to avoid false-positive restart loops
in unusual checkouts (sparse clones, read-only filesystems).
"""
if _loaded_modules_missing_expected_exports():
return True
if not self._boot_wall_time or not self._boot_repo_mtime:
return False
try:
Expand Down
33 changes: 30 additions & 3 deletions tests/gateway/test_stale_code_self_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
"""

import os
import sys
import time
import types
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from gateway.run import (
GatewayRunner,
_compute_repo_mtime,
_loaded_modules_missing_expected_exports,
_STALE_CODE_SENTINELS,
)

Expand Down Expand Up @@ -96,6 +96,33 @@ def test_detect_stale_code_false_when_files_unchanged(tmp_path):
assert runner._detect_stale_code() is False


def test_loaded_modules_missing_expected_exports_detects_stale_config(monkeypatch):
stale_cfg = types.ModuleType("hermes_cli.config")
monkeypatch.setitem(sys.modules, "hermes_cli.config", stale_cfg)
assert _loaded_modules_missing_expected_exports() is True


def test_loaded_modules_missing_expected_exports_detects_stale_utils(monkeypatch):
stale_utils = types.ModuleType("utils")
monkeypatch.setitem(sys.modules, "utils", stale_utils)
assert _loaded_modules_missing_expected_exports() is True


def test_detect_stale_code_true_when_sys_modules_config_lacks_cfg_get(
tmp_path, monkeypatch
):
stale_cfg = types.ModuleType("hermes_cli.config")
monkeypatch.setitem(sys.modules, "hermes_cli.config", stale_cfg)

repo = _make_tmp_repo(tmp_path)
baseline = time.time() - 100
for rel in _STALE_CODE_SENTINELS:
os.utime(repo / rel, (baseline, baseline))

runner = _make_runner(repo, boot_mtime=baseline, boot_wall=baseline)
assert runner._detect_stale_code() is True


def test_detect_stale_code_true_after_update(tmp_path):
"""Sentinel files newer than boot snapshot β†’ stale."""
repo = _make_tmp_repo(tmp_path)
Expand Down