mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
* feat: add entry-point plugin system * fix: harden plugin config and client loading * docs(workflow): add detailed manual for publishing reme-auto-fin to PyPI - Provide step-by-step instructions for updating project.version and merging branches - Explain dependency verification for reme-ai on PyPI during build - Specify requirements for GitHub Actions secret configuration and version uniqueness - Describe manual workflow triggering and input of version number - Recommend publishing order for related projects - Clarify that only manual dispatch triggers publishing, no automatic triggers on push or tag * feat: support plugin-defined component types * refactor: simplify plugin configuration * fix: isolate plugin loading and defer client fallback * refactor: freeze built-in component registry * fix: isolate config entry point loading * fix: complete auto-fin package metadata
177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
"""Tests for the psutil-based service discovery helpers in ``service_utils``.
|
|
|
|
Covers the cross-platform replacements for the old ``lsof`` / ``pgrep``
|
|
shell-outs:
|
|
|
|
* ``_pid_on_port`` — real integration (open a listening socket, find its
|
|
PID) plus a mock test proving inaccessible processes are skipped, not
|
|
crashed on.
|
|
* ``_scan_reme_procs`` — mock ``psutil.process_iter`` to assert cmdline
|
|
parsing, defaulting, filtering, and AccessDenied tolerance.
|
|
"""
|
|
|
|
# pylint: disable=protected-access,missing-function-docstring,unused-argument
|
|
|
|
import os
|
|
import socket
|
|
from types import SimpleNamespace
|
|
|
|
import psutil
|
|
|
|
from reme.utils import service_utils as su
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Fakes for mocking psutil.process_iter
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def _conn(port: int, status=psutil.CONN_LISTEN, has_laddr: bool = True):
|
|
return SimpleNamespace(status=status, laddr=SimpleNamespace(port=port) if has_laddr else None)
|
|
|
|
|
|
class _FakeProc:
|
|
"""Stand-in for a psutil.Process yielded by process_iter."""
|
|
|
|
def __init__(self, pid: int, *, cmdline=None, conns=None, conn_exc=None):
|
|
self.info = {"pid": pid, "cmdline": cmdline if cmdline is not None else []}
|
|
self._conns = conns or []
|
|
self._conn_exc = conn_exc
|
|
|
|
def net_connections(self, kind="tcp"):
|
|
if self._conn_exc is not None:
|
|
raise self._conn_exc
|
|
return self._conns
|
|
|
|
|
|
class _RaisingInfoProc:
|
|
"""Process whose `.info` access raises (simulates AccessDenied in iteration)."""
|
|
|
|
def __init__(self, exc):
|
|
self._exc = exc
|
|
|
|
@property
|
|
def info(self):
|
|
raise self._exc
|
|
|
|
|
|
def _patch_iter(monkeypatch, procs):
|
|
monkeypatch.setattr(su.psutil, "process_iter", lambda attrs=None: iter(procs))
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# _pid_on_port
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def test_pid_on_port_finds_own_listening_socket():
|
|
"""Real integration: a listening socket is attributed to this process's PID."""
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
srv.bind(("127.0.0.1", 0))
|
|
srv.listen(1)
|
|
port = srv.getsockname()[1]
|
|
try:
|
|
assert su._pid_on_port(port) == os.getpid()
|
|
finally:
|
|
srv.close()
|
|
|
|
|
|
def test_pid_on_port_none_when_nobody_listening():
|
|
"""A port with no listener resolves to None."""
|
|
# Grab then immediately release an ephemeral port to get a number that is
|
|
# (almost certainly) unbound right now.
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.bind(("127.0.0.1", 0))
|
|
free_port = s.getsockname()[1]
|
|
s.close()
|
|
assert su._pid_on_port(free_port) is None
|
|
|
|
|
|
def test_pid_on_port_skips_inaccessible_processes(monkeypatch):
|
|
"""AccessDenied on one process must not abort the scan of the rest."""
|
|
procs = [
|
|
_FakeProc(111, conn_exc=psutil.AccessDenied(pid=111)),
|
|
_FakeProc(222, conn_exc=psutil.NoSuchProcess(pid=222)),
|
|
_FakeProc(333, conns=[_conn(9999)]),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._pid_on_port(9999) == 333
|
|
|
|
|
|
def test_pid_on_port_ignores_non_listen_and_mismatched(monkeypatch):
|
|
"""Only LISTEN sockets on the exact port match."""
|
|
procs = [
|
|
_FakeProc(1, conns=[_conn(9999, status=psutil.CONN_ESTABLISHED)]), # right port, wrong state
|
|
_FakeProc(2, conns=[_conn(8888)]), # listening, wrong port
|
|
_FakeProc(3, conns=[_conn(9999, has_laddr=False)]), # no laddr
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._pid_on_port(9999) is None
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# _scan_reme_procs
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
def test_scan_reme_procs_parses_host_and_port(monkeypatch):
|
|
procs = [
|
|
_FakeProc(
|
|
123,
|
|
cmdline=["python", "-m", "reme.reme", "start", "service.host=0.0.0.0", "service.port=8123"],
|
|
),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(123, "0.0.0.0", 8123)]
|
|
|
|
|
|
def test_scan_reme_procs_defaults_when_args_absent(monkeypatch):
|
|
procs = [_FakeProc(7, cmdline=["reme", "start"])]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(7, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
def test_scan_reme_procs_filters_unrelated(monkeypatch):
|
|
procs = [
|
|
_FakeProc(1, cmdline=["reme", "find_reme"]), # no 'start' token
|
|
_FakeProc(2, cmdline=["python", "-m", "http.server", "start"]), # no reme token
|
|
_FakeProc(3, cmdline=[]), # empty cmdline
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert not su._scan_reme_procs()
|
|
|
|
|
|
def test_scan_reme_procs_ignores_non_digit_port(monkeypatch):
|
|
"""A malformed service.port= falls back to the default port, not a crash."""
|
|
procs = [_FakeProc(9, cmdline=["reme", "start", "service.port=notaport"])]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(9, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
def test_scan_reme_procs_skips_access_denied(monkeypatch):
|
|
"""A process that denies cmdline access is skipped, others still scanned."""
|
|
procs = [
|
|
_RaisingInfoProc(psutil.AccessDenied(pid=99)),
|
|
_FakeProc(5, cmdline=["reme", "start"]),
|
|
]
|
|
_patch_iter(monkeypatch, procs)
|
|
assert su._scan_reme_procs() == [(5, su.REME_DEFAULT_HOST, su.REME_DEFAULT_PORT)]
|
|
|
|
|
|
def test_running_app_config_preserves_plugins(monkeypatch):
|
|
"""Process replay exposes the full config while the compatibility helper returns service only."""
|
|
monkeypatch.setattr(su, "_reme_start_argv", lambda: [["config=example"]])
|
|
monkeypatch.setattr("reme.config.parse_args", lambda *_args: ("start", {"config": "example"}))
|
|
monkeypatch.setattr(
|
|
"reme.config.resolve_app_config",
|
|
lambda **_kwargs: {
|
|
"plugins": ["example"],
|
|
"service": {"backend": "plugin-client", "port": 9911},
|
|
},
|
|
)
|
|
|
|
assert su.running_app_config() == {
|
|
"plugins": ["example"],
|
|
"service": {"backend": "plugin-client", "port": 9911},
|
|
}
|
|
assert su.running_service_config() == {"backend": "plugin-client", "port": 9911}
|