<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/tests/test_platform.py, branch 0.20</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.20</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.20'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-10-10T07:57:29Z</updated>
<entry>
<title>feat(packaging): the Store package declares what the NSIS scripts do</title>
<updated>2026-10-10T07:57:29Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-10-09T16:19:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=42b562fcf312ddceb78438b5daea49d4c9b465c8'/>
<id>urn:sha1:42b562fcf312ddceb78438b5daea49d4c9b465c8</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): prefer the bundled ffmpeg over PATH in the frozen build</title>
<updated>2026-10-07T07:41:18Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-10-07T07:41:18Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c1f9b0e324c2c47d832e21e5086f09e000d77eb5'/>
<id>urn:sha1:c1f9b0e324c2c47d832e21e5086f09e000d77eb5</id>
<content type='text'>
shutil.which never looks beside meshbay-node.exe, and node-runtime is
appended to the user PATH, so any earlier ffmpeg.exe on PATH ran instead
of the pinned copy. Log the resolved media tools at startup.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): a Windows daemon that stops properly, starts honestly and runs once</title>
<updated>2026-09-27T20:20:35Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-27T20:20:35Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=7662484cae8e74b7d9aa383bd6cd0dad4690aadc'/>
<id>urn:sha1:7662484cae8e74b7d9aa383bd6cd0dad4690aadc</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): read the packaged TMDB token in place</title>
<updated>2026-09-26T10:23:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-26T10:23:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=fc761e7df40eac828d4e9858fab56958078c928b'/>
<id>urn:sha1:fc761e7df40eac828d4e9858fab56958078c928b</id>
<content type='text'>
A node onboarded by the desktop client never ran `init`, so default.env was
never copied to node.env; and default.env was 0600 root, unreadable to a
per-user node anyway. The daemon now loads it beneath node.env, 0644.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>style: ruff's own fixes, mechanically applied</title>
<updated>2026-09-19T12:24:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T12:24:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=86188385cbdae1ee90c1dca7a7b9db2edef1ecd4'/>
<id>urn:sha1:86188385cbdae1ee90c1dca7a7b9db2edef1ecd4</id>
<content type='text'>
`ruff check .` had gone unrun long enough to report 568 errors, which is the
same as having no linter: the next real finding would have been invisible in the
noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the
98 it cannot fix are the next commit.

What actually changed: import sorting (225), imports nobody used (87, none of
them a re-export — no `__init__.py` is touched, which was the one way this could
have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69)
and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12
this project requires, `Optional[X]` to `X | None` (24), and f-strings with
nothing to interpolate (19).

Checked rather than assumed: every module in the three packages still imports,
and the suite is 2893 passed — the same count, test for test, as the merge
before it.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>test(node): close the eleven failures, and the order-dependence behind seven</title>
<updated>2026-09-08T11:54:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T11:54:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=28f1b5686c7ab200aeda6782f5f6e829c24759dd'/>
<id>urn:sha1:28f1b5686c7ab200aeda6782f5f6e829c24759dd</id>
<content type='text'>
Nine of the eleven were defects in the suite, two were assertions describing
behaviour the code had deliberately changed. None was a bug in the node.

Seven had one cause. `check_media_tools()` writes two module globals;
`monkeypatch` restores what a test patched and knows nothing about what the
call under test then wrote, so a test that pointed `shutil.which` at
"/opt/bin/{n}.exe" left `_ffprobe_path` there — a Windows path, on Linux — for
the rest of the session. Every later test that actually runs ffprobe died on
FileNotFoundError, in two files about video transcoding, for a reason nowhere
near themselves. Run those files alone and they passed; that is what made it
look like an environment problem for so long.

The autouse `_restore_media_tool_paths` fixture in conftest.py puts both back
after every test. That closes the class, not just this instance: any future test
that resolves media tools is undone whether it remembers to or not, which is the
only way an order-dependent suite stops being one. Verified by removing the
call-site guard entirely and running the whole suite — green, so the fixture is
carrying it, and the call site keeps a pointer rather than a second copy of the
explanation.

The other four:

