From 9aaf8aa4d5a572ee2eda52f22ea72a043501f5ec Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:07:31 +0000 Subject: [PATCH] fix(webtorrent): ensure webseed-only streams are marked healthy Previously, `probePeers` would mark a stream as unhealthy (healthy: false) if no P2P peers were found, even if a valid webseed was present. This caused the UI to potentially disable playback or show error states for webseed-only content. This fix ensures that if `peers === 0` but `hasWebSeed` is true, the probe result returns `healthy: true` with reason `"webseed"`. Added `tests/webtorrent-regression.test.mjs` to verify this behavior and prevent future regressions. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/webtorrent.js | 14 +++-- tests/webtorrent-regression.test.mjs | 90 ++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 tests/webtorrent-regression.test.mjs diff --git a/js/webtorrent.js b/js/webtorrent.js index d4f8698d..b9cad53a 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -346,23 +346,25 @@ export class TorrentClient { torrent.once("error", (err) => { const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); + const webseedOnly = peers === 0 && hasWebSeed; finalize({ - healthy: false, - reason: "error", + healthy: webseedOnly, + reason: webseedOnly ? "webseed" : "error", error: toError(err), peers, - webseedOnly: peers === 0 && hasWebSeed, + webseedOnly, }); }); if (safeTimeout > 0) { timeoutId = setTimeout(() => { const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); + const webseedOnly = peers === 0 && hasWebSeed; finalize({ - healthy: false, + healthy: webseedOnly, peers, - reason: "timeout", - webseedOnly: peers === 0 && hasWebSeed, + reason: webseedOnly ? "webseed" : "timeout", + webseedOnly, }); }, safeTimeout); } diff --git a/tests/webtorrent-regression.test.mjs b/tests/webtorrent-regression.test.mjs new file mode 100644 index 00000000..737d896b --- /dev/null +++ b/tests/webtorrent-regression.test.mjs @@ -0,0 +1,90 @@ + +import { test, describe, it, after, before } from "node:test"; +import assert from "node:assert"; +import EventEmitter from "events"; +import { TorrentClient } from "../js/webtorrent.js"; + +// Mock WebTorrent class +class MockWebTorrent extends EventEmitter { + constructor() { + super(); + this.torrents = []; + this.setMaxListeners = () => {}; + } + + add(magnet, opts, cb) { + const torrent = new MockTorrent(magnet, opts); + this.torrents.push(torrent); + + setTimeout(() => { + if (typeof cb === 'function') cb(torrent); + this.emit('torrent', torrent); + }, 10); + + return torrent; + } + + destroy(cb) { + if (typeof cb === 'function') cb(); + } +} + +class MockTorrent extends EventEmitter { + constructor(magnet, opts) { + super(); + this.magnet = magnet; + this.opts = opts; + this.numPeers = 0; + this.files = []; + } + + destroy(opts, cb) { + if (typeof cb === 'function') cb(); + } +} + +describe("WebTorrent Regression Tests", () => { + before(() => { + if (typeof global.window === "undefined") { + global.window = {}; + } + if (typeof global.window.location === "undefined") { + global.window.location = { hostname: "localhost" }; + } + if (typeof global.navigator === "undefined") { + global.navigator = { userAgent: "node" }; + } + }); + + it("probePeers should report healthy if webseed is present even with 0 peers", async () => { + const client = new TorrentClient({ webTorrentClass: MockWebTorrent }); + const magnet = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567"; + const webSeedUrl = "http://localhost:8080/video.mp4"; + + const result = await client.probePeers(magnet, { + timeoutMs: 50, // Short timeout + urlList: [webSeedUrl] + }); + + assert.strictEqual(result.webseedOnly, true, "Should detect webseed only scenario"); + assert.strictEqual(result.healthy, true, "Should be healthy due to webseed"); + assert.strictEqual(result.reason, "webseed", "Reason should be 'webseed'"); + }); + + it("probePeers should report unhealthy if no webseed and 0 peers", async () => { + const client = new TorrentClient({ webTorrentClass: MockWebTorrent }); + const magnet = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567"; + + // Adding tracker to avoid "no-trackers" immediate return + const magnetWithTr = magnet + "&tr=wss://tracker.example.com"; + + const result = await client.probePeers(magnetWithTr, { + timeoutMs: 50, + urlList: [] + }); + + assert.strictEqual(result.webseedOnly, false, "Should not be webseed only"); + assert.strictEqual(result.healthy, false, "Should be unhealthy"); + assert.strictEqual(result.reason, "timeout", "Reason should be 'timeout'"); + }); +});