aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_task_lifetime.py
Commit message (Collapse)AuthorAgeFilesLines
* test: read the WebRTC transport's source as a set of filesChristophe Besson29 hours1-35/+16
| | | | | | | | | Source-reading tests take their text from node_source (node) and node_tree (hub): webrtc_server.py plus anything under transport/webrtc/, so a check for something's absence keeps reading the code it guards if that code moves. test_node_source_scope holds the boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: hold every background task, in both codebasesChristophe Besson12 days1-0/+8
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | asyncio keeps only a weak reference to a task, so a coroutine started with `asyncio.ensure_future(...)` whose result is discarded can be collected while it is still running: the loop logs "Task was destroyed but it is pending!" and the work simply does not happen. No error reaches the caller, and what is lost is whatever that coroutine was in the middle of. The node already had a guard for this, written after an abandoned stream task lost a transcode slot for good — and it read one file, `webrtc_server.py`, because that is where the defect was found. Outside that file there were nineteen sites: the hub's `chat_notify` (a notification for every member of a group), the indexer's debounce (every real-time index update), eleven in `daemon.py` including the SIGHUP reload and each enrichment pass, two in `ops.py`, and five in the loopback API. `meshbay_common.background.spawn()` is the one door. It holds the task, drops it when it finishes, and logs what it raised under the coroutine's own name — an exception in a task nobody awaits was otherwise reported by asyncio at collection time, out of context or not at all. A peer session's `_spawn` stays as it is: that one can also *cancel* what it holds, which a module-level holder cannot, because a session ends and a process does not. `test_background_tasks.py` walks every package's source and refuses a discarded handle. It parses rather than greps, so an assignment, a comprehension or an await is not mistaken for one, and it was checked against a deliberate reintroduction. A guard that stops at the edge of the file where the bug was found is a guard against that bug, not against its class. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW
* refactor(mnp)!: remove stream_seg, the last unencrypted content messageChristophe Besson2026-09-071-2/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | `stream_seg` answered with an MPEG-TS segment as base64 with no encryption at all — the one message on the content plane that never went through a GEK-derived key. Live on both transports, answering any authenticated member. It predates `stream_data`, which does the same job properly (`chunk_ciphertext`, keyed per segment, AES-256-GCM) and has since Phase 12. Its only browser caller, `fetchStreamSegment`, was defined and never once invoked — a plaintext media endpoint with no client. Removed rather than repaired. Gone with it: `_extract_segment` and the ffmpeg semaphore in quic_server, the `fetch_stream_segment` QUIC client method, and `_b64decode` in transport.js, which had no other caller. The H6 regression test lived on this handler — it pinned `_do_stream_segment_async` to a coroutine so `subprocess.run` could not stall the event loop for thirty seconds per request. It is replaced by the property that outlives the handler: no transport carries media outside an AEAD, asserted on `stream_seg` and `data_b64` across all three transport modules. The half of H6 that survives — the live streaming path still spawns ffmpeg — keeps its own test. BREAKING CHANGE: `stream_seg` is no longer answered on either transport. No shipping client sends it. Recorded as part of MNP 2.0, whose other half — the sealed upload — carries the version bump. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
* test(node): make the suite pass on WindowsChristophe Besson2026-09-041-1/+1
| | | | | | | | | | | | | | | | | | | - `.read_text()` on source files now `encoding="utf-8"` — cp1252 chokes on the em dashes / box-drawing chars those files contain. - test node.toml templates embed paths via `Path.as_posix()`: a raw Windows path in a basic TOML string is a parse error (`\U`, `\a`, ... are escapes). - new `test_platform.py` covers `meshbay_node.platform` by mocking `sys.platform` / `os.environ` — runs on both OSes. - `skipif(sys.platform == "win32")`, in `conftest.needs_subprocess` and inline, for the documented gaps: ffmpeg/ffprobe via asyncio subprocess (the win32 selector loop, forced for aiortc, cannot spawn one), the systemd `reload`/`restart-daemon` delegation (Windows path is W3), the keystore `st_mode == 600` assertion (NTFS ignores mode bits), and the symlink-escape test (needs Developer Mode). Windows: 781 passed, 25 skipped. No change on Linux. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: update source-reading tests that drifted from the codeChristophe Besson2026-08-221-1/+1
| | | | | | | | | | | | | - test_transport_contracts: CreateGroupPage was refactored into a routing wrapper; assertions now read CreateGroupFormSimple - test_task_lifetime: _spawn now uses an _on_done wrapper instead of a bare self._tasks.discard callback; assertion checks both parts - test_video_buffer_ceiling: target the real updateend handler, not the settled() utility; add awaitingInitRef to the MSE harness scope - test_video_seek: silence debug console.log in window_leak harness so it does not pollute the JSON output Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(node): the stream capacity is the operator's to set, and a log that ↵Christophe Besson2026-08-161-8/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | says who stopped Bounding the client's read-ahead changed what a transcode slot is. It used to be a burst — the browser took segments as fast as it could append them, so a slot came back within the minute whatever the length of the film. Now it is held for as long as someone is watching, so the cap counts simultaneous viewers, and two of them meant the third was refused for the next hour and a half. The right number depends on the machine, so it belongs to the operator: `[node] max_concurrent_streams` in node.toml, or MESHBAY_MAX_CONCURRENT_STREAMS. Default 8 — one ffmpeg per viewer, remuxing rather than encoding, idle on a pipe for most of the film. Zero, a negative number, a non-number and a bool are refused with a warning naming the setting: `Semaphore(0)` is not "no limit", it is a node where no video ever plays and nothing says why, and TOML `true` would have become 1 by way of `int()`. A stream also ends on the peer's silence now rather than on its stinginess. A viewer buffered well ahead deliberately grants nothing for minutes, and the old budget accumulated over the whole wait, so a keepalive that granted no credit could not keep a paused film alive. The rest is diagnosis, which is what this cost. `client_diag` carries the player's own view — readyState, refused appends, buffered ranges, the video element's error — into the node's log at DEBUG, next to the node's view of the same stream. It is the only window into a phone, and every field is stringified and cut short because all of it is peer-controlled. The node also logs the first keepalive, which distinguishes a paced client from an unpaced one at a glance, and progress every hundred segments, whose last line says where a stream stopped and which side stopped it.
* fix(node): stop losing transcode slots, and reap ffmpeg without deadlockingChristophe Besson2026-08-161-0/+256
Reported from a phone: play a video, close the viewer, open another — the second hangs and the third is refused. Three separate causes, found by instrumenting rather than guessing, after two fixes that addressed real but different bugs. A task nobody holds can be collected mid-flight. asyncio keeps only a weak reference, so `ensure_future` with the result discarded may be garbage-collected while running — "Task was destroyed but it is pending!" — and `_stream_video` never reached the exit of its `async with sem`. `_spawn` holds every background task; all nineteen call sites go through it. Losing the peer must stop its work. The connectionstatechange handler popped the session from a dict and nothing else, so a closed tab went on transcoding for the full 120 s credit timeout. Measured in the log: 91 s of ffmpeg after the connection closed. `shutdown_tasks()` now runs on the way out, and the credit wait checks the channel before sleeping and polls in slices instead of once. And `await proc.wait()` after `kill()` still deadlocks. ffmpeg outruns a credit-paced viewer and fills the stdout pipe; stop reading it and the transport cannot finish closing, SIGKILL or not. Measured against the live node with a 169 MB video, closing the viewer after 20 segments and asking for the next one: 15.1 s then "Server busy" before, 0.1 s / 0.0 s / 0.0 s after. Chunk replies wait for room on the channel. Eight megabyte-sized chunks answered as they arrived queued 8 MB with nothing watching — measured at 7.3 MB of bufferedAmount in milliseconds. Fine on a LAN, minutes of head-of-line delay on a busy link. Upload names accept any script. The rule was ASCII-only, so `été.txt` was refused — and so was `rapport (1).pdf`, which is the form `_free_name` produces itself, meaning the node rejected names it had chosen. Widened to Unicode with the C5a and H2 protections intact, plus a refusal of names that lie about themselves: trailing space or dot, and the right-to-left override. Errors now name the file, so one bad name no longer fails every upload in flight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>