client-reliability-2: make concurrent-init test hermetic and close feed transport

This commit is contained in:
thePR0M3TH3AN committed 2026-10-19 16:22:57 -04:00
1 parent 3968a97922
commit 181eae95d9
7 files changed
+141 -31

No files matched your search

+72 -30
View File
@@ -1,4 +1,3 @@
import "../test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
@@ -6,54 +5,97 @@ import { NostrClient } from "../../js/nostr/client.js";
// Mock global.window if missing
if (typeof globalThis.window === "undefined") {
globalThis.window = {};
globalThis.window = {};
}
function deferred() {
let resolve;
const promise = new Promise((done) => { resolve = done; });
return { promise, resolve };
}
test("NostrClient initialization race condition: concurrent init() calls", async (t) => {
const client = new NostrClient();
// Mock ensurePool to simulate a slow initialization
const relay = "wss://race.example.invalid";
client.relays = [relay];
const poolEntered = deferred();
const releasePool = deferred();
let poolReady = false;
let subscription;
let initPromiseA;
const subscriptions = [];
// We mock connectionManager.ensurePool via the instance
client.connectionManager.ensurePool = async () => {
// Delay to simulate async work
await new Promise(resolve => setTimeout(resolve, 50));
// Teardown runs even if an assertion fails before init or subscription setup.
t.after(async () => {
releasePool.resolve();
try {
await initPromiseA;
} finally {
subscription?.unsub();
client.resetRelayReconnectState();
}
});
// Create a fake pool
const fakePool = {
sub: () => ({ on: () => {}, unsub: () => {} }),
get: async () => null,
list: async () => [],
ensureRelay: async () => {},
};
// Virtualize the real pool transport boundary; keep init, L1 subscription
// management and the returned feed subscription's cleanup real.
const fakePool = {
sub: (relays, filters) => {
assert.equal(poolReady, true, "Subscription must use the initialized pool");
const record = { relays, filters, closed: 0 };
subscriptions.push(record);
return {
on: () => {},
unsub: () => { record.closed += 1; },
};
},
get: async () => null,
list: async () => [],
ensureRelay: async () => {},
};
t.mock.method(client.connectionManager, "ensurePool", async () => {
poolEntered.resolve();
await releasePool.promise;
client.connectionManager.pool = fakePool;
poolReady = true;
return fakePool;
};
});
// An empty result means connection failure and starts real reconnect timers.
// Model a successful connection at the manager boundary used by init/retry.
t.mock.method(client.connectionManager, "connectToRelays", async () => [
{ url: relay, success: true },
]);
t.mock.method(client, "restoreLocalData", async () => true);
t.mock.method(client, "scheduleStoredRemoteSignerRestore", async () => {});
// Also mock connectToRelays to avoid network calls
client.connectToRelays = async () => [];
client.restoreLocalData = async () => true;
// ACT: Call init() (Caller A)
const initPromiseA = client.init();
// ACT: Call init() again immediately (Caller B)
initPromiseA = client.init();
const initPromiseB = client.init();
// ASSERT: Caller B should receive a promise that resolves when initialization is complete.
// In the buggy version, initPromiseB is undefined because it returns early.
assert.ok(initPromiseB instanceof Promise, "Second init() call should return a Promise, not undefined");
await poolEntered.promise;
let secondInitSettled = false;
void initPromiseB.then(() => { secondInitSettled = true; });
await Promise.resolve();
assert.equal(secondInitSettled, false, "Second caller must wait for pool initialization");
releasePool.resolve();
await initPromiseB;
assert.equal(poolReady, true, "Pool should be ready after awaiting second init() call");
await initPromiseA;
assert.equal(client.connectionManager.relayReconnectTimer, null, "Successful fixture initialization must not schedule relay retries");
// Verify we can use the client
try {
client.subscribeVideos(() => {});
subscription = client.subscribeVideos(() => {});
} catch (e) {
assert.fail("Should be able to subscribe after awaiting init()");
}
assert.equal(subscriptions.length, 1, "Post-init feed subscription must reach the pool transport");
assert.ok(subscriptions[0].relays.length > 0, "Feed must subscribe to a nonempty relay set");
assert.ok(subscriptions[0].filters.some((filter) =>
filter.kinds.includes(30078) && filter["#t"].includes("video")),
"Feed must request video events");
subscription.unsub();
assert.equal(subscriptions[0].closed, 1, "Feed cleanup must close its transport subscription");
subscription.unsub();
assert.equal(subscriptions[0].closed, 1, "Feed cleanup must be idempotent");
});