"""The node's life on a Windows desktop: set up at sign-in, stopped for real. Two defects from the first Windows beta tester, each found again live on a clean install (2026-10-07): - After a service-mode install the node was never set up for the account that signed in, the Node section stayed out of the sidebar, and the way out was `meshbay-node init` in a terminal plus the key pasted on the profile page. - A Stop left meshbay-node.exe running while the Node page said "Stopped": the control API closes before the process exits, and the page asked nothing else. What can run for real here does (processes, the CLI's stop, the exit timer); the desktop application's side is pinned by reading main.js and app.js, the only evidence available without launching it -- tests/e2e_windows_app.py launches it. """ import inspect import os import subprocess import sys import textwrap import time from pathlib import Path import pytest from meshbay_node import platform as plat from meshbay_node.cli import lifecycle ROOT = Path(__file__).resolve().parents[3] MAIN_JS = ROOT / "packages" / "meshbay-client" / "src" / "main.js" STATIC = ROOT / "packages" / "meshbay-hub" / "src" / "meshbay_hub" / "static" def _js(path: Path) -> str: return path.read_text(encoding="utf-8") def _block(src: str, start: str, end: str = "\n });\n") -> str: return src.split(start, 1)[1].split(end, 1)[0] # ── node.toml ──────────────────────────────────────────────────────────────── def test_a_byte_order_mark_in_node_toml_is_read(tmp_path): from meshbay_node.config import load_config cfg_file = tmp_path / "node.toml" cfg_file.write_bytes(b"\xef\xbb\xbf" + b'[hub]\nurl = "http://127.0.0.1:1"\nusername = "someone"\n') assert load_config(cfg_file).hub.username == "someone" def test_a_node_toml_it_cannot_read_is_said_not_swallowed(tmp_path): """A service node has no console: the reason it will not start goes to the log, where it used to die after "Logging to ..." in silence.""" cfg_file = tmp_path / "node.toml" cfg_file.write_text("[hub\nusername = ", encoding="utf-8") env = {**os.environ, "LOCALAPPDATA": str(tmp_path), "APPDATA": str(tmp_path), "HOME": str(tmp_path), "USERPROFILE": str(tmp_path)} r = subprocess.run([sys.executable, "-c", "from meshbay_node.daemon import main; main()", "--config", str(cfg_file)], capture_output=True, text=True, timeout=60, env=env) assert r.returncode == 1, r.stdout + r.stderr assert "cannot read" in r.stdout if sys.platform == "win32": log = tmp_path / "meshbay" / "state" / "node.log" assert "cannot read" in log.read_text(encoding="utf-8") # ── the daemon leaves when it has stopped ──────────────────────────────────── def test_a_stopped_node_does_not_wait_for_a_busy_thread(): """A worker still walking a large tree kept the process alive after _shutdown(), with its control API already closed.""" script = textwrap.dedent(""" import threading, time from meshbay_node.daemon import exit_after threading.Thread(target=time.sleep, args=(120,), name="walker").start() exit_after(0.5, "the node has stopped") """) t0 = time.monotonic() r = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, timeout=60) assert r.returncode == 0, r.stderr assert time.monotonic() - t0 < 30 assert "walker" in r.stderr, "it names what it did not wait for" def test_only_the_real_daemon_ends_its_own_process(): """Tests run daemons inside pytest: the timer must be armed by main() alone, both before _shutdown() (a hung shutdown) and after it.""" from meshbay_node import daemon assert daemon.NodeDaemon.exit_process_when_stopped is False assert "daemon.exit_process_when_stopped = True" in inspect.getsource(daemon.main) shutdown = inspect.getsource(daemon.NodeDaemon._shutdown) assert shutdown.count("if self.exit_process_when_stopped:") == 2 assert shutdown.index("SHUTDOWN_DEADLINE_SECS") < shutdown.index('log.info("Node stopped")') \ < shutdown.index("EXIT_GRACE_SECS") # ── the CLI's stop: by pid, then checked ───────────────────────────────────── class _Stub: def __init__(self, monkeypatch, *, answering=None, graceful=False, pids=(), kill_works=True): self.killed: list[int] = [] self.pids = list(pids) monkeypatch.setattr(lifecycle, "_running_status", lambda cfg: answering) monkeypatch.setattr(plat, "request_graceful_stop", lambda *a, **k: graceful) monkeypatch.setattr(plat, "service_status", lambda: {"installed": False, "state": ""}) monkeypatch.setattr(plat, "_pid_alive", lambda pid: pid in self.pids) monkeypatch.setattr(plat, "autostart_end", lambda: None) monkeypatch.setattr(plat, "node_pids", lambda: list(self.pids)) def kill(pid): self.killed.append(pid) if kill_works: self.pids.remove(pid) monkeypatch.setattr(plat, "kill_pid", kill) clock = iter(range(0, 10_000)) monkeypatch.setattr(time, "monotonic", lambda: next(clock)) monkeypatch.setattr(time, "sleep", lambda s: None) class _Cfg: data_dir = Path(".") class node: ui_port = 1 def test_a_stop_the_node_did_not_finish_kills_it_by_pid(monkeypatch): stub = _Stub(monkeypatch, answering={"pid": 4242}, graceful=False, pids=[4242]) assert lifecycle._stop_node(_Cfg) == "stopped (forced)" assert stub.killed == [4242] def test_a_node_process_left_behind_is_never_called_stopped(monkeypatch): _Stub(monkeypatch, answering=None, graceful=False, pids=[5151], kill_works=False) with pytest.raises(lifecycle.NodeStillRunning, match="5151"): lifecycle._stop_node(_Cfg) def test_a_node_answering_nothing_is_still_a_node(monkeypatch): """Its API closed, its process there: not "not running".""" stub = _Stub(monkeypatch, answering=None, graceful=False, pids=[6161]) monkeypatch.setattr(plat, "autostart_end", lambda: stub.pids.clear()) assert lifecycle._stop_node(_Cfg) == "stopped (forced)" def test_a_stop_that_fails_exits_non_zero_with_the_reason(monkeypatch, capsys): def refuse(cfg): raise lifecycle.NodeStillRunning("meshbay-node is still running (pid 7)") monkeypatch.setattr(lifecycle, "_stop_node", refuse) monkeypatch.setattr(lifecycle, "load_config", lambda p: _Cfg) class Args: config = None subcommand = "stop" monkeypatch.setattr(plat, "service_status", lambda: {"installed": False, "state": ""}) monkeypatch.setattr(sys, "platform", "win32") with pytest.raises(SystemExit) as e: lifecycle.autostart(Args) assert e.value.code == 1 assert "pid 7" in capsys.readouterr().out @pytest.mark.skipif(sys.platform != "win32", reason="lists and kills real processes") def test_node_pids_finds_every_copy_but_its_caller(tmp_path, monkeypatch): """Under an image name of this test's own: the real one would find, and kill, the developer's node.""" import shutil exe = tmp_path / "mbpidtest.exe" shutil.copy(r"C:\Windows\System32\PING.EXE", exe) proc = subprocess.Popen([str(exe), "-n", "300", "127.0.0.1"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) monkeypatch.setattr(plat, "NODE_IMAGE", "mbpidtest.exe") try: assert plat.node_pids() == [proc.pid] plat.kill_pid(proc.pid) assert proc.wait(timeout=10) is not None assert plat.node_pids() == [] finally: if proc.poll() is None: proc.kill() # ── the desktop application: what it says, and when it sets the node up ────── def test_the_node_page_asks_the_node_not_only_the_task(): """A node started from a terminal runs while the service task reads "Ready"; one on its way out answers nothing for a while. The page said "Stopped" about both.""" main_js = _js(MAIN_JS) status = main_js.split("async function nodeServiceStatus()", 1)[1].split("\n }\n", 1)[0] assert "/running/i.test(svc.state)" not in status assert status.count("winNodeActivity()") == 2 activity = main_js.split("async function winNodeActivity()", 1)[1].split("\n }\n", 1)[0] assert "probeNode()" in activity and "winNodePids()" in activity assert "'unknown'" in activity and "'inactive'" in activity page = _js(STATIC / "node-page.js") assert "const stoppable = info.activeState !== 'inactive';" in page assert "disabled=${!!busy || !stoppable}" in page def test_signing_in_sets_up_this_machines_node(): main_js = _js(MAIN_JS) assert "handle('node:ensure'" in main_js ensure = main_js.split("async function ensureNode(opts)", 1)[1].split("\n }\n", 1)[0] # Only the node this build ships, on Windows: Linux is unchanged. assert "process.platform !== 'win32' || !hasBundledNode()" in ensure # A node set up for someone else is left as it is... assert ensure.index("return { state: 'other_account' }") < ensure.index("provisionNode(") # ...and so is an account already linked to another node. assert "if (linked && linked !== p.pk_node_ed25519) return { state: 'other_node' };" in ensure assert ensure.index("state: 'other_node'") < ensure.index("linkNodeKeyAndAwaitRunning(") # Started however this machine is set up, through the CLI like every start. assert "winNodeStartVia(['autostart', 'start'])" in ensure # One start at a time. start = main_js.split("handle('node:start'", 1)[1][:400] assert "if (nodeEnsuring) await nodeEnsuring" in start preload = _js(ROOT / "packages" / "meshbay-client" / "src" / "preload.js") assert "ensure: (opts) => ipcRenderer.invoke('node:ensure', opts)" in preload assert "bridge.node.ensure(opts)" in _js(STATIC / "platform.js") def test_the_node_section_shows_for_a_node_not_yet_linked(): """It waited for the hub to hold a node key, which nothing gave it.""" app = _js(STATIC / "app.js") assert "showNode=${hasNodeKey || localNode}" in app assert "platform.capabilities.nodeAdmin && (hasNodeKey || localNode)" in app effect = app.split("const signedInAs = user ? user.username : null;", 1)[1] effect = effect.split("}, [signedInAs]);", 1)[0] assert "platform.node.ensure(" in effect and "platform.node.installed()" in effect # The token rotates; a renewal is not a sign-in. assert "}, [signedInAs]);" in app