aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_login_retry_is_resilient.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-27 22:20:35 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-27 22:20:35 +0200
commit7662484cae8e74b7d9aa383bd6cd0dad4690aadc (patch)
tree057609311e339c839dcad04b33de62e2541a9943 /packages/meshbay-node/tests/test_login_retry_is_resilient.py
parent326d7796c79f616f5a0f2058386df2bded657c78 (diff)
downloadmeshbay-7662484cae8e74b7d9aa383bd6cd0dad4690aadc.tar.gz
fix(node): a Windows daemon that stops properly, starts honestly and runs once
Found by installing the builds and driving every startup mode live: - Stop through the node's own control API first (POST /api/shutdown, loopback and per-run token): the one channel that reaches a daemon in any session without elevation -- a service node runs in session 0 -- and the one that runs its shutdown. Then Task Scheduler, then a forced stop. Nine stops in a row used to log no shutdown at all: each was a TerminateProcess. - The forced stop spares the command running it. The frozen meshbay-node.exe is the daemon and every CLI verb, so `taskkill /IM meshbay-node.exe` killed `autostart stop` and `restart-daemon` themselves: exit 1, no output, and no node after a restart. It excludes its own pid and its parent's, and /T takes a venv launcher's python child and a daemon's ffmpeg children with it. - Start and restart report the version that answered, never "started" about a node nobody asked; `service start` says so when no node answered, and where the log is. - A second instance fails before it touches anything. The daemon wrote ui-token, then failed to bind inside uvicorn's task and exited with the reason on a hidden console; the node still running then refused every stop and status, its token file naming a dead process. The control port is now bound first (exclusively on Windows, where SO_REUSEADDR would share it), and a refusal is logged and exits 2. Linux had the same order. - The daemon logs to %LOCALAPPDATA%\meshbay\state\node.log: Task Scheduler discards its stderr. Only the daemon run opens it, never a CLI verb. - Hub sign-in waits are interruptible, a stop requested before the node is up is honoured, and a hub that answers 429 or restarts leaves the node in waiting_for_hub rather than looking dead. - operator_paired is null until the roster is read, instead of a false that showed "No operator paired" about a node whose pairing was intact. The node test conftest also points HOME, USERPROFILE, LOCALAPPDATA and APPDATA at a throwaway directory for every test, and keeps log_file() away from the developer's own node: redirecting HOME alone isolates nothing on Windows, and the CLI tests had been writing invite and pairing codes into the real profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_login_retry_is_resilient.py')
-rw-r--r--packages/meshbay-node/tests/test_login_retry_is_resilient.py20
1 files changed, 18 insertions, 2 deletions
diff --git a/packages/meshbay-node/tests/test_login_retry_is_resilient.py b/packages/meshbay-node/tests/test_login_retry_is_resilient.py
index e37a413..39358bb 100644
--- a/packages/meshbay-node/tests/test_login_retry_is_resilient.py
+++ b/packages/meshbay-node/tests/test_login_retry_is_resilient.py
@@ -14,8 +14,7 @@ import asyncio
import httpx
import pytest
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from meshbay_node.config import Config, GroupConfig, HubConfig, KeystoreConfig, NodeConfig
+from meshbay_node.config import Config, HubConfig, KeystoreConfig, NodeConfig
from meshbay_node.daemon import NodeDaemon
@@ -96,6 +95,23 @@ async def test_a_401_still_retries_and_stays_alive(tmp_path, monkeypatch):
assert daemon._state.get("status") in ("waiting_for_node_key", "waiting_for_account")
+def test_a_node_waiting_for_its_link_stays_under_the_hubs_sign_in_limit():
+ """At 5s a node waiting for its key made 12 attempts a minute against a
+ limit of 10: it put itself in 429 back-off within a minute, every minute,
+ and the desktop app did not recognise that state as a node at all."""
+ import re
+ from pathlib import Path
+
+ from meshbay_node.daemon import NodeDaemon
+
+ nodes_py = (Path(__file__).resolve().parents[2] / "meshbay-hub" / "src" / "meshbay_hub"
+ / "api" / "nodes.py").read_text(encoding="utf-8")
+ m = re.search(r'@router\.post\("/auth"\)\s*@limiter\.limit\("(\d+)/minute"\)', nodes_py)
+ assert m, "the hub's node sign-in limit moved; update this test"
+ per_minute = int(m.group(1))
+ assert 60 / NodeDaemon.LINK_WAIT_S < per_minute
+
+
@pytest.mark.asyncio
async def test_a_genuine_client_error_still_raises(tmp_path, monkeypatch):
"""A 400/422 is a bug, not a transient state — it must not be swallowed."""