aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_windows_node_lifecycle.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-09 18:19:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-10 09:57:29 +0200
commit42b562fcf312ddceb78438b5daea49d4c9b465c8 (patch)
tree770913e96292b4c5123f8c5ecba2d62577307f67 /packages/meshbay-node/tests/test_windows_node_lifecycle.py
parent25152ddb61a89ea3ea29d3aef5de13343429d32a (diff)
downloadmeshbay-42b562fcf312ddceb78438b5daea49d4c9b465c8.tar.gz
feat(packaging): the Store package declares what the NSIS scripts do
A test-signed install of the MSIX build, in the WindowsApps folder a Store install uses, showed that every script-made piece of the NSIS model breaks there, because each names the install folder and every update deletes it: the firewall rules went stale, the PATH entries piled up pointing at deleted folders, and the Startup-folder .vbs was refused ("Permission denied") right after sign-in. The network capabilities the manifest declared covered nothing: they make rules for sandboxed apps only, and a listener in the package still got the Windows firewall prompt. The package's own startup task was on by default, started the node whatever mode the Node page said, and ran the console executable, whose window stopped the node when closed. The package now declares what Windows then creates at install, carries across updates and removes with the app, all without an administrator prompt (each measured on the real install, through an update and a reboot): - firewall rules for the node, in a custom manifest template, since only a package-level element can hold them; - the startup task, off by default, running meshbay-nodew.exe, a new build of the daemon without a console; - an execution alias for meshbay-node.exe, so the app adds no PATH entry. The node's CLI switches the startup task (platform.startup_task, ctypes over the WinRT ABI): Windows gives the package's identity to the executables in it, not to a powershell.exe the app starts, which got "Element not found". `meshbay-node autostart install | remove | status` therefore works in the Store package from the app and a terminal alike; the app caches the answer, since the Node page polls. Starting at boot stays the .exe installer's: the Store package offers no service mode, and the CLI refuses `service install` there. Process listings count both image names. The Node page's status poll cleared the message of a refused action within five seconds; the two errors are kept apart now (all Windows builds). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_windows_node_lifecycle.py')
-rw-r--r--packages/meshbay-node/tests/test_windows_node_lifecycle.py106
1 files changed, 105 insertions, 1 deletions
diff --git a/packages/meshbay-node/tests/test_windows_node_lifecycle.py b/packages/meshbay-node/tests/test_windows_node_lifecycle.py
index 241f583..b85b6ea 100644
--- a/packages/meshbay-node/tests/test_windows_node_lifecycle.py
+++ b/packages/meshbay-node/tests/test_windows_node_lifecycle.py
@@ -173,7 +173,7 @@ def test_node_pids_finds_every_copy_but_its_caller(tmp_path, monkeypatch):
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")
+ monkeypatch.setattr(plat, "NODE_IMAGES", ("mbpidtest.exe",))
try:
assert plat.node_pids() == [proc.pid]
plat.kill_pid(proc.pid)
@@ -184,6 +184,110 @@ def test_node_pids_finds_every_copy_but_its_caller(tmp_path, monkeypatch):
proc.kill()
+@pytest.mark.skipif(sys.platform != "win32", reason="lists and kills real processes")
+def test_node_pids_counts_the_windowless_daemon_too(tmp_path, monkeypatch):
+ """The Store package's startup task runs meshbay-nodew.exe: a Stop that
+ looked for meshbay-node.exe alone would call that node gone."""
+ import shutil
+ procs = []
+ for name in ("mbpidtest.exe", "mbpidtestw.exe"):
+ shutil.copy(r"C:\Windows\System32\PING.EXE", tmp_path / name)
+ procs.append(subprocess.Popen([str(tmp_path / name), "-n", "300", "127.0.0.1"],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL))
+ monkeypatch.setattr(plat, "NODE_IMAGES", ("mbpidtest.exe", "mbpidtestw.exe"))
+ try:
+ assert sorted(plat.node_pids()) == sorted(p.pid for p in procs)
+ finally:
+ for p in procs:
+ p.kill()
+ assert set(plat.NODE_IMAGES) == {"mbpidtest.exe", "mbpidtestw.exe"}
+ monkeypatch.undo()
+ assert plat.NODE_IMAGES == ("meshbay-node.exe", "meshbay-nodew.exe")
+
+
+def _current_package_family() -> str:
+ import ctypes
+ length = ctypes.c_uint32(0)
+ k32 = ctypes.windll.kernel32
+ if k32.GetCurrentPackageFamilyName(ctypes.byref(length), None) != 122:
+ return ""
+ buf = ctypes.create_unicode_buffer(length.value)
+ k32.GetCurrentPackageFamilyName(ctypes.byref(length), buf)
+ return buf.value
+
+
+@pytest.mark.skipif(sys.platform != "win32", reason="package identity is Windows'")
+def test_only_meshbays_own_package_counts_as_the_store_install(monkeypatch):
+ """A terminal inside another packaged application (an IDE or an agent
+ from the Store) gives its processes that application's identity, and the
+ test suite itself may run in one: that is not MeshBay's package."""
+ assert plat.in_store_package() is False
+ family = _current_package_family()
+ if family:
+ monkeypatch.setattr(plat, "STORE_PACKAGE_FAMILY_PREFIX", family.split("_")[0] + "_")
+ assert plat.in_store_package() is True
+
+
+def test_the_store_package_switches_its_startup_task_not_a_launcher(monkeypatch, capsys):
+ """There a Startup-folder launcher or a boot task would name the versioned
+ WindowsApps path each update deletes (and right after sign-in Windows
+ refused the launcher that path): `autostart` switches the package's
+ startup task, and says so on a first line the desktop app reads."""
+ monkeypatch.setattr(sys, "platform", "win32")
+ monkeypatch.setattr(plat, "in_store_package", lambda: True)
+ monkeypatch.setattr(plat, "autostart_install", lambda *a: pytest.fail("wrote a launcher"))
+ monkeypatch.setattr(plat, "autostart_remove", lambda *a: pytest.fail("removed a launcher"))
+ monkeypatch.setattr(plat, "service_install", lambda *a: pytest.fail("made a boot task"))
+ monkeypatch.setattr(plat, "service_status", lambda: {"installed": False, "state": ""})
+ asked = []
+ answers = {"enable": "Enabled", "disable": "Disabled", "query": "Disabled"}
+ monkeypatch.setattr(plat, "startup_task", lambda a: asked.append(a) or answers[a])
+
+ class Args:
+ config = None
+ subcommand = "install"
+ for sub, action, state in (("install", "enable", "Enabled"), ("remove", "disable", "Disabled"),
+ ("status", "query", "Disabled")):
+ Args.subcommand = sub
+ lifecycle.autostart(Args)
+ assert asked[-1] == action
+ assert capsys.readouterr().out.splitlines()[0] == f"startup task {state}"
+
+ # Switched off by the user in Windows Settings: theirs to switch back on.
+ answers["enable"] = "DisabledByUser"
+ Args.subcommand = "install"
+ with pytest.raises(SystemExit) as e:
+ lifecycle.autostart(Args)
+ assert e.value.code == 1
+ assert "Settings > Apps > Startup" in capsys.readouterr().out
+
+ # At boot is the .exe installer's.
+ with pytest.raises(SystemExit) as e:
+ lifecycle.service(Args)
+ assert e.value.code == 1
+ assert ".exe" in capsys.readouterr().out
+
+
+def test_a_refused_action_stays_on_the_node_page_until_the_next_action():
+ """The status poll (every five seconds) cleared the one error state there
+ was, so a refusal vanished before it was read."""
+ page = _js(STATIC / "node-page.js")
+ panel = page.split("function NodeServicePanel(", 1)[1].split("\nfunction ", 1)[0]
+ refresh = panel.split("const refresh = useCallback(", 1)[1].split("}, []);", 1)[0]
+ act = panel.split("const act = useCallback(", 1)[1].split("}, [", 1)[0]
+ assert "setActErr" not in refresh and "setPollErr('')" in refresh
+ assert "setActErr(platform.bridgeMessage(e))" in act
+ assert "const err = actErr || pollErr;" in panel
+
+
+def test_the_startup_task_is_refused_outside_the_store_package(monkeypatch):
+ monkeypatch.setattr(plat, "in_store_package", lambda: False)
+ with pytest.raises(RuntimeError, match="Microsoft Store package"):
+ plat.startup_task("query")
+ with pytest.raises(ValueError):
+ plat.startup_task("toggle")
+
+
# ── 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():