mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
CI has been failing on unstable for weeks, across four unit files and a set of e2e specs. None of it was a product regression: every failure is a test that encodes behavior a later commit deliberately changed. Each is corrected against the spec that superseded it, not adjusted until it passed. Unit: - video-modal-moderation: the fixture set flat `blurThumbnail`/`trustedMuteCount`, but normalizeVideoModerationContext derives shouldShow from `original.blurThumbnail`/`trustedCount`, so the assertion ran against a context whose early return had just deleted the blur it was checking.4525c1c6also made a lone trusted mute a non-blocking ranking signal (its sibling test was renamed to say exactly that; this one was missed). Fixture now mirrors what moderationDecorator emits. - trustBootstrap: asserted a subscription to onWhitelistChange that18e08bf2deliberately removed -- buildTrustedSeeds derives seeds from the super admin, editors and fallback seeds, so a whitelist change cannot alter them. The removal is now pinned so it is not reinstated by reflex. - moderation-service:18e08bf2removed `status.whitelisted ||` from isTrustedReporter (being allowed to publish is not a statement that your reports carry weight). A whitelisted reporter now counts toward the report total but not toward trusted. - playbackService_forcedSource: forced P2P now falls back to a hosted URL when the torrent definitively fails -- playbackService documents the change and its rollback. The old assertion did not merely fail, it HUNG: forced sources skip the start timeout, so the now-reachable hosted attempt waited forever on a <video> that never emits. E2E: - ui-modals: asserted exactly one `.bv-modal` per component page, but profile-modal.html also bundles #storageCorsModal (d7ad1516). Requires at least one and drives the primary, which is what the test's own injected querySelector already used. - coverage-utility-modules: used `author:abc123`, which9afbc0cbcorrectly began rejecting as undecodable. Uses a real pubkey for the success path and pins the rejection separately. - webseed-playback: asserted the FINAL src equals the CDN URL while also tolerating "No playable source found" in its status check -- contradictory, since a failed attempt calls resetVideoElement() and clears src. Headless CI cannot decode the archive.org H.264 sample. Now records src attachments and asserts the URL was attached, which is the behavior under test. - nostr-runtime-coverage: asserted a legacy plaintext session-actor entry reads back as null. Since8692dac6added per-account storage, readV1Entry PURGES the plaintext entry and the reader falls back to the sole remaining encrypted account -- so null no longer describes the security property. Asserts the property directly: no plaintext key survives, in the entry or in raw storage. Also drops the syncAuthUiState share-gate wiring added ind141c316. It was redundant: Application.updateShareNostrAuthState() already fires on init, login, logout, pubkey-change and signer-change and forwards to videoModal.setShareNostrAuthState(). That call short-circuited on its own `if (!this.videoModal?.setShareNostrAuthState) return;` guard purely because the method did not exist -- adding it (d141c316) was the whole fix. The tests now pin that contract instead of a second, competing code path. All 416 unit files, all three CI shards, both DM suites and lint are green. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
import { describe, test, mock, before, after, beforeEach, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createUiDom } from "../ui/helpers/jsdom-test-helpers.mjs";
|
|
import { PlaybackService } from "../../js/services/playbackService.js";
|
|
|
|
describe("PlaybackService Forced Source Logic", () => {
|
|
let dom;
|
|
let document;
|
|
let window;
|
|
let service;
|
|
let video;
|
|
let playMock;
|
|
let loadMock;
|
|
let pauseMock;
|
|
|
|
before(() => {
|
|
dom = createUiDom();
|
|
window = dom.window;
|
|
document = dom.document;
|
|
|
|
if (!window.HTMLMediaElement) {
|
|
window.HTMLMediaElement = {
|
|
HAVE_NOTHING: 0,
|
|
HAVE_METADATA: 1,
|
|
HAVE_CURRENT_DATA: 2,
|
|
HAVE_FUTURE_DATA: 3,
|
|
HAVE_ENOUGH_DATA: 4,
|
|
};
|
|
}
|
|
});
|
|
|
|
after(() => {
|
|
dom.cleanup();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mock.timers.enable({ apis: ['setTimeout'] });
|
|
service = new PlaybackService({
|
|
logger: () => {},
|
|
isValidMagnetUri: () => true,
|
|
playbackStartTimeout: 100,
|
|
});
|
|
video = document.createElement("video");
|
|
|
|
playMock = mock.fn(async () => {});
|
|
loadMock = mock.fn();
|
|
pauseMock = mock.fn();
|
|
|
|
video.play = playMock;
|
|
video.load = loadMock;
|
|
video.pause = pauseMock;
|
|
|
|
Object.defineProperty(video, 'readyState', {
|
|
writable: true,
|
|
value: window.HTMLMediaElement.HAVE_NOTHING
|
|
});
|
|
Object.defineProperty(video, 'networkState', {
|
|
writable: true,
|
|
value: 0
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
mock.timers.reset();
|
|
});
|
|
|
|
test("Normal flow: Timeout triggers fallback to Torrent if URL stalls", async () => {
|
|
const probeUrl = mock.fn(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
return { outcome: "good", status: 200 };
|
|
});
|
|
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
|
|
|
const session = service.createSession({
|
|
url: "https://example.com/video.mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
});
|
|
|
|
const startPromise = session.start();
|
|
|
|
// Tick enough to trigger timeout (100ms) but not enough to resolve probeUrl (200ms)
|
|
mock.timers.tick(150);
|
|
// await microtasks to ensure timeout callback runs
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
|
|
const result = await startPromise;
|
|
|
|
assert.equal(result.source, "torrent");
|
|
assert.equal(playViaWebTorrent.mock.callCount(), 1);
|
|
});
|
|
|
|
test("Forced Source 'url': Ignores playbackStartTimeout and does NOT fallback to Torrent", async () => {
|
|
const probeUrl = mock.fn(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
return { outcome: "good", status: 200 };
|
|
});
|
|
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
|
|
|
const session = service.createSession({
|
|
url: "https://example.com/video.mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
forcedSource: "url",
|
|
});
|
|
|
|
const startPromise = session.start();
|
|
|
|
// Advance time past the 100ms default timeout
|
|
mock.timers.tick(150);
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
|
|
// The promise should NOT be resolved yet (still probing)
|
|
// We can't easily check 'pending' state without race, but we assume it didn't return 'torrent'.
|
|
|
|
// Advance time to complete probeUrl (200ms total)
|
|
mock.timers.tick(100);
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
|
|
// Now trigger playing
|
|
video.dispatchEvent(new window.Event("playing"));
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
|
|
const result = await startPromise;
|
|
|
|
assert.equal(result.source, "url");
|
|
assert.equal(playViaWebTorrent.mock.callCount(), 0);
|
|
});
|
|
|
|
test("Hosted playback clears crossOrigin so a CORS-less CDN URL still plays", async () => {
|
|
// Simulate a leftover crossOrigin from a prior WebTorrent attempt on the
|
|
// reused <video> element. Direct hosted playback must not require CORS, or
|
|
// CDNs/redirect targets without Access-Control-Allow-Origin fail the load
|
|
// with MEDIA_ERR_SRC_NOT_SUPPORTED ("source not supported or blocked").
|
|
video.crossOrigin = "anonymous";
|
|
video.setAttribute("crossorigin", "anonymous");
|
|
|
|
const probeUrl = mock.fn(async () => ({ outcome: "good", status: 200 }));
|
|
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
|
|
|
const session = service.createSession({
|
|
url: "https://archive.org/download/x/My Video (1080p).mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
forcedSource: "url",
|
|
});
|
|
|
|
const startPromise = session.start();
|
|
mock.timers.tick(50);
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
|
|
// By the time the hosted source is assigned, CORS must be cleared.
|
|
assert.equal(video.crossOrigin, null, "crossOrigin property cleared");
|
|
assert.equal(
|
|
video.getAttribute("crossorigin"),
|
|
null,
|
|
"crossorigin attribute removed",
|
|
);
|
|
|
|
video.dispatchEvent(new window.Event("playing"));
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
const result = await startPromise;
|
|
assert.equal(result.source, "url", "hosted URL plays");
|
|
assert.equal(playViaWebTorrent.mock.callCount(), 0, "no torrent fallback");
|
|
});
|
|
|
|
test("CDN-first: a torrent-first instance still plays the URL first when one exists", async () => {
|
|
// Even with a torrent-first instance default, a video that ALSO has a hosted
|
|
// URL should play the URL immediately (instant/reliable) instead of waiting
|
|
// on a P2P cold start. WebTorrent is reserved for URL-less videos.
|
|
const torrentFirstService = new PlaybackService({
|
|
logger: () => {},
|
|
isValidMagnetUri: () => true,
|
|
playbackStartTimeout: 100,
|
|
urlFirstEnabled: false, // torrent-first instance
|
|
});
|
|
|
|
const probeUrl = mock.fn(async () => ({ outcome: "good", status: 200 }));
|
|
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
|
|
|
const session = torrentFirstService.createSession({
|
|
url: "https://cdn.example.com/v.mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
});
|
|
|
|
const startPromise = session.start();
|
|
mock.timers.tick(10);
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
video.dispatchEvent(new window.Event("playing"));
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
const result = await startPromise;
|
|
|
|
assert.equal(
|
|
result.source,
|
|
"url",
|
|
"URL plays first despite a torrent-first instance",
|
|
);
|
|
assert.equal(
|
|
playViaWebTorrent.mock.callCount(),
|
|
0,
|
|
"WebTorrent is NOT attempted when a hosted URL exists",
|
|
);
|
|
});
|
|
|
|
test("Forced Source 'url': Does NOT fallback to Torrent even if URL fails (probe bad)", async () => {
|
|
const probeUrl = mock.fn(async () => {
|
|
return { outcome: "bad", status: 404 };
|
|
});
|
|
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
|
|
|
const session = service.createSession({
|
|
url: "https://example.com/video.mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
forcedSource: "url",
|
|
});
|
|
|
|
const result = await session.start();
|
|
|
|
// It should have tried URL, failed, and returned error/null, skipping torrent fallback
|
|
assert.equal(result.source, null); // Or maybe 'url' with error? No, execute returns { source: null, error } if everything fails.
|
|
assert.equal(playViaWebTorrent.mock.callCount(), 0);
|
|
});
|
|
|
|
test("Forced Source 'torrent': Ignores playbackStartTimeout and does NOT fallback to URL", async () => {
|
|
const probeUrl = mock.fn(async () => ({ outcome: "good" }));
|
|
|
|
// Mock torrent to take longer than timeout
|
|
const playViaWebTorrent = mock.fn(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
return { infoHash: "123" };
|
|
});
|
|
|
|
const session = service.createSession({
|
|
url: "https://example.com/video.mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
forcedSource: "torrent",
|
|
});
|
|
|
|
const startPromise = session.start();
|
|
|
|
// Advance time past default timeout
|
|
mock.timers.tick(150);
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
|
|
// Complete torrent init
|
|
mock.timers.tick(100);
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
|
|
const result = await startPromise;
|
|
|
|
assert.equal(result.source, "torrent");
|
|
assert.equal(probeUrl.mock.callCount(), 0);
|
|
});
|
|
|
|
test("Forced Source 'torrent': falls back to the hosted URL when the torrent definitively fails", async () => {
|
|
// Spec changed deliberately in playbackService.js: "Even when P2P was
|
|
// explicitly forced, a hosted URL is a better outcome than a hard 'No
|
|
// playable source found' if the torrent DEFINITIVELY failed." The code
|
|
// carries the rollback note (`restore && forcedSource !== "torrent"` to
|
|
// make forced P2P hard-fail), so the old assertion here -- source null,
|
|
// probe never called -- encodes behavior that was intentionally removed.
|
|
//
|
|
// It did not merely fail: because forced sources skip the start timeout,
|
|
// the now-reachable hosted attempt waited forever on a <video> that never
|
|
// emits, and the test hung ("Promise resolution is still pending").
|
|
const probeUrl = mock.fn(async () => ({ outcome: "good", status: 200 }));
|
|
|
|
const playViaWebTorrent = mock.fn(async () => {
|
|
throw new Error("Torrent failed");
|
|
});
|
|
|
|
const session = service.createSession({
|
|
url: "https://example.com/video.mp4",
|
|
magnet: "magnet:?xt=urn:btih:123",
|
|
videoElement: video,
|
|
probeUrl,
|
|
playViaWebTorrent,
|
|
forcedSource: "torrent",
|
|
});
|
|
|
|
const startPromise = session.start();
|
|
|
|
// Let the torrent attempt reject and the hosted fallback attach, then
|
|
// signal that the element actually started playing.
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
mock.timers.tick(10);
|
|
await new Promise(resolve => process.nextTick(resolve));
|
|
video.dispatchEvent(new window.Event("playing"));
|
|
|
|
const result = await startPromise;
|
|
|
|
assert.equal(playViaWebTorrent.mock.callCount(), 1, "P2P is still tried first");
|
|
assert.equal(result.source, "url", "a working CDN beats a hard failure");
|
|
});
|
|
});
|