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:
Teknium authored and Teknium committed 2026-10-17 08:03:53 -07:00
1 parent 2b53b7c933
commit 861a0299ed
22 files changed
+106 -83

No files matched your search

+4 -4
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+50
View File
@@ -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
+4 -35
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+1
View File
@@ -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",
+2 -2
View File
@@ -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)),
)
+6 -6
View File
@@ -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"]))
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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"):
+3 -2
View File
@@ -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,
)
+6 -6
View File
@@ -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())
+1 -1
View File
@@ -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