mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +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]>
487 lines
15 KiB
JavaScript
487 lines
15 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { ModerationService } from "../js/services/moderationService.js";
|
|
import { USER_BLOCK_EVENTS } from "../js/userBlocks.js";
|
|
|
|
const nip19 = {
|
|
npubEncode(hex) {
|
|
if (typeof hex !== "string" || !hex) {
|
|
throw new Error("invalid hex");
|
|
}
|
|
return `npub${hex}`;
|
|
},
|
|
decode(value) {
|
|
if (typeof value !== "string" || !value.startsWith("npub")) {
|
|
throw new Error("invalid npub");
|
|
}
|
|
return { type: "npub", data: value.slice(4) };
|
|
},
|
|
};
|
|
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
|
|
const noopUserLogger = { info: () => {} };
|
|
|
|
test("trusted report summaries respect personal blocks and admin lists", async (t) => {
|
|
const previousTools = globalThis.NostrTools;
|
|
const previousWindowTools = globalThis.window?.NostrTools;
|
|
|
|
globalThis.NostrTools = { nip19 };
|
|
if (globalThis.window) {
|
|
globalThis.window.NostrTools = globalThis.NostrTools;
|
|
}
|
|
|
|
t.after(() => {
|
|
if (typeof previousTools === "undefined") {
|
|
delete globalThis.NostrTools;
|
|
} else {
|
|
globalThis.NostrTools = previousTools;
|
|
}
|
|
|
|
if (globalThis.window) {
|
|
if (typeof previousWindowTools === "undefined") {
|
|
delete globalThis.window.NostrTools;
|
|
} else {
|
|
globalThis.window.NostrTools = previousWindowTools;
|
|
}
|
|
}
|
|
});
|
|
|
|
const trustedHex = "a".repeat(64);
|
|
const whitelistedHex = "b".repeat(64);
|
|
const blacklistedHex = "c".repeat(64);
|
|
const blockedHex = "d".repeat(64);
|
|
const randomHex = "e".repeat(64);
|
|
const eventId = "f".repeat(64);
|
|
|
|
const userBlocksMock = {
|
|
async ensureLoaded() {},
|
|
isBlocked(pubkey) {
|
|
return pubkey === blockedHex;
|
|
},
|
|
on() { return () => {}; }
|
|
};
|
|
|
|
const accessControlMock = {
|
|
getWhitelist() {
|
|
return [`npub${whitelistedHex}`];
|
|
},
|
|
getBlacklist() {
|
|
return [`npub${blacklistedHex}`];
|
|
},
|
|
};
|
|
|
|
const service = new ModerationService({
|
|
logger: () => {},
|
|
userBlocks: userBlocksMock,
|
|
accessControl: accessControlMock,
|
|
userLogger: noopUserLogger,
|
|
});
|
|
|
|
service.trustedContacts = new Set([trustedHex]);
|
|
|
|
const reports = new Map();
|
|
reports.set(trustedHex, new Map([["nudity", { created_at: 100 }]]));
|
|
reports.set(whitelistedHex, new Map([["nudity", { created_at: 400 }]]));
|
|
reports.set(blacklistedHex, new Map([["nudity", { created_at: 500 }]]));
|
|
reports.set(blockedHex, new Map([["nudity", { created_at: 600 }]]));
|
|
reports.set(randomHex, new Map([["nudity", { created_at: 300 }]]));
|
|
service.reportEvents.set(eventId, reports);
|
|
|
|
service.recomputeSummaryForEvent(eventId);
|
|
|
|
// 18e08bf2 ("refine trust controls") removed `status.whitelisted ||` from
|
|
// isTrustedReporter and updated the doc comment from "trustedContacts set (or
|
|
// Admin Whitelist)" to "trustedContacts set". Being on the admin whitelist is
|
|
// permission to publish, not a statement that your reports carry weight, so a
|
|
// whitelisted reporter now counts toward the report TOTAL but not toward
|
|
// TRUSTED.
|
|
//
|
|
// Counted reporters: trusted (t=100), whitelisted (t=400), random (t=300).
|
|
// Excluded entirely: blacklisted (admin list) and blocked (personal block).
|
|
const summary = service.getTrustedReportSummary(eventId);
|
|
assert.equal(summary.totalTrusted, 1, "only the trusted contact is trusted");
|
|
assert.equal(summary.types.nudity.total, 3, "whitelisted still counts as a report");
|
|
assert.equal(summary.types.nudity.trusted, 1);
|
|
assert.equal(
|
|
summary.types.nudity.latest,
|
|
400,
|
|
"latest still reflects the whitelisted reporter's timestamp"
|
|
);
|
|
|
|
const reporters = service.getTrustedReporters(eventId, "nudity");
|
|
assert.deepEqual(
|
|
reporters.map((entry) => entry.pubkey),
|
|
[trustedHex],
|
|
"the whitelisted reporter is no longer surfaced as a trusted reporter"
|
|
);
|
|
assert.equal(service.trustedReportCount(eventId, "nudity"), 1);
|
|
});
|
|
|
|
test("user block updates recompute summaries and emit notifications", async (t) => {
|
|
const blockedHex = "1".repeat(64);
|
|
const trustedHex = "2".repeat(64);
|
|
const randomHex = "3".repeat(64);
|
|
const eventId = "4".repeat(64);
|
|
|
|
class FakeUserBlocks {
|
|
constructor() {
|
|
this.blocked = new Set();
|
|
this.listeners = new Set();
|
|
}
|
|
|
|
async ensureLoaded() {}
|
|
|
|
isBlocked(pubkey) {
|
|
return this.blocked.has(pubkey);
|
|
}
|
|
|
|
on(eventName, handler) {
|
|
if (eventName !== USER_BLOCK_EVENTS.CHANGE || typeof handler !== "function") {
|
|
return () => {};
|
|
}
|
|
this.listeners.add(handler);
|
|
return () => {
|
|
this.listeners.delete(handler);
|
|
};
|
|
}
|
|
|
|
emitChange(detail) {
|
|
for (const handler of Array.from(this.listeners)) {
|
|
handler(detail);
|
|
}
|
|
}
|
|
}
|
|
|
|
const userBlocks = new FakeUserBlocks();
|
|
const service = new ModerationService({ logger: () => {}, userBlocks, userLogger: noopUserLogger });
|
|
|
|
service.trustedContacts = new Set([blockedHex, trustedHex]);
|
|
|
|
const reports = new Map();
|
|
reports.set(
|
|
blockedHex,
|
|
new Map([["nudity", { created_at: 200 }]]),
|
|
);
|
|
reports.set(
|
|
trustedHex,
|
|
new Map([["nudity", { created_at: 150 }]]),
|
|
);
|
|
reports.set(
|
|
randomHex,
|
|
new Map([["nudity", { created_at: 100 }]]),
|
|
);
|
|
|
|
service.reportEvents.set(eventId, reports);
|
|
service.recomputeSummaryForEvent(eventId);
|
|
|
|
const initialSummary = service.getTrustedReportSummary(eventId);
|
|
assert.equal(initialSummary.totalTrusted, 2);
|
|
assert.equal(initialSummary.types.nudity.total, 3);
|
|
assert.equal(initialSummary.types.nudity.trusted, 2);
|
|
|
|
const initialReporters = service.getTrustedReporters(eventId, "nudity");
|
|
assert.deepEqual(
|
|
initialReporters.map((entry) => entry.pubkey),
|
|
[blockedHex, trustedHex],
|
|
);
|
|
|
|
const blockEvent = new Promise((resolve) => {
|
|
const unsubscribe = service.on("user-blocks", (detail) => {
|
|
unsubscribe?.();
|
|
resolve(detail);
|
|
});
|
|
});
|
|
|
|
userBlocks.blocked.add(blockedHex);
|
|
userBlocks.emitChange({ action: "block", targetPubkey: blockedHex });
|
|
|
|
const blockDetail = await blockEvent;
|
|
assert.equal(blockDetail.action, "block");
|
|
assert.equal(blockDetail.targetPubkey, blockedHex);
|
|
|
|
const afterBlockSummary = service.getTrustedReportSummary(eventId);
|
|
assert.equal(afterBlockSummary.totalTrusted, 1);
|
|
assert.equal(afterBlockSummary.types.nudity.total, 2);
|
|
assert.equal(afterBlockSummary.types.nudity.trusted, 1);
|
|
assert.equal(service.trustedReportCount(eventId, "nudity"), 1);
|
|
|
|
const reportersAfterBlock = service.getTrustedReporters(eventId, "nudity");
|
|
assert.deepEqual(reportersAfterBlock.map((entry) => entry.pubkey), [trustedHex]);
|
|
|
|
const unblockEvent = new Promise((resolve) => {
|
|
const unsubscribe = service.on("user-blocks", (detail) => {
|
|
unsubscribe?.();
|
|
resolve(detail);
|
|
});
|
|
});
|
|
|
|
userBlocks.blocked.delete(blockedHex);
|
|
userBlocks.emitChange({ action: "unblock", targetPubkey: blockedHex });
|
|
|
|
const unblockDetail = await unblockEvent;
|
|
assert.equal(unblockDetail.action, "unblock");
|
|
assert.equal(unblockDetail.targetPubkey, blockedHex);
|
|
|
|
const afterUnblockSummary = service.getTrustedReportSummary(eventId);
|
|
assert.equal(afterUnblockSummary.totalTrusted, 2);
|
|
assert.equal(afterUnblockSummary.types.nudity.total, 3);
|
|
assert.equal(afterUnblockSummary.types.nudity.trusted, 2);
|
|
assert.equal(service.trustedReportCount(eventId, "nudity"), 2);
|
|
|
|
const reportersAfterUnblock = service.getTrustedReporters(eventId, "nudity");
|
|
assert.deepEqual(
|
|
reportersAfterUnblock.map((entry) => entry.pubkey),
|
|
[blockedHex, trustedHex],
|
|
);
|
|
});
|
|
|
|
test("moderation thresholds emit logger hooks only when crossing", async (t) => {
|
|
const calls = [];
|
|
const userLoggerMock = {
|
|
info: (...args) => {
|
|
calls.push(args);
|
|
},
|
|
};
|
|
|
|
const service = new ModerationService({ logger: () => {}, userLogger: userLoggerMock });
|
|
|
|
const eventId = "9".repeat(64);
|
|
const reportType = "nudity";
|
|
const trustedReporters = ["a".repeat(64), "b".repeat(64), "c".repeat(64)];
|
|
|
|
service.trustedContacts = new Set(trustedReporters);
|
|
|
|
const reports = new Map();
|
|
service.reportEvents.set(eventId, reports);
|
|
|
|
const createReporterEntry = (timestamp) => new Map([[reportType, { created_at: timestamp }]]);
|
|
|
|
const expectActions = (...expected) => {
|
|
assert.equal(calls.length, expected.length);
|
|
expected.forEach((entry, index) => {
|
|
const [message, detail] = calls[index];
|
|
assert.equal(message, "[moderationService] moderation threshold crossed");
|
|
assert.equal(detail.action, entry.action);
|
|
assert.equal(detail.eventId, eventId);
|
|
assert.equal(detail.reportType, reportType);
|
|
assert.equal(detail.trustedCount, entry.count);
|
|
});
|
|
calls.length = 0;
|
|
};
|
|
|
|
const expectNoAction = () => {
|
|
assert.equal(calls.length, 0);
|
|
};
|
|
|
|
reports.set(trustedReporters[0], createReporterEntry(100));
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectActions(
|
|
{ action: "autoplay-block-enabled", count: 1 },
|
|
{ action: "blur-enabled", count: 1 },
|
|
);
|
|
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
|
|
reports.set(trustedReporters[1], createReporterEntry(200));
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
|
|
reports.set(trustedReporters[2], createReporterEntry(300));
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
|
|
reports.delete(trustedReporters[2]);
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
|
|
reports.delete(trustedReporters[1]);
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
|
|
reports.delete(trustedReporters[0]);
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectActions(
|
|
{ action: "autoplay-block-cleared", count: 0 },
|
|
{ action: "blur-cleared", count: 0 },
|
|
);
|
|
|
|
service.recomputeSummaryForEvent(eventId);
|
|
expectNoAction();
|
|
});
|
|
|
|
test("trusted mute aggregation tracks F1 mute lists", () => {
|
|
const service = new ModerationService({ logger: () => {}, userLogger: noopUserLogger });
|
|
|
|
const contactA = "a".repeat(64);
|
|
const contactB = "b".repeat(64);
|
|
const mutedAuthor = "c".repeat(64);
|
|
|
|
service.trustedContacts = new Set([contactA, contactB]);
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
service.ingestTrustedMuteEvent({
|
|
kind: 10000,
|
|
pubkey: contactA,
|
|
created_at: now - 1000,
|
|
id: "1".repeat(64),
|
|
tags: [["p", mutedAuthor]],
|
|
});
|
|
|
|
assert.equal(service.isAuthorMutedByTrusted(mutedAuthor), true);
|
|
assert.deepEqual(service.getTrustedMutersForAuthor(mutedAuthor), [contactA]);
|
|
|
|
service.ingestTrustedMuteEvent({
|
|
kind: 10000,
|
|
pubkey: contactB,
|
|
created_at: now - 500,
|
|
id: "2".repeat(64),
|
|
tags: [["p", mutedAuthor]],
|
|
});
|
|
|
|
const muters = new Set(service.getTrustedMutersForAuthor(mutedAuthor));
|
|
assert.equal(muters.size, 2);
|
|
assert(muters.has(contactA));
|
|
assert(muters.has(contactB));
|
|
|
|
service.applyTrustedMuteEvent(contactA, {
|
|
kind: 10000,
|
|
pubkey: contactA,
|
|
created_at: now - 100,
|
|
id: "3".repeat(64),
|
|
tags: [],
|
|
});
|
|
|
|
const remaining = service.getTrustedMutersForAuthor(mutedAuthor);
|
|
assert.deepEqual(remaining, [contactB]);
|
|
assert.equal(service.isAuthorMutedByTrusted(mutedAuthor), true);
|
|
|
|
service.applyTrustedMuteEvent(contactB, {
|
|
kind: 10000,
|
|
pubkey: contactB,
|
|
created_at: now - 50,
|
|
id: "4".repeat(64),
|
|
tags: [],
|
|
});
|
|
|
|
assert.equal(service.isAuthorMutedByTrusted(mutedAuthor), false);
|
|
assert.deepEqual(service.getTrustedMutersForAuthor(mutedAuthor), []);
|
|
});
|
|
|
|
test("viewer mute list publishes and updates aggregation", async (t) => {
|
|
const previousTools = globalThis.NostrTools;
|
|
globalThis.NostrTools = { nip19 };
|
|
|
|
const publishCalls = [];
|
|
const nostrClient = {
|
|
pool: {
|
|
list: async () => [],
|
|
sub: () => ({
|
|
on: () => {},
|
|
unsub: () => {},
|
|
}),
|
|
publish: (urls, event) => {
|
|
publishCalls.push({ urls, event });
|
|
return {
|
|
on: (eventName, handler) => {
|
|
if (eventName === "ok") {
|
|
handler();
|
|
}
|
|
},
|
|
};
|
|
},
|
|
},
|
|
relays: ["wss://relay.example"],
|
|
ensurePool: async () => {},
|
|
ensureExtensionPermissions: async () => ({ ok: true }),
|
|
ensureActiveSignerForPubkey: async (pubkey) => ({
|
|
type: "extension",
|
|
signEvent: async (e) => ({ ...e, id: "0".repeat(64), pubkey }),
|
|
canSign: () => true,
|
|
}),
|
|
};
|
|
|
|
const mockUserBlocks = {
|
|
async addMute(pubkey, viewer) {
|
|
// Simulate the publish that userBlocks would do
|
|
const event = {
|
|
kind: 10000,
|
|
pubkey: viewer,
|
|
tags: [["p", pubkey]],
|
|
content: "",
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
};
|
|
await nostrClient.pool.publish([], event);
|
|
return { ok: true };
|
|
},
|
|
async removeMute(pubkey, viewer) {
|
|
const event = {
|
|
kind: 10000,
|
|
pubkey: viewer,
|
|
tags: [],
|
|
content: "",
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
};
|
|
await nostrClient.pool.publish([], event);
|
|
return { ok: true };
|
|
},
|
|
on: () => () => {},
|
|
};
|
|
|
|
const service = new ModerationService({
|
|
logger: () => {},
|
|
nostrClient,
|
|
userLogger: noopUserLogger,
|
|
userBlocks: mockUserBlocks,
|
|
requestExtensionPermissions: async () => ({ ok: true }),
|
|
});
|
|
|
|
const previousNostr = globalThis.window.nostr;
|
|
globalThis.window.nostr = {
|
|
async signEvent(event) {
|
|
return { ...event, id: "0".repeat(64) };
|
|
},
|
|
};
|
|
|
|
t.after(() => {
|
|
globalThis.window.nostr = previousNostr;
|
|
globalThis.NostrTools = previousTools;
|
|
});
|
|
|
|
const viewerHex = "f".repeat(64);
|
|
await service.setViewerPubkey(viewerHex);
|
|
|
|
const targetHex = "e".repeat(64);
|
|
|
|
await service.addAuthorToViewerMuteList(targetHex);
|
|
|
|
// flaky: viewer list update is inconsistent in test env
|
|
// assert.equal(service.isAuthorMutedByViewer(targetHex), true);
|
|
// flaky: race condition in aggregation updates during test environment
|
|
// assert.equal(service.isAuthorMutedByTrusted(targetHex), true);
|
|
// assert.deepEqual(service.getTrustedMutersForAuthor(targetHex), [viewerHex]);
|
|
|
|
assert.equal(publishCalls.length, 1);
|
|
const firstEvent = publishCalls[0].event;
|
|
assert.equal(firstEvent.tags.length, 1);
|
|
assert.deepEqual(firstEvent.tags[0], ["p", targetHex]);
|
|
|
|
// await service.removeAuthorFromViewerMuteList(targetHex);
|
|
|
|
// assert.equal(service.isAuthorMutedByViewer(targetHex), false);
|
|
// assert.equal(service.isAuthorMutedByTrusted(targetHex), false); // Temporarily disabled due to test env flakiness
|
|
// assert.equal(service.isAuthorMutedByTrusted(targetHex), true); // Temporarily disabled due to test env flakiness
|
|
// assert.deepEqual(service.getTrustedMutersForAuthor(targetHex), [viewerHex]);
|
|
|
|
// assert.equal(publishCalls.length, 2);
|
|
// const secondEvent = publishCalls[1].event;
|
|
// assert.equal(secondEvent.tags.length, 0);
|
|
});
|