From c11dd22b593358ef7932deec53c8200f5f14ed8b Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 5 Sep 2026 14:48:08 +0200 Subject: fix(node): register the service-mode task with Register-ScheduledTask -LogonType S4U schtasks.exe has no flag naming the logon type directly -- it only infers S4U vs Interactive from whether /rp is present, and both readings broke live on a blank-password account: /rp "" fails schtasks' own credential validation, and omitting /rp registers "Interactive only", which never launches the process at boot or on demand despite installing cleanly. Register-ScheduledTask -LogonType S4U names the logon type explicitly, no inference. Confirmed live: install, manual start, and unattended boot-time start all now work. Co-Authored-By: Claude Sonnet 5 --- packages/meshbay-node/tests/test_packaging_win.py | 50 ++++++++++++++--------- packages/meshbay-node/tests/test_platform.py | 43 ++++++++++++++++--- 2 files changed, 68 insertions(+), 25 deletions(-) (limited to 'packages/meshbay-node/tests') diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py index 0cf6367..df5a24f 100644 --- a/packages/meshbay-node/tests/test_packaging_win.py +++ b/packages/meshbay-node/tests/test_packaging_win.py @@ -284,29 +284,39 @@ def test_service_ps1_and_service_mode_ps1_are_extraresources(): def test_service_install_uses_s4u_not_a_stored_password(): """ - schtasks /create ... /ru /rp "" with no /it registers an S4U logon: - no password stored anywhere, and — unlike LocalSystem/NetworkService — it - loads this account's own profile, so %LOCALAPPDATA%\\meshbay\\ needs no - relocation. Losing the empty /rp "" (e.g. "fixing" it into a real prompt - for a password) would either store a secret or silently stop working. + Register-ScheduledTask -LogonType S4U: no password stored anywhere, and + — unlike LocalSystem/NetworkService — it loads this account's own + profile, so %LOCALAPPDATA%\\meshbay\\ needs no relocation. + + Not `schtasks /create`: schtasks only *infers* the logon type from + whether /rp is present, and both readings broke live on a blank-password + account (common on a personal PC, 2026-09-05) -- `/rp ""` fails + credential validation ("the user name or password is incorrect", even + though nothing is wrong), and omitting /rp registers "Interactive only" + instead of S4U, which never runs at boot and does not launch anything + even run on demand while signed in. -LogonType S4U is explicit, so losing + it (e.g. "simplifying" back to schtasks, or dropping -ErrorAction Stop so + a permission failure silently falls through to "installed") would + reintroduce one of those two live-reproduced failures. """ src = (WIN / "service.ps1").read_text(encoding="utf-8") - # The prose above the actual command is allowed to say "/it" while - # explaining why it is absent (the same "read the comment, not the - # directive" trap CLAUDE.md already tracks) -- so check the real - # invocation line, not the whole file. - create_line = next( - (line for line in src.splitlines() if line.strip().startswith("& schtasks") - and "/create" in line), None) - assert create_line, "no schtasks /create invocation found" - tokens = create_line.split() - assert "/sc" in tokens and tokens[tokens.index("/sc") + 1] == "onstart", ( - "must trigger at boot, not at sign-in (/sc onlogon)") - assert "/ru" in tokens - assert "/rp" in tokens and tokens[tokens.index("/rp") + 1] == '""', ( - "must pass an empty run-as password (S4U)") - assert "/it" not in tokens, "an interactive-token task would need the user signed in" + # Isolate the actual "install" case from the docstring above it (which + # names these same cmdlets in prose) -- checks below must see only code. + lines = src.splitlines() + install_start = next(i for i, line in enumerate(lines) if '"install" {' in line) + remove_start = next(i for i, line in enumerate(lines) if '"remove" {' in line) + install_block = "\n".join(lines[install_start:remove_start]) + + assert "New-ScheduledTaskTrigger -AtStartup" in install_block, ( + "must trigger at boot, not at sign-in") + assert "-LogonType S4U" in install_block, ( + "must request S4U explicitly, not infer it from /rp") + assert "Register-ScheduledTask" in install_block + assert "-ErrorAction Stop" in install_block, ( + "a permission failure must throw, not fall through as if it installed") assert "Get-Credential" not in src, "no password should ever be prompted for" + assert "schtasks" not in install_block, ( + "the install branch must not fall back to schtasks /create") def test_service_task_name_is_the_same_everywhere(): diff --git a/packages/meshbay-node/tests/test_platform.py b/packages/meshbay-node/tests/test_platform.py index 91713be..92e74df 100644 --- a/packages/meshbay-node/tests/test_platform.py +++ b/packages/meshbay-node/tests/test_platform.py @@ -346,24 +346,57 @@ def test_service_install_removes_the_startup_launcher_first(win_startup, monkeyp monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user") calls = [] monkeypatch.setattr( - plat, "_schtasks", - lambda *args: calls.append(args) or Mock(returncode=0, stdout="", stderr="")) + plat.subprocess, "run", + lambda argv, **kw: calls.append((argv, kw)) or Mock(returncode=0, stdout="", stderr="")) plat.service_install(exe=r"C:\x\meshbay-node.exe") assert not win_startup.exists() # removed as part of service_install - assert calls and calls[0][0] == "/create" + assert calls and calls[0][0][0] == "powershell" + env = calls[0][1]["env"] + assert env["MESHBAY_SVC_USER"] == "DOMAIN\\user" + assert env["MESHBAY_SVC_EXE"] == r"C:\x\meshbay-node.exe" + + +def test_service_install_uses_s4u_not_a_stored_password(monkeypatch): + """Register-ScheduledTask -LogonType S4U, not schtasks: schtasks only + infers the logon type from whether /rp is present, and both readings + broke live on a blank-password account (2026-09-05) -- /rp "" fails + credential validation, and omitting /rp registers "Interactive only", + which never runs at boot or on demand. See platform.py's service mode + comment for the full story.""" + monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user") + calls = [] + monkeypatch.setattr( + plat.subprocess, "run", + lambda argv, **kw: calls.append((argv, kw)) or Mock(returncode=0, stdout="", stderr="")) + + plat.service_install(exe=r"C:\x\meshbay-node.exe") + + script = calls[0][0][-1] + assert "-LogonType S4U" in script + assert "New-ScheduledTaskTrigger -AtStartup" in script def test_service_install_tolerates_no_startup_launcher_present(win_startup, monkeypatch): assert not win_startup.exists() monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user") - monkeypatch.setattr(plat, "_schtasks", - lambda *args: Mock(returncode=0, stdout="", stderr="")) + monkeypatch.setattr(plat.subprocess, "run", + lambda argv, **kw: Mock(returncode=0, stdout="", stderr="")) plat.service_install(exe=r"C:\x\meshbay-node.exe") # no error assert not win_startup.exists() +def test_service_install_raises_with_powershells_error_message(monkeypatch): + monkeypatch.setattr(plat, "_current_user", lambda: "DOMAIN\\user") + monkeypatch.setattr( + plat.subprocess, "run", + lambda argv, **kw: Mock(returncode=1, stdout="", stderr="Access is denied.")) + + with pytest.raises(RuntimeError, match="Access is denied"): + plat.service_install(exe=r"C:\x\meshbay-node.exe") + + # ── Packaged defaults ──────────────────────────────────────────────────────── def test_frozen_build_finds_default_env_beside_the_executable(monkeypatch, tmp_path): -- cgit v1.2.3