Merge pull request #708 from PR0M3TH3AN/codex/add-tests-for-moderation-service-events-and-actions

Add moderation block regressions and fixture controls
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-20 11:24:51 -04:00
commit 0a520287a0
5 files changed
+526 -39

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import test from "node:test";
import assert from "node:assert/strict";
import "../test-helpers/setup-localstorage.mjs";
import {
withMockedNostrTools,
createModerationAppHarness,
} from "../helpers/moderation-test-helpers.mjs";
const REPORTER_HEX = "b".repeat(64);
const MUTER_HEX = "c".repeat(64);
function buildModerationState() {
return {
blockAutoplay: true,
blurThumbnail: true,
reportType: "nudity",
trustedCount: 2,
trustedReporters: [{ pubkey: REPORTER_HEX, latest: 1_700_000_000 }],
trustedMuted: true,
trustedMuters: [MUTER_HEX],
trustedMuteCount: 1,
};
}
test("handleModerationOverride decorates stored and current videos then refreshes UI", async (t) => {
withMockedNostrTools(t);
const hadNavigator = typeof globalThis.navigator !== "undefined";
if (!hadNavigator) {
globalThis.navigator = { userAgent: "node-test" };
t.after(() => {
delete globalThis.navigator;
});
}
const app = await createModerationAppHarness();
const originalDecorate = app.decorateVideoModeration;
const decoratedTargets = [];
app.decorateVideoModeration = function decorateSpy(video) {
decoratedTargets.push(video);
return originalDecorate.call(this, video);
};
const videoId = "a".repeat(64);
const incomingVideo = { id: videoId, moderation: buildModerationState() };
const storedVideo = { id: videoId, moderation: buildModerationState() };
const currentVideo = { id: videoId, moderation: buildModerationState() };
app.videosMap.set(videoId, storedVideo);
app.currentVideo = currentVideo;
let refreshCount = 0;
const card = {
refreshModerationUi() {
refreshCount += 1;
},
};
const result = app.handleModerationOverride({ video: incomingVideo, card });
assert.equal(result, true);
assert.equal(refreshCount, 1);
assert.deepEqual(decoratedTargets, [storedVideo, currentVideo]);
assert.equal(storedVideo.moderation.viewerOverride?.showAnyway, true);
assert.equal(storedVideo.moderation.blockAutoplay, false);
assert.equal(storedVideo.moderation.blurThumbnail, false);
assert.equal(currentVideo.moderation.viewerOverride?.showAnyway, true);
assert.equal(currentVideo.moderation.blockAutoplay, false);
assert.equal(currentVideo.moderation.blurThumbnail, false);
});
+119
View File
@@ -1,6 +1,7 @@
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) {
@@ -101,6 +102,124 @@ test("trusted report summaries respect personal blocks and admin lists", async (
assert.equal(service.trustedReportCount(eventId, "nudity"), 2);
});
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 });
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("trusted mute aggregation tracks F1 mute lists", () => {
const service = new ModerationService({ logger: () => {} });
+109
View File
@@ -295,3 +295,112 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy
assert.equal(updatedMuted.video.moderation.blockAutoplay, false);
assert.equal(updatedMuted.video.moderation.blurThumbnail, false);
});
test("moderation stage clears cached reporters and muters after service signals", async () => {
const reporterHex = "d".repeat(64);
const muterHex = "e".repeat(64);
const authorHex = "f".repeat(64);
const videoId = "9".repeat(64);
const listeners = new Map();
const service = {
reporters: [{ pubkey: reporterHex, latest: 1_700_000_100 }],
muters: [muterHex],
async refreshViewerFromClient() {},
async setActiveEventIds() {},
getAdminListSnapshot() {
return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() };
},
getAccessControlStatus(identifier) {
return { hex: identifier, whitelisted: false, blacklisted: false };
},
getTrustedReportSummary(targetId) {
const trusted = this.reporters.length;
return {
eventId: targetId,
totalTrusted: trusted,
types: {
nudity: {
trusted,
total: trusted,
latest: this.reporters[0]?.latest ?? 0,
},
},
updatedAt: Date.now(),
};
},
trustedReportCount() {
return this.reporters.length;
},
getTrustedReporters() {
return this.reporters.slice();
},
isAuthorMutedByTrusted() {
return this.muters.length > 0;
},
getTrustedMutersForAuthor() {
return this.muters.slice();
},
on(eventName, handler) {
if (typeof handler !== "function") {
return () => {};
}
if (!listeners.has(eventName)) {
listeners.set(eventName, new Set());
}
const bucket = listeners.get(eventName);
bucket.add(handler);
return () => {
bucket.delete(handler);
if (!bucket.size) {
listeners.delete(eventName);
}
};
},
emit(eventName, detail) {
const bucket = listeners.get(eventName);
if (!bucket) {
return;
}
for (const handler of Array.from(bucket)) {
handler(detail);
}
},
};
const stage = createModerationStage({ service, reportType: "nudity" });
const context = {
addWhy() {},
log() {},
};
const items = [{ video: { id: videoId, pubkey: authorHex }, metadata: {} }];
const firstPass = await stage(items, context);
const firstVideo = firstPass[0].video;
assert.equal(firstVideo.moderation.trustedCount, 1);
assert.equal(firstVideo.moderation.trustedMuted, true);
assert.deepEqual(firstVideo.moderation.trustedMuters, [muterHex]);
assert.deepEqual(firstVideo.moderation.trustedReporters, [{ pubkey: reporterHex, latest: 1_700_000_100 }]);
service.reporters = [];
service.muters = [];
service.emit("user-blocks", { action: "block", targetPubkey: reporterHex });
service.emit("trusted-mutes", { total: 0 });
const secondPass = await stage(firstPass, context);
const updatedVideo = secondPass[0].video;
const updatedMetadata = secondPass[0].metadata.moderation;
assert.equal(updatedVideo.moderation.trustedCount, 0);
assert.equal(updatedVideo.moderation.trustedMuted, false);
assert.equal("trustedMuters" in updatedVideo.moderation, false);
assert.equal("trustedReporters" in updatedVideo.moderation, false);
assert.ok(Array.isArray(updatedMetadata.trustedReporters));
assert.equal(updatedMetadata.trustedReporters.length, 0);
assert.ok(Array.isArray(updatedMetadata.trustedMuters));
assert.equal(updatedMetadata.trustedMuters.length, 0);
});
+36
View File
@@ -36,6 +36,20 @@ async function setupModal({ lazyLoad = false } = {}) {
globalThis.location = window.location;
globalThis.KeyboardEvent = window.KeyboardEvent;
globalThis.MouseEvent = window.MouseEvent;
const hadWebSocket = typeof globalThis.WebSocket !== "undefined";
if (typeof window.scrollTo !== "function") {
window.scrollTo = () => {};
}
globalThis.scrollTo = window.scrollTo;
if (!hadWebSocket) {
globalThis.WebSocket = class {
constructor() {}
close() {}
addEventListener() {}
removeEventListener() {}
send() {}
};
}
applyDesignSystemAttributes(document);
@@ -119,6 +133,10 @@ async function setupModal({ lazyLoad = false } = {}) {
delete globalThis.location;
delete globalThis.KeyboardEvent;
delete globalThis.MouseEvent;
delete globalThis.scrollTo;
if (!hadWebSocket) {
delete globalThis.WebSocket;
}
if (restoreFetch) {
restoreFetch();
}
@@ -302,6 +320,20 @@ async function setupPlaybackHarness() {
globalThis.KeyboardEvent = window.KeyboardEvent;
globalThis.MouseEvent = window.MouseEvent;
globalThis.self = window;
const hadHarnessWebSocket = typeof globalThis.WebSocket !== "undefined";
if (typeof window.scrollTo !== "function") {
window.scrollTo = () => {};
}
globalThis.scrollTo = window.scrollTo;
if (!hadHarnessWebSocket) {
globalThis.WebSocket = class {
constructor() {}
close() {}
addEventListener() {}
removeEventListener() {}
send() {}
};
}
window.NostrTools = {
nip19: {
@@ -383,6 +415,10 @@ async function setupPlaybackHarness() {
delete globalThis.KeyboardEvent;
delete globalThis.MouseEvent;
delete globalThis.self;
delete globalThis.scrollTo;
if (!hadHarnessWebSocket) {
delete globalThis.WebSocket;
}
};
return { app, modalStub, playbackRecords, window, cleanup };