- two service tests were the only ones in test_platform.py that never set
  `sys.platform` to "win32", so they hit "service mode is Windows-only";
- test_apps_enabled_policy expected `["chat"]` where `roster.enabled_apps`
  inserts "files" at the front on read (and `ops.set_enabled_apps` on write),
  because Settings is the one way back if every app were turned off. The code
  is right; the assertion predates the guard, and is now ["files", "chat"];
- test_invite_then_join_delivers_the_gek passed a bare Path as a group's
  `roots` two lines below building a RootSet for the transport. The handshake
  died on `'PosixPath' object has no attribute 'describe'` and answered `error`
  — scaffolding that never followed the move to several named roots (draft v6,
  change 1).

1081 passed, 4 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(node): register the service-mode task with Register-ScheduledTask -LogonType S4U</title>
<updated>2026-09-05T12:48:08Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-05T12:48:08Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c11dd22b593358ef7932deec53c8200f5f14ed8b'/>
<id>urn:sha1:c11dd22b593358ef7932deec53c8200f5f14ed8b</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(win): graceful shutdown, one startup-mode control, and a stray-\r bug</title>
<updated>2026-09-05T11:06:55Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-05T11:06:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e89a57bb97b5a0d624e8d490b6b8aa38ba140817'/>
<id>urn:sha1:e89a57bb97b5a0d624e8d490b6b8aa38ba140817</id>
<content type='text'>
Windows-only changes, all found by actually running the previous session's
work rather than by review alone:

- CTRL_CLOSE_EVENT/LOGOFF/SHUTDOWN handler (platform.py, ctypes
  SetConsoleCtrlHandler) so closing a console window, signing off, or a
  system shutdown runs the daemon's real _shutdown() instead of Windows
  just ending the process — closing WebRTC sessions and any in-flight
  ffmpeg transcode instead of orphaning it. `taskkill /F` itself stays
  uncatchable (like SIGKILL), so autostart_run() now spawns with
  CREATE_NEW_PROCESS_GROUP instead of DETACHED_PROCESS and autostart_end()
  tries CTRL_BREAK_EVENT against the recorded pid first, falling back to
  the hard kill only if that doesn't stop it in time.

- Replaced the Node page's two independent autostart/service-mode toggles
  with one "start automatically" select (off / at sign-in / as a
  background service). The old pair let both be active at once — starting
  the daemon twice, at boot and at sign-in — and their layout broke
  wrapping inside .node-service's flex row. The new control always removes
  whichever mechanism is active before installing the target; platform.py's
  service_install() does the same on the CLI side. The "background
  service" option disables itself (with a hint pointing at the CLI) when
  running unpackaged, since service-mode.ps1/service.ps1/firewall.ps1 all
  assume an installed build's layout — verified live rather than assumed
  by actually running those scripts unelevated.

- findNodeBinary() no longer bakes a stray \r into resolved paths. Found by
  rebooting after enabling per-user autostart: where.exe listed two
  matches, and stdout.trim().split('\n')[0] only strips the whole string's
  ends, leaving line one's own trailing \r attached — which landed inside
  the Startup .vbs's quoted path and broke it with "Unterminated string
  constant" at boot. Fixed by splitting on \r?\n and trimming every line.

- Dependency audit for the Windows installer (docs/WINDOWS-PORT.md): no
  VC++ Redistributable needed, confirmed by inspecting the built
  node-runtime's actual import table rather than assuming. New
  docs/windows-build.md: a concise clone-to-installer build guide.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(packaging): actually ship the TMDB token, on Linux and Windows</title>
<updated>2026-09-04T12:33:33Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T12:33:33Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c2eade6db582966fa7fc3dd037f952baf3ae1cb5'/>
<id>urn:sha1:c2eade6db582966fa7fc3dd037f952baf3ae1cb5</id>
<content type='text'>
default.env was empty in every build, for three independent reasons:

1. build-node.sh read QE/node.env, which does not exist. Even pointed at the
   real file it would have failed: its `grep MESHBAY_TMDB_DEFAULT_TOKEN=`
   cannot match QE/tmdb.txt, which is a free-form note, not KEY=VALUE.

