mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
refactor(gateway): no re-export shims for the split status/base helpers; the lock split is one-way
The round-1 health offset moved safe_url_for_log and the scoped-lock release pair into siblings but re-exported them from gateway.status / gateway.platforms.base, and status_scoped_locks read gateway.status privates at call time while gateway.status re-exported the sibling's privates (_unusable_lock_dir, _warned_unusable_lock_dirs). AGENTS.md: no re-export shims for internal moves. - gateway/status_lock_dir.py: the unusable-lock-dir report (ScopedLockDirUnusable + public unusable_lock_dir / _recovered / reset / reported). gateway.status imports it; it imports nothing back. - gateway/status_scoped_locks.py: only the release pair, reading gateway.status at call time; gateway.status no longer imports it. - Every importer points at the real home: base.py, run_bootstrap.py, the Feishu and Slack adapters, and the tests that patch them (gateway.status_scoped_locks.release_*). safe_url_for_log comes from gateway.platforms.base_log_safety; base.py imports it only for its own use. - classify_changes: status_lock_dir.py rides with gateway/status.py on the e2e_upgrade lane. code_health: 0 blocking (status.py stays under its main value). Review finding by @unsupportedpastels on #140490 (minor 4) and by @alt-glitch on #140490 (suggestion 4) Refs #134889 (W2) Refs #140337
This commit is contained in:
1 parent
2b53b7c933
commit
861a0299ed
22 files changed
+106
-83
No files matched your search
@@ -19,7 +19,7 @@ import uuid
|
||||
import weakref
|
||||
from abc import ABC, abstractmethod
|
||||
from hermes_dir_acl import make_user_dir, unusable_dir_remedy
|
||||
from gateway.platforms.base_log_safety import safe_url_for_log # re-exported: adapters import it from here
|
||||
from gateway.platforms.base_log_safety import safe_url_for_log
|
||||
|
||||
from utils import normalize_proxy_url
|
||||
from agent.i18n import t
|
||||
@@ -2191,8 +2191,8 @@ class BasePlatformAdapter(ABC):
|
||||
"""Acquire a scoped lock for this adapter; True on success. A live cross-HERMES_HOME
|
||||
holder is replaced only when the runner armed this adapter for its initial
|
||||
``--replace`` connect (the status module validates ownership and terminates)."""
|
||||
from gateway.status import (
|
||||
ScopedLockDirUnusable, acquire_scoped_lock, scoped_lock_owner_label, take_over_scoped_lock_holder)
|
||||
from gateway.status import acquire_scoped_lock, scoped_lock_owner_label, take_over_scoped_lock_holder
|
||||
from gateway.status_lock_dir import ScopedLockDirUnusable
|
||||
self._platform_lock_scope, self._platform_lock_identity = scope, identity
|
||||
lock_meta = {"platform": self.platform.value}
|
||||
try:
|
||||
@@ -2245,7 +2245,7 @@ class BasePlatformAdapter(ABC):
|
||||
identity = getattr(self, '_platform_lock_identity', None)
|
||||
if not identity:
|
||||
return
|
||||
from gateway.status import release_scoped_lock
|
||||
from gateway.status_scoped_locks import release_scoped_lock
|
||||
release_scoped_lock(self._platform_lock_scope, identity)
|
||||
self._platform_lock_identity = None
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Log-safe rendering split from :mod:`gateway.platforms.base` (re-exported there)."""
|
||||
"""Log-safe URL rendering for adapter logs (split from :mod:`gateway.platforms.base`)."""
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ async def _start_gateway_replace_existing_instance(existing_pid: int, replace: b
|
||||
_clear_takeover_marker_quiet()
|
||||
# Stopped (Ctrl+Z) processes don't release scoped locks on exit; stale lock files block the new gateway.
|
||||
try:
|
||||
from gateway.status import release_all_scoped_locks
|
||||
from gateway.status_scoped_locks import release_all_scoped_locks
|
||||
_released = release_all_scoped_locks(owner_pid=existing_pid, owner_start_time=existing_start_time)
|
||||
if _released:
|
||||
logger.info("Released %d stale scoped lock(s) from old gateway.", _released)
|
||||
|
||||
+4
-6
@@ -29,9 +29,7 @@ from gateway.status_inline_source import (
|
||||
)
|
||||
from hermes_constants import _get_platform_default_hermes_home, get_hermes_home, get_process_hermes_home
|
||||
from hermes_cli._subprocess_compat import pid_exists_stdlib
|
||||
from gateway.status_scoped_locks import ( # re-exported: the public scoped-lock API lives on gateway.status
|
||||
ScopedLockDirUnusable, _unusable_lock_dir, _warned_unusable_lock_dirs, release_all_scoped_locks,
|
||||
release_scoped_lock)
|
||||
from gateway.status_lock_dir import unusable_lock_dir, unusable_lock_dir_recovered
|
||||
from utils import atomic_json_write
|
||||
|
||||
if sys.platform == "win32":
|
||||
@@ -1550,14 +1548,14 @@ def acquire_scoped_lock(
|
||||
) -> tuple[bool, Optional[dict[str, Any]]]:
|
||||
"""Acquire a machine-local lock keyed by scope + identity (one Telegram token across homes).
|
||||
|
||||
Raises :class:`ScopedLockDirUnusable` (logged once per dir with the repair) when this token may
|
||||
Raises :class:`gateway.status_lock_dir.ScopedLockDirUnusable` (logged once per dir with the repair) when this token may
|
||||
not use the lock dir at all."""
|
||||
lock_path = _get_scope_lock_path(scope, identity)
|
||||
try:
|
||||
acquired = _acquire_scoped_lock_at(lock_path, scope, identity, metadata)
|
||||
except PermissionError as exc:
|
||||
raise _unusable_lock_dir(lock_path.parent, exc) from exc
|
||||
_warned_unusable_lock_dirs.discard(lock_path.parent)
|
||||
raise unusable_lock_dir(lock_path.parent, exc) from exc
|
||||
unusable_lock_dir_recovered(lock_path.parent)
|
||||
return acquired
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""The unusable scoped-lock dir report (#134889 W2). :mod:`gateway.status` raises it from
|
||||
``acquire_scoped_lock``; nothing here reads back into that module."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_dir_acl import unusable_dir_remedy
|
||||
|
||||
log = logging.getLogger("gateway.status")
|
||||
|
||||
|
||||
class ScopedLockDirUnusable(PermissionError):
|
||||
"""This token cannot create, read or replace locks in the scoped-lock dir (#134889 W2).
|
||||
|
||||
On Windows an elevated install/update creates ``gateway-locks`` owned by Administrators with
|
||||
no entry for the user; the same user's non-elevated gateway then gets ``Permission denied`` on
|
||||
every token lock. The adapter reports it once and names the repair; it stays retryable (the
|
||||
dir is shared by every token-locked adapter), so a repair is picked up without a restart. A
|
||||
``PermissionError`` subclass so existing callers keep their handling.
|
||||
"""
|
||||
|
||||
|
||||
# Lock dirs already reported unusable by this process: WARNING once per dir, DEBUG after, since the
|
||||
# reconnect watcher and every adapter sharing the dir re-enter the acquire path.
|
||||
_warned: set[Path] = set()
|
||||
|
||||
|
||||
def unusable_lock_dir(lock_dir: Path, exc: PermissionError) -> ScopedLockDirUnusable:
|
||||
"""Log *lock_dir* as unusable (WARNING once, then DEBUG) and return the error to raise."""
|
||||
level = logging.DEBUG if lock_dir in _warned else logging.WARNING
|
||||
_warned.add(lock_dir)
|
||||
log.log(level, "Gateway lock directory %s is not usable by this process (%s); it was likely created "
|
||||
"by an elevated (administrator) Hermes. Platforms that need a token lock stay down until it "
|
||||
"is repaired. To repair, %s", lock_dir, exc, unusable_dir_remedy(lock_dir))
|
||||
return ScopedLockDirUnusable(exc.errno, f"gateway lock directory is not usable: {exc}", str(lock_dir))
|
||||
|
||||
|
||||
def unusable_lock_dir_recovered(lock_dir: Path) -> None:
|
||||
"""A lock was taken in *lock_dir*: warn again if it ever becomes unusable later."""
|
||||
_warned.discard(lock_dir)
|
||||
|
||||
|
||||
def reset_unusable_lock_dir_reports() -> None:
|
||||
"""Forget every report (a fresh process's view; tests)."""
|
||||
_warned.clear()
|
||||
|
||||
|
||||
def unusable_lock_dir_reported(lock_dir: Path) -> bool:
|
||||
return lock_dir in _warned
|
||||
@@ -1,48 +1,18 @@
|
||||
"""Scoped-lock helpers split from :mod:`gateway.status` (re-exported there): the unusable lock-dir
|
||||
report (#134889 W2) and lock release. Status-module helpers are read through the module at call time
|
||||
so tests that patch ``gateway.status`` keep steering them."""
|
||||
"""Scoped-lock release, split from :mod:`gateway.status` (acquire stays there). One-way: this
|
||||
module reads the status helpers through the module at call time (so tests that patch
|
||||
``gateway.status`` keep steering them); :mod:`gateway.status` never imports it."""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from hermes_dir_acl import unusable_dir_remedy
|
||||
|
||||
log = logging.getLogger("gateway.status")
|
||||
|
||||
|
||||
class ScopedLockDirUnusable(PermissionError):
|
||||
"""This token cannot create, read or replace locks in the scoped-lock dir (#134889 W2).
|
||||
|
||||
On Windows an elevated install/update creates ``gateway-locks`` owned by Administrators with
|
||||
no entry for the user; the same user's non-elevated gateway then gets ``Permission denied`` on
|
||||
every token lock. Retrying cannot fix an ACL, so the adapter reports it once, names the repair
|
||||
and stays down instead of crash-looping. A ``PermissionError`` subclass so existing callers
|
||||
keep their handling.
|
||||
"""
|
||||
|
||||
|
||||
# Lock dirs already reported unusable by this process: WARNING once per dir, DEBUG after, since the
|
||||
# reconnect watcher and every adapter sharing the dir re-enter the acquire path.
|
||||
_warned_unusable_lock_dirs: set[Path] = set()
|
||||
|
||||
|
||||
def _unusable_lock_dir(lock_dir: Path, exc: PermissionError) -> ScopedLockDirUnusable:
|
||||
level = logging.DEBUG if lock_dir in _warned_unusable_lock_dirs else logging.WARNING
|
||||
_warned_unusable_lock_dirs.add(lock_dir)
|
||||
log.log(level, "Gateway lock directory %s is not usable by this process (%s); it was likely created "
|
||||
"by an elevated (administrator) Hermes. Platforms that need a token lock stay down until it "
|
||||
"is repaired. To repair, %s", lock_dir, exc, unusable_dir_remedy(lock_dir))
|
||||
return ScopedLockDirUnusable(exc.errno, f"gateway lock directory is not usable: {exc}", str(lock_dir))
|
||||
from gateway import status as _status
|
||||
|
||||
|
||||
def release_scoped_lock(scope: str, identity: str) -> None:
|
||||
"""Release a scope lock owned by this PID. No start_time equality check: on-disk null vs a live
|
||||
fingerprint would wedge reconnects."""
|
||||
from gateway import status as _status
|
||||
lock_path = _status._get_scope_lock_path(scope, identity)
|
||||
if (_status._read_json_file(lock_path) or {}).get("pid") == os.getpid():
|
||||
_status._unlink_quietly(lock_path)
|
||||
@@ -53,7 +23,6 @@ def release_all_scoped_locks(
|
||||
) -> int:
|
||||
"""Remove scoped lock files (--replace cleanup); returns the count removed. With ``owner_pid``
|
||||
only that gateway's records go (``owner_start_time`` narrows against PID reuse)."""
|
||||
from gateway import status as _status
|
||||
lock_dir = _status._get_lock_dir()
|
||||
if not lock_dir.exists():
|
||||
return 0
|
||||
|
||||
@@ -92,7 +92,8 @@ from gateway.platforms.base import (
|
||||
cache_audio_from_bytes_async, cache_image_from_bytes_async,
|
||||
)
|
||||
from gateway.platforms.event import MessageEvent, MessageType, ProcessingOutcome
|
||||
from gateway.status import acquire_scoped_lock, release_scoped_lock
|
||||
from gateway.status import acquire_scoped_lock
|
||||
from gateway.status_scoped_locks import release_scoped_lock
|
||||
from hermes_constants import get_hermes_home
|
||||
from utils import atomic_json_write, env_float, env_int
|
||||
|
||||
|
||||
@@ -44,11 +44,12 @@ from gateway.platforms._shared import (
|
||||
extra_or_secret as _extra_or_secret, get_scoped_secret as _get_scoped_secret,
|
||||
platform_gate_env as _scoped_gate_env, send_error
|
||||
)
|
||||
from gateway.platforms.base_log_safety import safe_url_for_log
|
||||
from gateway.platforms.helpers import MessageDeduplicator
|
||||
from gateway.platforms.base import (
|
||||
gateway_trust_env, BasePlatformAdapter, ExecApprovalPrompt,
|
||||
SendResult, SUPPORTED_DOCUMENT_TYPES, SUPPORTED_VIDEO_TYPES, _TEXT_INJECT_EXTENSIONS,
|
||||
is_host_excluded_by_no_proxy, resolve_proxy_url, safe_url_for_log, _ssrf_redirect_guard,
|
||||
is_host_excluded_by_no_proxy, resolve_proxy_url, _ssrf_redirect_guard,
|
||||
cache_document_from_bytes_async, cache_video_from_bytes_async,
|
||||
)
|
||||
from gateway.platforms.event import MessageEvent, MessageType, ProcessingOutcome
|
||||
|
||||
@@ -281,6 +281,7 @@ _UPDATE_PIPELINE = (
|
||||
"hermes_dir_acl.py", # update restores the Windows user ACL on the home after elevation
|
||||
"gateway/status.py", # code_sha stamp the fleet verify reads
|
||||
"gateway/status_inline_source.py", # the Windows pause identifies inline-bootstrapped gateways
|
||||
"gateway/status_lock_dir.py", # gateway/status.py imports it at module level
|
||||
"gateway/control_socket.py", # pause-for-update verb
|
||||
"gateway/code_skew.py",
|
||||
"gateway/host_rendezvous.py",
|
||||
|
||||
@@ -2873,11 +2873,11 @@ class TestBuzzAdapterLifecycle:
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_releases_scoped_lock(self, monkeypatch):
|
||||
"""The identity lock taken in connect() must be released on disconnect."""
|
||||
import gateway.status as gateway_status
|
||||
import gateway.status_scoped_locks as scoped_locks
|
||||
|
||||
released = []
|
||||
monkeypatch.setattr(
|
||||
gateway_status,
|
||||
scoped_locks,
|
||||
"release_scoped_lock",
|
||||
lambda platform, key: released.append((platform, key)),
|
||||
)
|
||||
|
||||
@@ -202,7 +202,7 @@ async def test_reconnect_closes_previous_client_to_prevent_zombie_websocket(monk
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
|
||||
monkeypatch.setattr("gateway.status.acquire_scoped_lock", lambda scope, identity, metadata=None: (True, None))
|
||||
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr("gateway.status_scoped_locks.release_scoped_lock", lambda scope, identity: None)
|
||||
|
||||
intents = SimpleNamespace(
|
||||
message_content=False, dm_messages=False, guild_messages=False,
|
||||
@@ -271,7 +271,7 @@ async def test_connect_clears_previous_fatal_error(monkeypatch):
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
|
||||
monkeypatch.setattr("gateway.status.acquire_scoped_lock", lambda scope, identity, metadata=None: (True, None))
|
||||
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr("gateway.status_scoped_locks.release_scoped_lock", lambda scope, identity: None)
|
||||
|
||||
intents = SimpleNamespace(
|
||||
message_content=False, dm_messages=False, guild_messages=False,
|
||||
@@ -311,7 +311,7 @@ async def test_connect_timeout_cancels_bot_task(monkeypatch):
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
|
||||
monkeypatch.setattr("gateway.status.acquire_scoped_lock", lambda scope, identity, metadata=None: (True, None))
|
||||
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr("gateway.status_scoped_locks.release_scoped_lock", lambda scope, identity: None)
|
||||
|
||||
intents = SimpleNamespace(
|
||||
message_content=False, dm_messages=False, guild_messages=False,
|
||||
@@ -359,7 +359,7 @@ async def test_disconnect_cancels_running_bot_task(monkeypatch):
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
|
||||
monkeypatch.setattr("gateway.status.acquire_scoped_lock", lambda scope, identity, metadata=None: (True, None))
|
||||
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr("gateway.status_scoped_locks.release_scoped_lock", lambda scope, identity: None)
|
||||
|
||||
# Simulate a zombie bot_task that never finishes (as if discord.py is mid-handshake)
|
||||
async def _forever():
|
||||
@@ -874,7 +874,7 @@ class TestPrivilegedIntentsRequiredFatal:
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
|
||||
@@ -919,7 +919,7 @@ async def test_skill_catalog_scan_runs_off_the_event_loop(monkeypatch):
|
||||
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
monkeypatch.setattr("gateway.status.acquire_scoped_lock", lambda scope, identity, metadata=None: (True, None))
|
||||
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr("gateway.status_scoped_locks.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr(discord_platform.Intents, "default", lambda: SimpleNamespace(
|
||||
message_content=False, dm_messages=False, guild_messages=False, members=False, voice_states=False))
|
||||
monkeypatch.setattr(discord_platform.commands, "Bot", lambda **kw: FakeBot(intents=kw["intents"]))
|
||||
|
||||
@@ -237,7 +237,7 @@ async def _connect(adapter: DiscordAdapter, monkeypatch, bot_factory):
|
||||
"gateway.status.acquire_scoped_lock",
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr("gateway.status.release_scoped_lock", lambda scope, identity: None)
|
||||
monkeypatch.setattr("gateway.status_scoped_locks.release_scoped_lock", lambda scope, identity: None)
|
||||
intents = SimpleNamespace(
|
||||
message_content=False, dm_messages=False, guild_messages=False,
|
||||
members=False, voice_states=False,
|
||||
|
||||
@@ -14,11 +14,11 @@ from gateway.platforms.base import (
|
||||
BasePlatformAdapter,
|
||||
SendResult,
|
||||
cache_image_from_bytes,
|
||||
safe_url_for_log,
|
||||
utf16_len,
|
||||
_prefix_within_utf16_limit,
|
||||
cache_audio_from_bytes,
|
||||
)
|
||||
from gateway.platforms.base_log_safety import safe_url_for_log
|
||||
from gateway.platforms.event import MessageEvent
|
||||
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ async def test_start_gateway_replace_reaps_old_gateway_children_posix(
|
||||
"gateway.status._get_process_start_time", lambda pid: 0 if pid == 42 else None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_all_scoped_locks", lambda **kwargs: 0
|
||||
"gateway.status_scoped_locks.release_all_scoped_locks", lambda **kwargs: 0
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status._snapshot_gateway_children",
|
||||
|
||||
@@ -175,7 +175,7 @@ async def test_start_gateway_replace_aborts_when_force_killed_pid_still_alive(
|
||||
monkeypatch.setattr("gateway.status.get_running_pid", lambda: 42)
|
||||
monkeypatch.setattr("gateway.status.remove_pid_file", _mock_remove_pid_file)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_all_scoped_locks",
|
||||
"gateway.status_scoped_locks.release_all_scoped_locks",
|
||||
_mock_release_all_scoped_locks,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -300,7 +300,7 @@ async def test_start_gateway_replace_writes_takeover_marker_before_sigterm(
|
||||
"gateway.status._get_process_start_time", lambda pid: 0 if pid == 42 else None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_all_scoped_locks",
|
||||
"gateway.status_scoped_locks.release_all_scoped_locks",
|
||||
lambda **kwargs: 0,
|
||||
)
|
||||
monkeypatch.setattr("gateway.status.write_takeover_marker", record_write_marker)
|
||||
|
||||
@@ -15,6 +15,8 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from gateway import status
|
||||
from gateway import status_lock_dir as scoped_locks
|
||||
from gateway.status_scoped_locks import release_scoped_lock
|
||||
from gateway.platforms.base import BasePlatformAdapter
|
||||
from gateway.restart import is_global_startup_conflict
|
||||
from gateway.run_startup import GatewayStartupMixin
|
||||
@@ -57,7 +59,7 @@ def locked_out_dir(tmp_path, monkeypatch):
|
||||
lock_dir.mkdir()
|
||||
lock_dir.chmod(stat.S_IRUSR | stat.S_IXUSR) # r-x: lists, cannot create the token lock
|
||||
monkeypatch.setenv("HERMES_GATEWAY_LOCK_DIR", str(lock_dir))
|
||||
getattr(status, "_warned_unusable_lock_dirs", set()).clear()
|
||||
scoped_locks.reset_unusable_lock_dir_reports()
|
||||
yield lock_dir
|
||||
lock_dir.chmod(stat.S_IRWXU)
|
||||
|
||||
@@ -83,12 +85,12 @@ def test_unusable_lock_dir_parks_the_adapter_with_the_repair(locked_out_dir, cap
|
||||
|
||||
|
||||
def test_lock_dir_repair_is_picked_up_without_restart(locked_out_dir):
|
||||
with pytest.raises(status.ScopedLockDirUnusable):
|
||||
with pytest.raises(scoped_locks.ScopedLockDirUnusable):
|
||||
status.acquire_scoped_lock("telegram-bot-token", "tok")
|
||||
locked_out_dir.chmod(stat.S_IRWXU)
|
||||
assert status.acquire_scoped_lock("telegram-bot-token", "tok") == (True, None)
|
||||
assert locked_out_dir not in status._warned_unusable_lock_dirs
|
||||
status.release_scoped_lock("telegram-bot-token", "tok")
|
||||
assert not scoped_locks.unusable_lock_dir_reported(locked_out_dir)
|
||||
release_scoped_lock("telegram-bot-token", "tok")
|
||||
|
||||
|
||||
def test_unreadable_stale_token_lock_is_named_not_reported_as_a_holderless_conflict(tmp_path, monkeypatch):
|
||||
@@ -100,7 +102,7 @@ def test_unreadable_stale_token_lock_is_named_not_reported_as_a_holderless_confl
|
||||
stale.chmod(0)
|
||||
lock_dir.chmod(stat.S_IRUSR | stat.S_IXUSR) # the stale file cannot be read or unlinked either
|
||||
try:
|
||||
with pytest.raises(status.ScopedLockDirUnusable):
|
||||
with pytest.raises(scoped_locks.ScopedLockDirUnusable):
|
||||
status.acquire_scoped_lock("telegram-bot-token", "tok")
|
||||
finally:
|
||||
lock_dir.chmod(stat.S_IRWXU)
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestSignalConnectCleanup:
|
||||
|
||||
with patch("plugins.platforms.signal.adapter.httpx.AsyncClient", return_value=mock_client), \
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
|
||||
patch("gateway.status.release_scoped_lock") as mock_release:
|
||||
patch("gateway.status_scoped_locks.release_scoped_lock") as mock_release:
|
||||
result = await adapter.connect()
|
||||
|
||||
assert result is False
|
||||
|
||||
@@ -547,7 +547,7 @@ class TestSlackConnectCleanup:
|
||||
),
|
||||
patch.dict(os.environ, {"SLACK_APP_TOKEN": "xapp-fake"}),
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)),
|
||||
patch("gateway.status.release_scoped_lock"),
|
||||
patch("gateway.status_scoped_locks.release_scoped_lock"),
|
||||
patch("asyncio.create_task", side_effect=_fake_create_task),
|
||||
):
|
||||
result = await adapter.connect()
|
||||
@@ -556,7 +556,7 @@ class TestSlackConnectCleanup:
|
||||
first_handler.close_async.assert_awaited_once_with()
|
||||
assert adapter._handler is second_handler
|
||||
|
||||
with patch("gateway.status.release_scoped_lock"):
|
||||
with patch("gateway.status_scoped_locks.release_scoped_lock"):
|
||||
await adapter.disconnect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -594,7 +594,7 @@ class TestSlackConnectCleanup:
|
||||
adapter._proxy_url = "http://proxy.example.com:3128"
|
||||
adapter._bot_user_id = "U_BOT"
|
||||
|
||||
with patch("gateway.status.release_scoped_lock") as mock_release:
|
||||
with patch("gateway.status_scoped_locks.release_scoped_lock") as mock_release:
|
||||
await adapter.disconnect()
|
||||
|
||||
handler.close_async.assert_awaited_once_with()
|
||||
@@ -684,7 +684,7 @@ class TestSlackSocketWatchdog:
|
||||
patch.object(_slack_mod, "AsyncSocketModeHandler", fake_factory),
|
||||
patch.dict(os.environ, {"SLACK_APP_TOKEN": "xapp-fake"}),
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)),
|
||||
patch("gateway.status.release_scoped_lock"),
|
||||
patch("gateway.status_scoped_locks.release_scoped_lock"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ def _connect_with_recording_app(
|
||||
patch.object(_slack_mod, "AsyncSocketModeHandler", return_value=MagicMock()), \
|
||||
patch.dict(os.environ, {"SLACK_APP_TOKEN": "xapp-fake"}), \
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
|
||||
patch("gateway.status.release_scoped_lock"), \
|
||||
patch("gateway.status_scoped_locks.release_scoped_lock"), \
|
||||
patch("hermes_cli.plugins.get_plugin_manager", return_value=fake_mgr), \
|
||||
patch("asyncio.create_task"):
|
||||
result = asyncio.run(adapter.connect())
|
||||
@@ -253,7 +253,7 @@ class TestSlackAdapterPluginActionWiring:
|
||||
patch.object(_slack_mod, "AsyncSocketModeHandler", return_value=MagicMock()), \
|
||||
patch.dict(os.environ, {"SLACK_APP_TOKEN": "xapp-fake"}), \
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
|
||||
patch("gateway.status.release_scoped_lock"), \
|
||||
patch("gateway.status_scoped_locks.release_scoped_lock"), \
|
||||
patch("hermes_cli.plugins.get_plugin_manager",
|
||||
side_effect=RuntimeError("plugins broken")), \
|
||||
patch("asyncio.create_task"):
|
||||
|
||||
@@ -11,6 +11,7 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
|
||||
from gateway import status
|
||||
from gateway.status_scoped_locks import release_all_scoped_locks, release_scoped_lock
|
||||
from datetime import UTC
|
||||
|
||||
|
||||
@@ -889,7 +890,7 @@ class TestScopedLocks:
|
||||
}))
|
||||
monkeypatch.setattr(status, "_get_process_start_time", lambda pid: 987654321)
|
||||
|
||||
status.release_scoped_lock("discord-bot-token", "secret")
|
||||
release_scoped_lock("discord-bot-token", "secret")
|
||||
|
||||
assert not lock_path.exists()
|
||||
|
||||
@@ -1007,7 +1008,7 @@ class TestScopedLocks:
|
||||
"kind": "hermes-gateway",
|
||||
}))
|
||||
|
||||
removed = status.release_all_scoped_locks(
|
||||
removed = release_all_scoped_locks(
|
||||
owner_pid=111,
|
||||
owner_start_time=222,
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_polling_conflict_retries_before_fatal(monkeypatch):
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ async def test_polling_conflict_becomes_fatal_after_retries(monkeypatch):
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
|
||||
@@ -294,7 +294,7 @@ async def test_connect_clears_webhook_before_polling(monkeypatch):
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
|
||||
@@ -353,7 +353,7 @@ async def test_connect_does_not_block_on_post_connect_housekeeping(monkeypatch):
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
|
||||
@@ -434,7 +434,7 @@ async def test_polling_conflict_reschedule_uses_running_loop(monkeypatch):
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
|
||||
@@ -598,7 +598,7 @@ async def test_conflict_callback_disarms_before_scheduling(monkeypatch):
|
||||
lambda scope, identity, metadata=None: (True, None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"gateway.status.release_scoped_lock",
|
||||
"gateway.status_scoped_locks.release_scoped_lock",
|
||||
lambda scope, identity: None,
|
||||
)
|
||||
monkeypatch.setattr("asyncio.sleep", AsyncMock())
|
||||
|
||||
@@ -174,7 +174,7 @@ class TestConnectCleanup:
|
||||
patch.object(Path, "exists", autospec=True, side_effect=_path_exists), \
|
||||
patch("subprocess.run", return_value=install_result), \
|
||||
patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
|
||||
patch("gateway.status.release_scoped_lock") as mock_release:
|
||||
patch("gateway.status_scoped_locks.release_scoped_lock") as mock_release:
|
||||
result = await adapter.connect()
|
||||
|
||||
assert result is False
|
||||
|
||||
Reference in new issue
Block a user