Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

316 lines
12 KiB
Python

"""Tests for the startup orphan / stale-lockfile sweep (issue #1501)."""
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import patch
import pytest
import tools.system.fleet_monitoring.sweep as sweep_module
from tools.system.fleet_monitoring.registry import AgentRecord, AgentRegistry
# A PID large enough to never be allocated on Linux/macOS
# (``kernel.pid_max`` defaults to 32768 or 4194304). Cross-platform
# Windows would also fail to find this process.
_DEAD_PID = 2**31 - 1
@pytest.fixture
def isolated_registry(tmp_path: Path) -> AgentRegistry:
"""An ``AgentRegistry`` writing to a tmp dir so tests don't touch
the developer's real ``~/.opensre/agents.jsonl``."""
return AgentRegistry(path=tmp_path / "agents.jsonl")
# ---------------------------------------------------------------------------
# Registry-side sweep
# ---------------------------------------------------------------------------
def test_dead_pid_record_is_forgotten(isolated_registry: AgentRegistry, tmp_path: Path) -> None:
isolated_registry.register(AgentRecord(name="ghost", pid=_DEAD_PID, command="bin"))
assert isolated_registry.get(_DEAD_PID) is not None # sanity: registered ok
result = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "no-such-dir")
assert isolated_registry.get(_DEAD_PID) is None
assert len(result.removed_records) == 1
assert result.removed_records[0].pid == _DEAD_PID
def test_live_pid_record_is_kept(isolated_registry: AgentRegistry, tmp_path: Path) -> None:
"""The current Python process is alive; its record must not be pruned."""
self_pid = os.getpid()
isolated_registry.register(AgentRecord(name="opensre", pid=self_pid, command="opensre"))
result = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "no-such-dir")
assert isolated_registry.get(self_pid) is not None
assert result.removed_records == ()
def test_access_denied_pid_is_kept_not_pruned(
isolated_registry: AgentRegistry, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A live PID owned by another user (where ``psutil.Process(pid)``
construction would raise ``AccessDenied`` and the old probe-based
check would falsely return ``None``) must NOT be pruned.
Locks in the fix for the Copilot review concern: the sweep now
uses ``psutil.pid_exists`` which doesn't traverse the access
boundary, so cross-user processes survive both the registry and
lockfile sweeps.
"""
foreign_pid = 4242 # arbitrary; we mock pid_exists to say "alive"
isolated_registry.register(
AgentRecord(name="other-users-claude", pid=foreign_pid, command="claude")
)
monkeypatch.setattr(sweep_module, "pid_exists", lambda pid: pid == foreign_pid)
result = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "no-such-dir")
assert isolated_registry.get(foreign_pid) is not None
assert result.removed_records == ()
def test_access_denied_pid_lockfile_is_kept(
isolated_registry: AgentRegistry, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Symmetric to the registry case: a lockfile for a live foreign
PID must survive the sweep even though the calling process can't
introspect it. Without the ``pid_exists`` fix, the sweep would
delete this file and break the foreign agent's coordination.
"""
foreign_pid = 4242
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
foreign_lock = lock_dir / f"{foreign_pid}.lock"
foreign_lock.write_text("locked")
monkeypatch.setattr(sweep_module, "pid_exists", lambda pid: pid == foreign_pid)
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
assert foreign_lock.exists()
assert result.removed_locks == ()
def test_many_dead_pids_trigger_one_rewrite_not_n(
isolated_registry: AgentRegistry, tmp_path: Path
) -> None:
"""Pruning N dead records must rewrite the JSONL once, not N times.
Spies on ``AgentRegistry._rewrite`` to count calls — locks in the
Copilot review concern about boot-time N-rewrite scaling.
"""
for pid in range(_DEAD_PID - 5, _DEAD_PID):
isolated_registry.register(AgentRecord(name=f"ghost-{pid}", pid=pid, command="bin"))
rewrite_call_count = 0
real_rewrite = isolated_registry._rewrite
def _spy() -> None:
nonlocal rewrite_call_count
rewrite_call_count += 1
real_rewrite()
isolated_registry._rewrite = _spy # type: ignore[method-assign]
result = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "no-such-dir")
assert len(result.removed_records) == 5
assert rewrite_call_count == 1, (
f"expected 1 batched rewrite for 5 dead PIDs, got {rewrite_call_count}"
)
# ---------------------------------------------------------------------------
# Idempotency — the headline acceptance criterion
# ---------------------------------------------------------------------------
def test_idempotent_second_run_is_a_noop(isolated_registry: AgentRegistry, tmp_path: Path) -> None:
"""Running ``sweep`` twice in a row: the second invocation removes
nothing because the first already cleaned up. This is the contract
the issue spec leans on for safe boot-time invocation."""
isolated_registry.register(AgentRecord(name="ghost", pid=_DEAD_PID, command="bin"))
isolated_registry.register(AgentRecord(name="opensre", pid=os.getpid(), command="opensre"))
first = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "no-such-dir")
second = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "no-such-dir")
assert len(first.removed_records) == 1
assert second.removed_records == ()
# The live record survives both rounds.
assert isolated_registry.get(os.getpid()) is not None
# ---------------------------------------------------------------------------
# Lockfile-side sweep
# ---------------------------------------------------------------------------
def test_stale_lockfile_for_dead_pid_is_removed(
isolated_registry: AgentRegistry, tmp_path: Path
) -> None:
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
stale_lock = lock_dir / f"{_DEAD_PID}.lock"
stale_lock.write_text("locked")
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
assert not stale_lock.exists()
assert stale_lock in result.removed_locks
def test_live_lockfile_is_kept(isolated_registry: AgentRegistry, tmp_path: Path) -> None:
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
live_lock = lock_dir / f"{os.getpid()}.lock"
live_lock.write_text("locked")
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
assert live_lock.exists()
assert result.removed_locks == ()
def test_non_pid_lockfile_names_are_ignored(
isolated_registry: AgentRegistry, tmp_path: Path
) -> None:
"""Files in lock_dir whose stem isn't an integer (foreign tools'
artifacts, future conventions, etc.) are left untouched. The
sweep only acts on its own ``<pid>.lock`` shape."""
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
foreign = lock_dir / "registry.lock"
foreign.write_text("not ours")
other = lock_dir / "settings.json"
other.write_text("{}")
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
assert foreign.exists()
assert other.exists()
assert result.removed_locks == ()
def test_missing_lock_dir_is_tolerated(isolated_registry: AgentRegistry, tmp_path: Path) -> None:
"""A non-existent lock_dir doesn't raise — common on a fresh
install where no agent has registered yet."""
result = sweep_module.sweep(isolated_registry, lock_dir=tmp_path / "definitely-not-here")
assert result.removed_locks == ()
def test_lockfile_unlink_failure_is_logged_with_exc_info(
isolated_registry: AgentRegistry, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""If the OS refuses to remove a lockfile (e.g. permission denied
on a read-only filesystem), the sweep logs at WARNING **with**
``exc_info`` so the operator can diagnose the cause, then
continues rather than crashing the REPL boot.
"""
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
stuck_lock = lock_dir / f"{_DEAD_PID}.lock"
stuck_lock.write_text("locked")
with (
patch.object(Path, "unlink", side_effect=PermissionError("read-only fs")),
caplog.at_level("WARNING", logger="tools.system.fleet_monitoring.sweep"),
):
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
# The file is still there because unlink was mocked to fail
assert stuck_lock.exists()
# ...but the sweep didn't claim a successful removal
assert stuck_lock not in result.removed_locks
# ...and it logged the failure with exception info attached so
# an ops operator sees *why* the unlink failed, not just *that*
# it did.
matching = [r for r in caplog.records if "failed to remove stale lockfile" in r.getMessage()]
assert matching, "expected a WARNING log for the unlink failure"
assert matching[0].exc_info is not None, (
"warning log must include exc_info so the underlying exception is visible"
)
def test_lockfile_already_gone_is_silently_idempotent(
isolated_registry: AgentRegistry, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A concurrent sweep / external cleanup may have already removed
the lockfile between our existence check and our unlink call.
``FileNotFoundError`` is expected behaviour, not a warning —
treat it as idempotent success and emit no log line.
"""
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
racy_lock = lock_dir / f"{_DEAD_PID}.lock"
racy_lock.write_text("locked")
with (
patch.object(Path, "unlink", side_effect=FileNotFoundError("already gone")),
caplog.at_level("WARNING", logger="tools.system.fleet_monitoring.sweep"),
):
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
# The race-already-removed file is treated as nothing-to-do:
# not in removed_locks, but also no warning logged.
assert racy_lock not in result.removed_locks
assert not [r for r in caplog.records if "failed to remove stale lockfile" in r.getMessage()]
# ---------------------------------------------------------------------------
# SweepResult ergonomics
# ---------------------------------------------------------------------------
def test_sweep_result_total_property(isolated_registry: AgentRegistry, tmp_path: Path) -> None:
"""``SweepResult.total`` is the sum of removed records and locks —
the single number that's most useful in a debug log line."""
lock_dir = tmp_path / "agents"
lock_dir.mkdir()
isolated_registry.register(AgentRecord(name="ghost", pid=_DEAD_PID, command="bin"))
(lock_dir / f"{_DEAD_PID}.lock").write_text("locked")
# ``_DEAD_PID - 1`` to stay within int32 — ``_DEAD_PID`` is the
# signed-int32 max, so ``+ 1`` would overflow ``psutil.pid_exists``.
(lock_dir / f"{_DEAD_PID - 1}.lock").write_text("locked")
result = sweep_module.sweep(isolated_registry, lock_dir=lock_dir)
assert result.total == 1 + 2 # 1 record + 2 locks
def test_empty_sweep_result_is_falsy_in_total() -> None:
"""A no-op sweep produces ``SweepResult()`` with total == 0 — the
boot-time logger uses this to decide whether to emit a message."""
empty = sweep_module.SweepResult()
assert empty.total == 0
assert empty.removed_records == ()
assert empty.removed_locks == ()
# ---------------------------------------------------------------------------
# REPL-boot wrapper
# ---------------------------------------------------------------------------
def test_run_startup_sweep_swallows_exceptions(monkeypatch: pytest.MonkeyPatch) -> None:
"""``run_startup_sweep`` is the REPL's boot hook; an unexpected
exception inside must NOT propagate, otherwise a sweep bug could
block the REPL from starting at all."""
def _explode(*_args: object, **_kwargs: object) -> None:
raise RuntimeError("simulated registry-load failure")
monkeypatch.setattr(sweep_module, "AgentRegistry", _explode)
result = sweep_module.run_startup_sweep()
assert isinstance(result, sweep_module.SweepResult)
assert result.total == 0