2. Nothing consumed default.env. packaging/README.md and build-node.sh both
   claimed `meshbay-node init` copies it to &lt;config&gt;/node.env; grep found the
   name in exactly two places, the README and the script that writes it. No
   code implemented the copy, and `EnvironmentFile=-` hid the absence.

3. build-win.ps1 had no env handling at all, so Windows was empty for a
   different reason than Linux.

Now: the build extracts the v4 read token -- tmdb.py sends `Authorization:
Bearer`, so it is the JWT, not the 32-char v3 key beside it in the same file --
matching KEY=VALUE first and then by shape, from MESHBAY_TMDB_TOKEN,
MESHBAY_TMDB_TOKEN_FILE, QE/node.env, QE/tmdb.txt. It writes default.env 0600
and *fails the build* if no token resolves; MESHBAY_ALLOW_NO_TMDB=1 opts out.
An empty default.env is invisible until a user opens Videos and finds no
metadata, which is how this shipped empty on two platforms at once.

platform.py gains packaged_default_env()/install_node_env()/load_node_env().
init copies the packaged file once, never overwriting an existing node.env,
and the daemon loads node.env itself at startup: systemd does this on Linux
via EnvironmentFile, but Windows autostart is a Startup-folder .vbs with no
equivalent. Already-set variables always win.

Also fixes an UnboundLocalError in main(): `config_dir` was assigned at the
top of the init branch, which made it function-local for all of main(), while
the reset branch calls `config_dir()` as the imported function. init returns
before that line, so `meshbay-node reset` could only ever raise. The local is
now cfg_dir.

Verified end to end on Linux: token baked (239 chars), init writes
&lt;config&gt;/node.env 0600 with it. The PowerShell half is written but unrun --
no pwsh on this machine.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
</content>
</entry>
<entry>
<title>feat: Windows daemon lifecycle (W3) — Startup-folder autostart</title>
<updated>2026-09-04T01:58:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T01:58:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=220e6e701806213a576ce80fa655cd9cf4a51880'/>
<id>urn:sha1:220e6e701806213a576ce80fa655cd9cf4a51880</id>
<content type='text'>
The Linux node runs under `systemctl --user`. Windows has no per-user
equivalent that works without elevation: `schtasks /create /sc ONLOGON`
(even `/rl LIMITED /it`) fails with "Access is denied" for a non-admin
user, because a logon trigger touches machine-wide scheduler state.

So autostart is a `.vbs` in the per-user Startup folder instead:

    CreateObject("WScript.Shell").Run Chr(34) &amp; "&lt;exe&gt;" &amp; Chr(34), 0, False

wscript runs it at every sign-in, hidden (0) and non-blocking. No admin,
no console window, no new dependency. Verified end to end: the launcher
brings the daemon up with no window and it answers its loopback API.

node/platform.py
  autostart_install/remove/status  — write / delete / detect the launcher
  autostart_run/end                — start now (DETACHED|NO_WINDOW) / taskkill
  _node_exe                        — PATH, then next to sys.executable, then argv[0]

node/daemon.py
  new `autostart install|remove|start|stop|status` verb
  reload (win32)         -&gt; POST /api/reload on the loopback API
  restart-daemon (win32) -&gt; autostart_end + autostart_run
  reset (win32)          -&gt; also removes the launcher

client/main.js, preload.js
  node:autostart handler + winAutostart* helpers (kept in step with platform.py)
  node:service-status (win32) probes the daemon; stop/restart/start use
  taskkill + a detached, windowless spawn

Tests: 8 autostart cases in test_platform.py (mocked sys.platform, APPDATA
pointed at tmp); `autostart status` added to the CLI dispatch sweep. Full
meshbay-node suite green on Windows (784 passed / 34 skipped).

Still open: no CTRL_CLOSE_EVENT handler, so a bare taskkill / window close
does not run _shutdown() (SetConsoleCtrlHandler, follow-up). Service mode
(pywin32/NSSM) stays Phase 2.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
