From e54ba2ade2c9eab2f6dd023b95d70dca4f713020 Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Sat, 8 Aug 2026 10:02:47 +0700 Subject: [PATCH] test(gateway): cover null start_time scoped-lock self-reacquire Regression for Discord 503 reconnect false-positive discord-bot-token lock against the live gateway PID (#81468). --- tests/gateway/test_status.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/gateway/test_status.py b/tests/gateway/test_status.py index 237331782bb83..a4a1e106a6073 100644 --- a/tests/gateway/test_status.py +++ b/tests/gateway/test_status.py @@ -418,6 +418,62 @@ class TestScopedLocks: assert payload["pid"] == os.getpid() assert payload["metadata"]["platform"] == "telegram" + def test_acquire_scoped_lock_self_reacquires_when_disk_start_time_null( + self, tmp_path, monkeypatch + ): + """#81468: same PID with on-disk start_time=null must self-reacquire. + + After Discord 503 reconnect, the scoped lock still names this process + but may carry ``start_time: null`` (psutil miss at first write). The + freshly built record has a real fingerprint. Requiring equality made + the gateway report its own PID as a foreign token squatter. + """ + monkeypatch.setenv("HERMES_GATEWAY_LOCK_DIR", str(tmp_path / "locks")) + lock_path = tmp_path / "locks" / "discord-bot-token-2bb80d537b1da3e3.lock" + lock_path.parent.mkdir(parents=True, exist_ok=True) + lock_path.write_text(json.dumps({ + "pid": os.getpid(), + "start_time": None, + "kind": "hermes-gateway", + "argv": ["hermes_cli/main.py", "--profile", "milena", "gateway", "run", "--replace"], + "scope": "discord-bot-token", + })) + + # Live process can resolve start_time; disk cannot — the mismatch + # that previously failed the self-reacquire short-circuit. + monkeypatch.setattr(status, "_get_process_start_time", lambda pid: 987654321) + # If we wrongly fall through to staleness, a gateway-looking self PID + # would be treated as a live foreign holder (return False). + monkeypatch.setattr(status, "_pid_exists", lambda pid: True) + monkeypatch.setattr(status, "_looks_like_gateway_process", lambda pid: True) + + acquired, existing = status.acquire_scoped_lock( + "discord-bot-token", "secret", metadata={"platform": "discord"} + ) + + assert acquired is True + assert existing["pid"] == os.getpid() + payload = json.loads(lock_path.read_text()) + assert payload["pid"] == os.getpid() + assert payload["start_time"] == 987654321 + assert payload["metadata"]["platform"] == "discord" + + def test_release_scoped_lock_allows_null_disk_start_time(self, tmp_path, monkeypatch): + """#81468: release must not no-op when disk start_time is null.""" + monkeypatch.setenv("HERMES_GATEWAY_LOCK_DIR", str(tmp_path / "locks")) + lock_path = tmp_path / "locks" / "discord-bot-token-2bb80d537b1da3e3.lock" + lock_path.parent.mkdir(parents=True, exist_ok=True) + lock_path.write_text(json.dumps({ + "pid": os.getpid(), + "start_time": None, + "kind": "hermes-gateway", + })) + monkeypatch.setattr(status, "_get_process_start_time", lambda pid: 987654321) + + status.release_scoped_lock("discord-bot-token", "secret") + + assert not lock_path.exists() + def test_acquire_scoped_lock_race_second_acquirer_loses(self, tmp_path, monkeypatch): """Two racing starters both observe the same stale lock. The loser's