mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 08:52:25 +00:00
Merge branch 'unstable' into add-count-diagnostics-tests-5953252333821392151
This commit is contained in:
5 files changed
+293
-10
No files matched your search
@@ -3969,6 +3969,11 @@ class Application {
|
||||
return this._feed.checkRelayHealthWarning(...args);
|
||||
}
|
||||
|
||||
async loadFeedVideos(...args) {
|
||||
this._initCoordinators();
|
||||
return this._feed.loadFeedVideos(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to videos (older + new) and render them as they come in.
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ const MAX_LISTENER_SNIPPETS = [
|
||||
|
||||
const MAX_LISTENER_CODES = new Set(["MaxListenersExceededWarning"]);
|
||||
|
||||
function collectCandidateStrings(value) {
|
||||
export function collectCandidateStrings(value) {
|
||||
const candidates = [];
|
||||
|
||||
if (!value) {
|
||||
@@ -40,7 +40,7 @@ function collectCandidateStrings(value) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function shouldSuppressWarning(...args) {
|
||||
export function shouldSuppressWarning(...args) {
|
||||
if (isVerboseDiagnosticsEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -352,18 +352,15 @@ class HashtagPreferencesService {
|
||||
eventId: this.eventId,
|
||||
createdAt: this.eventCreatedAt,
|
||||
};
|
||||
// Use the note type key directly, though "interests" maps to HASHTAG_PREFERENCES
|
||||
// in profileCache. Using NOTE_TYPES.HASHTAG_PREFERENCES explicitly is safer if we import it,
|
||||
// but relying on the mapped key "interests" is also consistent with current usage.
|
||||
// However, the fix requires us to ensure we use the unified path.
|
||||
// We'll stick to "interests" which maps to NOTE_TYPES.HASHTAG_PREFERENCES in profileCache.js
|
||||
profileCache.set("interests", payload);
|
||||
// Use the note type key directly. This maps to the same storage key as the legacy "interests"
|
||||
// mapping in profileCache, ensuring backward compatibility while using the unified path.
|
||||
profileCache.set(NOTE_TYPES.HASHTAG_PREFERENCES, payload);
|
||||
}
|
||||
|
||||
loadFromCache(pubkey) {
|
||||
// profileCache internally uses activePubkey, but we should verify if it matches
|
||||
try {
|
||||
let cached = profileCache.getProfileData(pubkey, "interests");
|
||||
let cached = profileCache.getProfileData(pubkey, NOTE_TYPES.HASHTAG_PREFERENCES);
|
||||
|
||||
// Legacy fallback migration
|
||||
if (!cached && typeof localStorage !== "undefined") {
|
||||
@@ -372,7 +369,7 @@ class HashtagPreferencesService {
|
||||
if (legacy) {
|
||||
const parsed = JSON.parse(legacy);
|
||||
// Migrate to unified profile cache
|
||||
profileCache.set("interests", parsed);
|
||||
profileCache.set(NOTE_TYPES.HASHTAG_PREFERENCES, parsed);
|
||||
localStorage.removeItem(HASHTAG_IDENTIFIER);
|
||||
cached = parsed;
|
||||
devLogger.log(`${LOG_PREFIX} Migrated legacy preferences to profileCache.`);
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test, describe, it, before, after, beforeEach } from "node:test";
|
||||
import { shouldSuppressWarning, collectCandidateStrings } from "../../js/nostr/maxListenerDiagnostics.js";
|
||||
|
||||
// Helper to control verbose mode mock
|
||||
function setVerboseMode(enabled) {
|
||||
if (!globalThis.window) {
|
||||
globalThis.window = {};
|
||||
}
|
||||
globalThis.window.__BITVID_VERBOSE_DEV_MODE__ = enabled;
|
||||
}
|
||||
|
||||
describe("maxListenerDiagnostics", () => {
|
||||
let originalWindow;
|
||||
|
||||
before(() => {
|
||||
originalWindow = globalThis.window;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
globalThis.window = originalWindow;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Default to non-verbose for most tests
|
||||
setVerboseMode(false);
|
||||
});
|
||||
|
||||
describe("collectCandidateStrings", () => {
|
||||
it("should return empty array for null/undefined/falsy", () => {
|
||||
assert.deepEqual(collectCandidateStrings(null), []);
|
||||
assert.deepEqual(collectCandidateStrings(undefined), []);
|
||||
assert.deepEqual(collectCandidateStrings(false), []);
|
||||
assert.deepEqual(collectCandidateStrings(0), []);
|
||||
});
|
||||
|
||||
it("should return array with string for string input", () => {
|
||||
assert.deepEqual(collectCandidateStrings("test warning"), ["test warning"]);
|
||||
});
|
||||
|
||||
it("should extract relevant fields from object", () => {
|
||||
const errorObj = {
|
||||
name: "ErrorName",
|
||||
message: "ErrorMessage",
|
||||
code: "ErrorCode",
|
||||
type: "ErrorType",
|
||||
other: "OtherField"
|
||||
};
|
||||
const result = collectCandidateStrings(errorObj);
|
||||
assert.ok(result.includes("ErrorName"));
|
||||
assert.ok(result.includes("ErrorMessage"));
|
||||
assert.ok(result.includes("ErrorCode"));
|
||||
assert.ok(result.includes("ErrorType"));
|
||||
assert.ok(!result.includes("OtherField"));
|
||||
assert.equal(result.length, 4);
|
||||
});
|
||||
|
||||
it("should ignore non-string fields in object", () => {
|
||||
const errorObj = {
|
||||
message: 123,
|
||||
code: null
|
||||
};
|
||||
assert.deepEqual(collectCandidateStrings(errorObj), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldSuppressWarning", () => {
|
||||
it("should NOT suppress anything if verbose mode is enabled", () => {
|
||||
setVerboseMode(true);
|
||||
assert.equal(shouldSuppressWarning("MaxListenersExceededWarning"), false);
|
||||
assert.equal(shouldSuppressWarning({ code: "MaxListenersExceededWarning" }), false);
|
||||
});
|
||||
|
||||
it("should suppress warning by code string", () => {
|
||||
setVerboseMode(false);
|
||||
assert.equal(shouldSuppressWarning("MaxListenersExceededWarning"), true);
|
||||
});
|
||||
|
||||
it("should suppress warning by object code property", () => {
|
||||
setVerboseMode(false);
|
||||
assert.equal(shouldSuppressWarning({ code: "MaxListenersExceededWarning" }), true);
|
||||
});
|
||||
|
||||
it("should suppress warning by message snippet", () => {
|
||||
setVerboseMode(false);
|
||||
assert.equal(shouldSuppressWarning("Possible EventEmitter memory leak detected. 11 listeners added."), true);
|
||||
});
|
||||
|
||||
it("should suppress warning by object message property snippet", () => {
|
||||
setVerboseMode(false);
|
||||
assert.equal(shouldSuppressWarning({ message: "Possible EventEmitter memory leak detected" }), true);
|
||||
});
|
||||
|
||||
it("should NOT suppress unrelated warnings", () => {
|
||||
setVerboseMode(false);
|
||||
assert.equal(shouldSuppressWarning("Some other warning"), false);
|
||||
assert.equal(shouldSuppressWarning({ message: "Some other warning" }), false);
|
||||
});
|
||||
|
||||
it("should handle multiple arguments", () => {
|
||||
setVerboseMode(false);
|
||||
// First arg unrelated, second arg matching
|
||||
assert.equal(shouldSuppressWarning("unrelated", { code: "MaxListenersExceededWarning" }), true);
|
||||
// Both unrelated
|
||||
assert.equal(shouldSuppressWarning("unrelated", "also unrelated"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("process.emitWarning patch", () => {
|
||||
it("should have patched process.emitWarning", () => {
|
||||
assert.ok(process.emitWarning.__BITVID_MAX_LISTENER_PATCHED__, "process.emitWarning should be patched");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
|
||||
import { describe, it, before, beforeEach, afterEach, after, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
// Set Dev Mode Override to ensure logs are emitted
|
||||
globalThis.__BITVID_DEV_MODE_OVERRIDE__ = true;
|
||||
|
||||
// Mock Worker globally before importing the service
|
||||
class MockWorker {
|
||||
constructor(scriptURL) {
|
||||
this.scriptURL = scriptURL;
|
||||
this.listeners = {};
|
||||
MockWorker.instances.push(this);
|
||||
}
|
||||
|
||||
addEventListener(event, handler) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(handler);
|
||||
}
|
||||
|
||||
removeEventListener(event, handler) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event] = this.listeners[event].filter((h) => h !== handler);
|
||||
}
|
||||
}
|
||||
|
||||
postMessage(data) {
|
||||
// Simulate async processing
|
||||
setImmediate(() => {
|
||||
if (MockWorker.onPostMessage) {
|
||||
MockWorker.onPostMessage(this, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to simulate incoming message from worker
|
||||
emitMessage(data) {
|
||||
if (this.listeners['message']) {
|
||||
const event = { data };
|
||||
this.listeners['message'].forEach((handler) => handler(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MockWorker.instances = [];
|
||||
MockWorker.onPostMessage = null;
|
||||
|
||||
globalThis.Worker = MockWorker;
|
||||
|
||||
describe('exploreDataService - buildWatchHistoryTagCounts', () => {
|
||||
let buildWatchHistoryTagCounts;
|
||||
let consoleWarnMock;
|
||||
let initialCallCount = 0;
|
||||
|
||||
before(async () => {
|
||||
// Mock console.warn BEFORE import so logger.js captures the mock
|
||||
// if console.warn is already mocked (e.g. by other tests? unlikely in isolated process), restore it first
|
||||
if (console.warn.mock) console.warn.mock.restore();
|
||||
|
||||
consoleWarnMock = mock.method(console, 'warn', () => {});
|
||||
|
||||
// Dynamic import
|
||||
const mod = await import('../../js/services/exploreDataService.js');
|
||||
buildWatchHistoryTagCounts = mod.buildWatchHistoryTagCounts;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (consoleWarnMock) consoleWarnMock.mock.restore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
MockWorker.instances = [];
|
||||
MockWorker.onPostMessage = null;
|
||||
initialCallCount = consoleWarnMock.mock.calls.length;
|
||||
});
|
||||
|
||||
it('should return empty Map if watchHistoryService is missing', async () => {
|
||||
const result = await buildWatchHistoryTagCounts({});
|
||||
assert.ok(result instanceof Map);
|
||||
assert.strictEqual(result.size, 0);
|
||||
});
|
||||
|
||||
it('should handle missing loadLatest method gracefully', async () => {
|
||||
const watchHistoryService = {};
|
||||
const result = await buildWatchHistoryTagCounts({ watchHistoryService });
|
||||
assert.strictEqual(result.size, 0);
|
||||
});
|
||||
|
||||
it('should handle loadLatest failure gracefully', async () => {
|
||||
const watchHistoryService = {
|
||||
loadLatest: mock.fn(async () => {
|
||||
throw new Error('Load failed');
|
||||
}),
|
||||
};
|
||||
|
||||
// Even if load fails, it should continue with empty items
|
||||
MockWorker.onPostMessage = (worker, data) => {
|
||||
assert.deepStrictEqual(data.payload.items, []);
|
||||
worker.emitMessage({ id: data.id, result: new Map() });
|
||||
};
|
||||
|
||||
const result = await buildWatchHistoryTagCounts({ watchHistoryService });
|
||||
|
||||
assert.ok(result instanceof Map);
|
||||
assert.strictEqual(result.size, 0);
|
||||
assert.strictEqual(watchHistoryService.loadLatest.mock.callCount(), 1);
|
||||
|
||||
// Verify warning was logged
|
||||
const newCalls = consoleWarnMock.mock.calls.slice(initialCallCount);
|
||||
const warning = newCalls.find(c => c.arguments[0] && String(c.arguments[0]).includes('Failed to load watch history entries'));
|
||||
assert.ok(warning, 'Should log warning about load failure');
|
||||
});
|
||||
|
||||
it('should return counts from worker on success', async () => {
|
||||
const watchHistoryService = {
|
||||
loadLatest: mock.fn(async () => ['item1']),
|
||||
};
|
||||
const nostrService = {
|
||||
getVideosMap: mock.fn(() => new Map([['v1', { tags: ['t1'] }]])),
|
||||
};
|
||||
|
||||
MockWorker.onPostMessage = (worker, data) => {
|
||||
assert.strictEqual(data.type, 'CALC_HISTORY_COUNTS');
|
||||
assert.deepStrictEqual(data.payload.items, ['item1']);
|
||||
|
||||
// Verify worker receives correct payload structure
|
||||
assert.ok(data.payload.videosMap instanceof Map);
|
||||
|
||||
// Simulate successful result
|
||||
worker.emitMessage({
|
||||
id: data.id,
|
||||
result: new Map([['t1', 5]])
|
||||
});
|
||||
};
|
||||
|
||||
const result = await buildWatchHistoryTagCounts({
|
||||
watchHistoryService,
|
||||
nostrService,
|
||||
actor: 'user1'
|
||||
});
|
||||
|
||||
assert.strictEqual(result.get('t1'), 5);
|
||||
});
|
||||
|
||||
it('should handle worker error gracefully', async () => {
|
||||
const watchHistoryService = {
|
||||
loadLatest: mock.fn(async () => ['item1']),
|
||||
};
|
||||
|
||||
MockWorker.onPostMessage = (worker, data) => {
|
||||
worker.emitMessage({
|
||||
id: data.id,
|
||||
error: 'Worker exploded'
|
||||
});
|
||||
};
|
||||
|
||||
const result = await buildWatchHistoryTagCounts({ watchHistoryService });
|
||||
assert.ok(result instanceof Map);
|
||||
assert.strictEqual(result.size, 0);
|
||||
|
||||
const newCalls = consoleWarnMock.mock.calls.slice(initialCallCount);
|
||||
const warning = newCalls.find(c => c.arguments[0] && String(c.arguments[0]).includes('Worker failed to calculate history counts'));
|
||||
assert.ok(warning, 'Should log warning about worker failure');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user