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

+39
View File
@@ -624,3 +624,42 @@ test_integrity_note:
Mutation evidence: removing the product display/visibility exclusion causes this scenario to fail with the hidden control IDs in its focus list (exit 1, 1 failed/0 passed). Mutant reverted. This fixes expensive JSDOM style evaluation in the test harness, not a demonstrated browser defect. Application CSS/layout remains outside this unit fixture's scope.
Fixture mutation: omitting native hidden-attribute filtering also fails the contract (exit 1, 1 failed/0 passed); restored before final checks.
---
# client-reliability-2
```yaml
test_integrity_note:
change_type: ["flake_fix", "new_tests"]
scenarios:
- id: SCN-concurrent-init-hermetic-subscription
given: "two init callers share a pool initialization held at a deterministic barrier, with a virtual successful relay connection"
when: "pool initialization is released and the second caller opens then closes a video subscription"
then: "the second caller waits for pool readiness, real L1 feed setup reaches the fake pool, and unsubscribe closes the transport exactly once with no reconnect timer"
observable_outcomes:
- "second init promise remains pending until the pool barrier is released"
- "pool is ready after awaiting the second caller; both init promises settle"
- "post-init subscription requests video events from a nonempty relay set"
- "subscription transport closes once, including repeated unsubscribe"
- "the process exits naturally with no public relay IO or reconnect retries"
determinism_controls:
- "deferred pool barrier replaces the old 50ms delay"
- "fake pool at the actual transport boundary; real init, L1, and feed cleanup"
- "manager connection boundary reports success rather than triggering background reconnect"
- "local-data/signer restore are virtualized; teardown awaits init, closes feed and resets reconnect state even after failure"
anti_cheat_rationale:
prevents:
- "early-return init mutant fails while pool initialization is held"
- "no-op subscription cannot satisfy the observed transport request"
- "no-op cleanup cannot satisfy exact close count"
- "failure fixture cannot hide a reconnect timer behind a passing assertion"
- "retry/sleep/forced-exit masking: no timeout changes or forced exit"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Original concurrent-promise, pool-ready and post-init no-throw assertions remain. The old empty connection result accidentally modeled failure; init correctly scheduled manager-owned retries that bypassed its client-level stub. The fixture now models successful init at the manager boundary and releases its feed subscription. This is harness cleanup, not a Nostr product change.
Mutants: a test-local historical early-return init variant fails the pending-barrier assertion (exit1, 1 failed); a connection-failure fixture variant fails the no-retry assertion (exit1, 1 failed). Both exit naturally and are reverted. No product file was edited for either mutant.
@@ -0,0 +1,5 @@
- Goal: make tests/race/nostr-client-init-race.test.mjs hermetic and naturally exit while preserving concurrent init, pool readiness, and post-init subscription coverage.
- Temporary ownership granted by dispatch via landed RULE-CHANGES/prompt and owner resume instruction; no Nostr product/shared helper/global runner edits.
- Revalidated unchanged from filedAt f303ab0e72; prior isolated assertion60.91ms passes but process timeout10s/exit124, full gate timeout60s at file227.
- Plan: inspect NIP01 and real init/L1 subscription lifecycle; replace delay with deferred pool barrier; virtualize current pool boundary, close subscription; add observable boundary/cleanup assertions; mutant-check, run both isolated suites, commit separately, merge unstable, lint and full60s/file gates, land normally or route further blockers.
- Preserve prior profile-modal commits and all assertions. No browser, real wallet, signer, external relay or deploy operations.
@@ -0,0 +1,8 @@
# client-reliability-2: hermetic concurrent init
- NIP01 REQ/CLOSE and docs/architecture-refactor.md L1 contract require callers to close subscriptions. Retain real init/L1/feed behavior; fake only IO boundaries.
- Root cause: original client.connectToRelays stub returned [], explicitly signaling initial connection failure. init schedules ConnectionManager's timer, whose callback calls manager.connectToRelays (not the client stub). That path attempts configured public relays and retains retries. Original subscription was also never unsubscribed.
- Replace 50ms mock sleep with deferred entry/release barriers. Successful manager-level connection fixture prevents retries without disabling product retry logic. Pin timer absence with assertion; teardown resets as failure-path hygiene, not a success substitute.
- Add stricter outcomes: pending second caller, observed pool feed filter, nonempty relays, and exactly-once transport closure. Preserve all original expectations and await both callers.
- Mutate via test-local IO/init overrides only; product Nostr/shared helpers/global runner remain untouched. Both early-return and failure-connection variants fail naturally.
- Prior profile-modal commits preserved separately. No actual signer/wallet/relay/browser/build/deploy qualification. Normal full gates required before either item lands.
@@ -1,7 +1,7 @@
{
"id": "client-reliability-2",
"area": "client-reliability",
"status": "open",
"status": "in-progress",
"title": "Make Nostr initialization race test hermetic and release retained relay work",
"blockedOn": null,
"addedOn": "2026-10-19",
@@ -0,0 +1,10 @@
# client-reliability-2 pre-commit checks
- Baseline race unchanged from filedAt f303ab0e72 through merged3968a979; prior full run failed60s/file after226/425 passed and isolated scenario passed60.91ms but process timed out10s/124. Dispatch independently confirmed62ms/124.
- Fixed direct isolated race run: exit0 naturally, 1 passed, 0 failed/cancelled/skipped; scenario9.43ms, total15.69ms (before failure-path teardown await refinement).
- Test-local historical early-return init mutant: exit1 naturally, 1 failed/0 passed; pending second caller assertion catches early settlement.
- Test-local failure connection mutant: exit1 naturally, 1 failed/0 passed; no-retry assertion catches the reconnect timer. Both mutations restored. Product files untouched.
- Final isolated/profile/lint and merged full gates recorded next. No full pass or landing claimed yet.
- Final direct isolated race file: exit0 naturally, 1 passed/0 failed/cancelled/skipped, scenario10.32ms / total14.86ms.
- Preserved profile-modal file: exit0 naturally, 27 passed/0 failed/cancelled/skipped, total2635.71ms.
- `npm run lint` unrestricted: exit0, all11 stages passed. `git diff --check`: exit0.
+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");
});
@@ -0,0 +1,6 @@
- [x] Read routing/mailbox/rules, merge unstable, claim client-reliability-2, revalidate unchanged test against filedAt.
- [x] Read NIP01 and subscription/lifecycle docs, classify manager retry escape and missing unsubscribe.
- [x] Add hermetic fixture, deferred pool barrier and real subscription/close assertions without relaxing existing checks.
- [x] Prove test-local early-return and failure-connection mutants each fail naturally.
- [ ] Run both isolated files, lint, commit separately, merge unstable and run full60s/file gate.
- [ ] Submit normal landing queue; verify durable state and unstable; close both backlog items after landing.