From c3fee190592fffe7bd1907b0f995885b7ba4ec6a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Mon, 19 Oct 2026 16:07:36 -0400 Subject: [PATCH 1/6] client-reliability-1: bound modal fixture style work with native focus contract --- TEST_INTEGRITY.md | 594 +----------------- ..._2026-10-19_20-05-00_client-reliability.md | 3 + ..._2026-10-19_20-10-00_client-reliability.md | 9 + docs/agents/backlog/client-reliability-1.json | 2 +- ..._2026-10-19_20-10-00_client-reliability.md | 12 + tests/profile-modal-controller.test.mjs | 62 ++ ..._2026-10-19_20-05-00_client-reliability.md | 6 + 7 files changed, 110 insertions(+), 578 deletions(-) create mode 100644 context/CONTEXT_2026-10-19_20-05-00_client-reliability.md create mode 100644 decisions/DECISIONS_2026-10-19_20-10-00_client-reliability.md create mode 100644 test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md create mode 100644 todo/TODO_2026-10-19_20-05-00_client-reliability.md diff --git a/TEST_INTEGRITY.md b/TEST_INTEGRITY.md index eda16051..58c1d470 100644 --- a/TEST_INTEGRITY.md +++ b/TEST_INTEGRITY.md @@ -1,590 +1,30 @@ -# Test Integrity Notes - -Machine-readable record of test-expectation changes made under the Spec -Correction Protocol (see CLAUDE.md → Dark Factory rules). Each entry justifies -why a previous expectation was wrong relative to intended behavior and confirms -the replacement is equally strict (or stricter). - ---- +# client-reliability-1 ```yaml test_integrity_note: - date: 2026-06-14 - file: tests/app-batch-fetch-profiles.test.mjs - change_type: ["spec_correction"] + change_type: ["flake_fix", "new_tests"] scenarios: - - id: SCN-profiles-batched - given: "Multiple relays configured (one fast, one failing) and uncached, valid pubkeys to fetch" - when: "batchFetchProfiles runs after profile fetches were migrated to the L1 SubscriptionManager" - then: "Profiles are fetched in ONE batched query across all relays (not one query per relay); the fast relay's newest profile still renders, cached profiles hydrate immediately, and a failing relay does not break the result" + - id: SCN-profile-focus-style-contract + given: "modal controls include visible, disabled, CSS-hidden, inherited-hidden, aria-hidden, and hidden-input cases" + when: "the real controller collects focus targets using fixture and native JSDOM styles, and the user tabs from the last control" + then: "both resolvers retain only visible enabled controls and Tab wraps to the first" observable_outcomes: - - "exactly one pool.list call, targeting all configured relays" - - "query authors = only uncached/valid pubkeys" - - "DOM updates: cached profile first, then newest fast-relay profile" - - "cache set with the newest (Fast) profile only" + - "focus target control IDs match the strict expected list" + - "Tab is prevented and document.activeElement moves to the first control" + - "all 26 existing modal scenarios preserve their assertions and complete naturally" determinism_controls: - - "mock pool.list with deterministic per-relay responses; fixed createdAt ordering" + - "stylesheet-free full-markup fixture virtualizes only display and visibility" + - "small contract scenario runs the same assertions against native JSDOM computed styles" + - "explicit test stylesheet, local services, real DOM focus and event dispatch; no network" anti_cheat_rationale: prevents: - - "over-mocking internal logic" - - "hard-coded return value" - note: "Only the per-relay CALL-COUNT assertion changed (2 -> 1), because the - refactor intentionally batches the fetch. All behavioral assertions - (correct/newest profile rendered, cached hydration, failing relay - tolerated) are unchanged, and a new assertion verifies the single batched - query targets all relays — so coverage is equal or stronger." + - "hard-coded focus list: varied hidden and visible controls" + - "over-mocking controller behavior: real updateFocusTrap and keyboard listener" + - "style resolver rubber-stamping: native resolver must satisfy the same scenario" + - "retry/sleep-based flake masking: no timeouts, retries, or sleeps added" relaxation: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` ---- - -```yaml -test_integrity_note: - date: 2026-06-14 - files: - - tests/relay-subscribe-cap.test.mjs - - tests/user-blocks.test.mjs - - tests/subscriptions-manager.test.mjs - - tests/nostr/client.test.mjs - change_type: ["spec_correction"] - scenarios: - - id: SCN-relay-reads-bounded - given: "A user's NIP-65 read set (in prod often ~20 relays, most dead) and a - cold login that must decrypt encrypted lists (blocks, subs, hashtags) via - the nip-07 extension" - when: "Reads/subscriptions are built via capReadRelays — the user's own - relays first, with RESERVED_DEFAULT_RELAY_SLOTS reserved for reliable - default aggregators, bounded to MAX_SUBSCRIBE_RELAYS=8; writes stay - uncapped" - then: "Every configured relay is still queried when the user's set is small - (data lives on the user's own relays); the distinct read fan-out never - exceeds 8 (so a large dead list can't storm the pool and starve nip-07 - decryption); at least the reserved reliable defaults remain reachable even - when the user supplied none; write reach is unchanged and defaults are NOT - injected into writes" - observable_outcomes: - - "read/subscribe relay set length <= 8" - - "writeRelays == full configured set; no default injected into writes" - - ">= RESERVED_DEFAULT_RELAY_SLOTS reliable defaults present in the read set" - - "user-blocks: every configured relay queried; fast-then-full background - ordering preserved; block list hydrates from the background relay payload" - - "subscriptions-manager: every configured relay queried even when one - rejects; only the newest event decrypted" - - "nostr/client: same events arriving from multiple seeded relays still - dedupe to the expected set; relay fan-out bounded" - determinism_controls: - - "deterministic per-relay mock pool.list responses; fixed created_at ordering" - - "capReadRelays is test-aware: the reserved core resolves to the test-relay - override under test, keeping harnesses isolated from prod default URLs" - - "tests set nostrClient.readRelays to the user's relays to mirror the - logged-in state applyRelayPreferences establishes in prod (the fetcher ranks - candidates by read preference)" - anti_cheat_rationale: - prevents: - - "hard-coded return value" - - "over-mocking internal logic" - - "retry/sleep-based flake masking" - note: "Only incidental EXACT relay-count assertions were loosened to BOUNDS - (<=8) because the storm fix intentionally adds a small reliable-default - backstop to the read set. Every behavioral assertion (correct/newest event - decrypted, block hydration from the right relay, dedup correctness, per-relay - coverage, writes uncapped) is unchanged or strengthened — coverage is equal - or stronger, and new assertions pin the reserved-defaults liveness guarantee." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "Exact-count assertions became bounded-count - assertions, but this reflects intended new behavior (reliable-default - backstop) rather than weakening a real correctness check; coverage of the - actual security/decrypt behavior is preserved." -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-23 - file: tests/nip71-mirror-flags.test.mjs - change_type: ["spec_correction"] - scenarios: - - id: SCN-mirror-delete-unconditional - given: "A video that was shared to NIP-71 on one device/session, then deleted from another device (or after the per-video opt-in flag — browser-local localStorage — was cleared)" - when: "resolveDeleteSync runs during delete with FEATURE_NIP71_MIRROR on but the local enabled flag false/absent" - then: "The mirror teardown (NIP-09 + empty tombstone, idempotent) is still attempted, so the NIP-71 mirror is not orphaned on other apps; the feature flag still fully gates it" - observable_outcomes: - - "resolveDeleteSync({featureOn:true, enabled:false|undefined}) -> {action:'unshare'}" - - "resolveDeleteSync({featureOn:false, ...}) -> {action:'none'} (feature flag still gates)" - determinism_controls: - - "pure function; explicit input matrix" - anti_cheat_rationale: - prevents: - - "hard-coded return value" - note: "The previous expectation (enabled:false -> none) encoded the bug: it - gated teardown on a browser-local flag, orphaning the mirror across devices. - The corrected spec is STRICTER — teardown fires whenever the feature is on — - and adds the enabled:undefined case. The feature-flag gate assertion is - retained, so no real gate was weakened." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-23 - file: tests/nostr-delete-flow.test.mjs - change_type: ["spec_correction", "refactor_tests"] - scenarios: - - id: SCN-delete-surfaces-relay-failures - given: "A logged-in delete where one write relay rejects the kind-5 deletion" - when: "deleteAllVersions publishes to the WRITE set via getDeletePublishRelays() (the afb6200b relay-scope fix)" - then: "The delete summary surfaces the failing relay" - - id: SCN-buffered-feed-tombstone-guard - given: "A delete event seeds a tombstone, then an OLDER non-deleted event for the same root arrives on the live stream" - when: "events flow through the real VideoEventBuffer (the path subscribeVideos builds via the SubscriptionManager), not the removed direct pool.sub() injection" - then: "The older event is tombstone-guarded (marked deleted, kept out of activeMap) and never surfaces as a LIVE video; a deletion event surfaces marked deleted:true so the UI removes the card" - observable_outcomes: - - "deleteSummary.summary.failed identifies wss://relay.fail (writeRelays now configured so the injected failure is exercised)" - - "no callback video with the deleted/older event id and deleted:false" - - "client.activeMap cleared for the root; tombstone advanced; stored event marked deleted" - determinism_controls: - - "videoEventVerifier virtualizes the off-thread signature worker (all events valid)" - - "buffer.scheduleFlush(true) awaited for synchronous commit" - - "real NostrClient + real VideoEventBuffer (no mocked tombstone guard)" - anti_cheat_rationale: - prevents: - - "over-mocking internal logic" - - "snapshot rubber-stamping" - note: "Two corrections. (1) The file used bare assert + top-level await, so the - unit runner (which requires a node:test import) SILENTLY EXCLUDED it — it had - rotted and was failing on main unnoticed. Converted to node:test so it runs - in CI. (2) The relay-failure test set only client.relays, but deletes moved to - the write set (getDeletePublishRelays); it now sets writeRelays so the - injected failure is actually exercised — the assertion was previously passing - vacuously. (3) The subscription assertions drove the REMOVED pool.sub() path - and expected 'no callback at all' for deleted events; the current - VideoEventBuffer intentionally surfaces deleted events marked deleted:true so - the UI can remove the card. Rewritten to drive the real buffer + real client - and assert the stricter, accurate invariant (never surfaces as LIVE; guarded - and removed from activeMap). Coverage is stronger: the buffer's own unit test - mocks the client's tombstone guard, whereas this exercises the real guard." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-23 - file: tests/watch-history.test.mjs - change_type: ["spec_correction"] - scenarios: - - id: SCN-watch-history-partial-acceptance-durable - given: "A logged-in user snapshots watch history to 3 write relays where 2 accept and 1 rejects (single attempt, no escalation)" - when: "watchHistoryService.snapshot runs after the 565a9618 fix (a snapshot is durable as soon as ANY relay accepts; reads take the newest event per d-tag)" - then: "The snapshot SUCCEEDS — it does not throw, the result is ok and not retryable, no republish is scheduled, the month result is flagged partial with the accepting/rejecting relays captured, and the pointer queue is emptied (the pointer was durably published)" - - id: SCN-watch-history-logged-in-not-session - given: "A logged-in user (nostrClient.pubkey set) records a watch and the entry is published and re-loaded" - when: "publishView resolves the owning actor as the logged-in pubkey and sets the session flag only when nobody is logged in" - then: "The round-tripped entry is NOT flagged session=true (it is the logged-in user's history, not a local-only session entry); the queue files under the logged-in pubkey so getQueuedPointers(actor) sees it" - observable_outcomes: - - "snapshot() resolves (does not throw); result.ok === true; result.retryable === false" - - "scheduleWatchHistoryRepublish NOT called" - - "result.results[0].partial === true; relayStatus.pointer shows relay.one/two accepted, relay.three rejected" - - "getQueuedPointers(actor).length === 0 after a successful snapshot" - - "resolvedItems[0].session !== true for a logged-in user" - determinism_controls: - - "poolHarness resolver with a fixed accept/reject plan per relay url" - - "session crypto + recordVideoView virtualized; stable relay set" - - "logged-in pubkey aligned with the queried actor (matches the convention used by the other tests in the file)" - anti_cheat_rationale: - prevents: - - "snapshot rubber-stamping" - - "hard-coded return value" - note: "Three corrections in this file, all matching shipped behavior. (1) The - partial-acceptance test asserted the INVERSE of intended behavior — that - snapshot() THROWS on partial acceptance and retries until EVERY relay accepts — - encoding exactly the all-relays false-failure bug 565a9618 fixed. Rewritten to - the success path (no throw, partial flag, per-relay status, queue drained, no - spurious republish). (2) testWatchHistoryServiceIntegration set the logged-in - pubkey to a value DIFFERENT from the actor it queried; since watch history now - files under the logged-in pubkey, the queue assertions looked at the wrong key. - Aligned pubkey to the queried actor (the convention every other test uses) — a - setup fix, no assertion weakened. (3) The same test asserted session===true for - a logged-in watch, which is the pre-fix bug (logged-in watches were wrongly - flagged session by comparing against the view actor); corrected to session!==true - per the current rule that session marks only logged-out, local-only entries. - With these the full file is green and was REMOVED from the run-unit-tests - QUARANTINE." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-23 - files: - - tests/zap-shared-state.test.mjs - - tests/video-modal-zap.test.mjs - change_type: ["spec_correction", "refactor_tests"] - scenarios: - - id: SCN-platform-fee-unparseable-override - given: "An unparseable platform-fee override value (e.g. 'not-a-number') with PLATFORM_FEE_PERCENT configured > 0" - when: "resolvePlatformFeePercent runs" - then: "It falls back to the configured platform default (getDefaultPlatformFeePercent), NOT to 0 — a junk override cannot silently disable/bypass the platform fee" - - id: SCN-zap-completion-state-resets-then-completes - given: "A zap whose receipts validate to a warning (awaiting validated receipt)" - when: "sendZap runs" - then: "The modal completion state is reset to false at the start of the send and set true at the terminal path, so two completion-state changes are recorded and the end state is true" - - id: SCN-zap-receipts-render-into-controller-element - given: "VideoModal.renderZapReceipts called with one validated (or one failed) receipt" - when: "rendering delegates to the inner zapController" - then: "Exactly one entry is rendered into the controller's modalZapReceipts element" - observable_outcomes: - - "resolvePlatformFeePercent('not-a-number') === getDefaultPlatformFeePercent() (> 0)" - - "completedStates grows by 2 (false then true) on a completing send; end state true" - - "controller.modalZapReceipts.children.length === 1 per rendered receipt" - determinism_controls: - - "pure fee function with explicit inputs; configured PLATFORM_FEE_PERCENT read via getDefaultPlatformFeePercent (not hard-coded)" - - "modal stub records setZapCompleted/resetZapForm calls; DummyElement DOM" - anti_cheat_rationale: - prevents: - - "hard-coded return value" - - "over-mocking internal logic" - note: "Three corrections, all to match shipped behavior. (1) The fee-fallback test - asserted 0; the function correctly falls back to the configured platform default - so a garbage override cannot zero out (bypass) the fee — corrected to assert - against getDefaultPlatformFeePercent and that the result is > 0 (STRICTER: pins - the no-bypass guarantee). (2) The completion-count test asserted +1; sendZap - legitimately resets completed=false at the start of every send and sets true at - the terminal path (+2), with the end-state==true assertion retained as the real - invariant. (3) The receipt-render tests set modalZapReceipts on the outer - VideoModal, but rendering was refactored to delegate to the inner zapController - which owns that element — set/assert on the controller's element (a harness fix, - no assertion weakened). NOTE: these accompany three PRODUCTION bug fixes found - via the same tests — NWC getPublicKey hex->bytes (connection was broken), the - in-flight 'Sending…' status mis-toned as warning, and the success path not using - the holistic resetZapForm." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-23 - file: tests/video-modal-zap.test.mjs - change_type: ["spec_correction"] - scenarios: - - id: SCN-zap-paid-but-receipt-unconfirmed-is-success - given: "A zap share whose PAYMENT succeeded (NWC returned a preimage) but whose kind-9735 zap receipt could not be validated on the advertised relays" - when: "sendZap finishes" - then: "It is reported as a SUCCESS (Sent N sats) with a soft note that the on-relay receipt couldn't be confirmed — NOT as a warning/error. The form resets and the attempt is marked completed." - observable_outcomes: - - "last zap status tone === 'success' (was 'warning')" - - "status message matches /sent 750 sats/i and /couldn't confirm the zap receipt/i (was /awaiting validated zap receipt/i)" - - "receipts rendered in partial mode; form reset; completedStates +2 ending true" - determinism_controls: - - "injected splitAndZap returns a paid receipt (status:success, payment.preimage) with validation.status:'failed'" - anti_cheat_rationale: - prevents: - - "snapshot rubber-stamping" - - "hard-coded return value" - note: "The previous expectation asserted the BUG: a paid share whose 9735 receipt - wasn't found was surfaced as a 'warning' error ('Awaiting validated zap receipt') - and routed through notifyError — telling users a SUCCESSFUL zap had failed - (confirmed live: the platform fee landed but the UI showed a red error). The NWC - payment preimage is proof the invoice was paid; the 9735 receipt is supplementary - and is commonly not seen by a static client (relay coverage/timing). Corrected to - assert success-with-a-soft-note. Genuine PAYMENT failures (status !== 'success') - still go through the failure/retry path, unchanged." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-23 - file: tests/zap-split.test.mjs - change_type: ["spec_correction"] - scenarios: - - id: SCN-zap-receipt-found-by-indexed-tags - given: "A published kind-9735 zap receipt for a video zap (the zap request carries #p recipient + #e event tags)" - when: "validateZapReceipt queries relays for the receipt" - then: "It filters by the reliably-indexed #e + #p tags (not #bolt11, which most relays don't index), then confirms via author pubkey + bolt11 + description hash" - observable_outcomes: - - "the relay query filter has filters[0]['#e'] === [eventId] and ['#p'] === [recipient]" - - "filters[0]['#bolt11'] is absent" - - "a matching receipt still validates as status:'passed'" - determinism_controls: - - "mock SimplePool.list captures the filter; fixed zap request tags + test bolt11 with matching description hash" - anti_cheat_rationale: - prevents: - - "snapshot rubber-stamping" - - "over-mocking internal logic" - note: "The previous test asserted filters[0]['#bolt11'][0] === invoice — encoding the - bug. Relays generally do not index arbitrary tags like bolt11, so that query found - nothing and a successfully-paid zap was reported as having no receipt. Corrected to - assert the reliably-indexed #e/#p anchors (added an #e tag to the request to model a - real video zap). The receipt-matching assertions (passed status, correct event, - checked relays) are unchanged/retained — coverage is stronger, not weaker." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-24 - file: tests/view-counter.test.mjs - change_type: ["spec_correction", "flake_fix"] - scenarios: - - id: SCN-view-counter-legacy-path - given: "The view-counter unit harness mocks the legacy per-pointer view APIs (list/count/subscribe) on the singleton client" - when: "A pointer is subscribed and hydrated" - then: "viewCounter exercises the legacy per-pointer hydrate/subscribe path (the one viewCounter.js explicitly designates 'for tests / mocks'), so the mocked APIs actually fire" - - id: SCN-view-counter-dedupe-window-aligned - given: "Two views by the same viewer within one fixed dedupe window" - when: "Counted" - then: "They dedupe to one (floor(created_at/window) bucket, matching the replaceable view-event d-tag)" - observable_outcomes: - - "hydration triggers a list()/count() call (legacy path) instead of the batched SubscriptionManager path silently taking over" - - "two same-pubkey views in one aligned bucket yield total === 1" - determinism_controls: - - "nostrClient.getSubscriptionManager pinned to () => null so the path is deterministic" - - "the dedupe test's base timestamp is aligned to a window-bucket boundary (was Date.now(), which made the +window/2 offset straddle a boundary depending on time of day)" - anti_cheat_rationale: - prevents: - - "over-mocking internal logic" - - "retry/sleep-based flake masking" - note: "The batched SubscriptionManager path was added later (relay-storm work); the - test mocks the legacy APIs but never pinned getSubscriptionManager, so the real - singleton's manager took over and the mocks never fired. Pinning it to null restores - the intended (and code-designated 'tests/mocks') legacy-path coverage. The dedupe - assertion (count === 1) is unchanged — only its setup was made deterministic." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -```yaml -test_integrity_note: - date: 2026-06-24 - file: tests/view-counter.test.mjs - change_type: ["spec_correction"] - scenarios: - - id: SCN-hydrate-root-timestamp - given: "A latest video revision whose root event is fetched by hydrateVideoHistory" - when: "History is hydrated, then the timestamp-sync step runs" - then: "rootCreatedAtByRoot caches the earliest (root) created_at, AND the active video's rootCreatedAt FIELD is set by syncActiveVideoRootTimestamp" - observable_outcomes: - - "nostrClient.rootCreatedAtByRoot.get(rootId) === root.created_at (hydrateVideoHistory's direct output)" - - "after syncActiveVideoRootTimestamp(...), latestVideo.rootCreatedAt === root.created_at" - determinism_controls: - - "pool.get returns a fixed root event; rootCreatedAtByRoot seeded empty" - anti_cheat_rationale: - prevents: - - "snapshot rubber-stamping" - note: "hydrateVideoHistory's contract is to populate the per-root created_at MAP; the - video OBJECT's rootCreatedAt field is applied by a separate util - (syncActiveVideoRootTimestamp) after a refactor moved that unit boundary. The test - previously expected hydrateVideoHistory to mutate the passed object directly, which it - no longer does. Corrected to assert the map (its real output) and to drive the sync - util for the field — stronger, not weaker (it now exercises the full real pipeline)." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - ---- - -## 2026-06-25 — todo-11b triage: un-quarantine nostr-boost-actions + admin-list-store - -```yaml -test_integrity_note: - change_type: ["spec_correction", "refactor_tests"] - scenarios: - - id: SCN-repost-optional-relay - given: "A repost is built with no relay hint available (buildRepostEvent, nostrEventSchemas.js)" - when: "buildRepostEvent({ eventId, authorPubkey, targetKind }) is called without eventRelay" - then: "It returns a VALID repost (kind 16 for a non-kind-1 target, two-element ['e', id] tag, ['p', author], ['k', kind]) instead of throwing" - - id: SCN-community-blacklist-merge-batched - given: "An admin community-blacklist sources list references two curator kind-30000 lists, each with p-tag members" - when: "loadAdminState() runs and fetches curator lists via the BATCHED SubscriptionManager path" - then: "adminState.blacklist merges the curator members with the direct entry (deduped, guards respected)" - observable_outcomes: - - "buildRepostEvent output shape (kind/tags) with no relay — asserted exactly, stricter than the old throws-check" - - "adminState.blacklist === [direct, communityMemberOne, communityMemberTwo]" - determinism_controls: - - "pure builder (no IO) for repost; in-memory list registry + mocked pool.list AND getSubscriptionManager for the admin store" - anti_cheat_rationale: - prevents: - - "hard-coded return value" - - "over-mocking internal logic" - note: | - nostr-boost-actions: STALE SPEC. The test required buildRepostEvent to throw - /missing-event-relay/ when no relay hint is given, but a relay hint is a NIP-18 - SHOULD (not MUST). The builder intentionally degrades to a two-element `e` tag so a - user can always repost. Replaced the throws-assertion with an EXACT assertion of the - valid output (kind 16 + e/p/k tags) — strictly stronger. - - admin-list-store: STALE HARNESS, no production change, no assertion weakened. Three - fidelity fixes so the test exercises the real batched community-blacklist path: - (1) curator lists are now fetched via the batched SubscriptionManager (cold-start - relay-storm fix), which the harness didn't mock — added a getSubscriptionManager().list - mock serving the same registry; (2) curator coordinates now use REAL 64-char hex - pubkeys (the old mock-"hex" started with "npub", so parseCommunityBlacklistReferences' - isHexPubkey()/npub branch decoded them twice and the batched filter no longer matched); - (3) createListEvent now emits a `d` tag (real kind-30000 lists always have one; - selectNewestEventsForReferences matches curator events to references by their d tag). - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - -## 2026-07-05 — todo-11b final: un-quarantine nwc-client (+ user-blocks / nostr-publish-rejection hangs) - -```yaml -test_integrity_note: - change_type: ["spec_correction", "refactor_tests", "flake_fix"] - scenarios: - - id: SCN-nwc-unsupported-encryption-scheme - given: "An NWC wallet whose info event (kind 13194) advertises ONLY an encryption scheme bitvid does not implement (nip44_v3)" - when: "ensureEncryptionForContext(context) negotiates a scheme" - then: "It rejects with message /Wallet advertises unsupported encryption schemes: nip44_v3\\./ AND error.code === 'UNSUPPORTED_ENCRYPTION'" - - id: SCN-nwc-mock-toolkit-selection - given: "Each nwc-client section installs a per-section window.NostrTools mock to drive encryption-scheme selection" - when: "The nostr-tools bootstrap has installed its frozen canonical toolkit on the global scope" - then: "Removing scope.__BITVID_CANONICAL_NOSTR_TOOLS__ lets readToolkitFromScope fall through to the section mock, so the wiring under test runs against the mock (not real @noble against fake keys)" - observable_outcomes: - - "ensureEncryptionForContext rejection message + error.code (UNSUPPORTED_ENCRYPTION) — asserted exactly" - - "nip44_v2 section still resolves via the mock's nip44 (assert.match on the mock ciphertext shape)" - determinism_controls: - - "per-section in-memory window.NostrTools mocks; canonical-toolkit removal is deterministic (one-time, nothing re-installs it); no network" - anti_cheat_rationale: - prevents: - - "over-mocking internal logic" - - "hard-coded return value" - note: | - nwc-client: TWO issues. (1) MOCK SHADOW (not a spec change): the bootstrap installs a - frozen canonical toolkit that readToolkitFromScope() prefers over window.NostrTools, so - the per-section mocks never took effect and real @noble ran against the fake test keys - ("bad point: is not on curve"). Deleting the scope canonical lets the resolver use each - section's mock — the tests exercise the NWC client's encryption-SELECTION wiring, not - @noble crypto (which is real elsewhere; the parseNwcUri hex-vs-bytes production bug is - guarded by tests/nwc-parse-uri.test.mjs). (2) SPEC CORRECTION: one section asserted a - rejection when the toolkit "lacks nip44" by omitting nip44 from the mock. That precondition - is architecturally unreachable — the bootstrap's mergeWithCanonical ALWAYS backfills the - canonical nip44 (even an explicit `nip44: null` is overwritten by `!merged.nip44`), so - nip44_v2 is never an unsupported scheme. Replaced with a wallet advertising a scheme bitvid - genuinely cannot provide (nip44_v3), exercising the SAME UNSUPPORTED_ENCRYPTION rejection - via a reachable scenario, and strengthened the check to also assert error.code — strictly - stronger than the old message-only match. - - user-blocks + nostr-publish-rejection: FLAKE_FIX (no assertion changed). Both are - bare-assert files that PASS all assertions then hang on lingering handles after completion - (user-blocks: a never-resolving decrypt's background retry timer that manager.reset() doesn't - cancel; nostr-publish-rejection: js/app.js + subscriptions + userBlocks module-load - timers/connection managers). Applied the repo's established post-completion - `setTimeout(() => process.exit(0), 50)` exit after cleanup. No behavior or expectation was - altered — the process simply exits once the assertions have all run. - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - -## 2026-07-05 — todo-11: uploadModal-reset zombie-callback test made deterministic (correct mock boundary) - -```yaml -test_integrity_note: - change_type: ["flake_fix", "refactor_tests"] - scenarios: - - id: SCN-upload-zombie-callback-guard - given: "An upload is in progress (videoUploadState.status === 'uploading') via a controllable pending mediaUploader.uploadVideo" - when: "resetUploads() runs mid-upload (bumping videoUploadId), then the in-flight upload later resolves (a 'zombie' completion)" - then: "The stale completion is ignored — status stays 'idle' and results.videoUrl stays '' — because handleVideoSelection guards on videoUploadId !== currentUploadId" - observable_outcomes: - - "videoUploadState.status: 'uploading' after start, 'idle' after reset, still 'idle' after the zombie resolve" - - "results.videoUrl.value === '' after the zombie resolve" - - "test exits 0 with no async-after-test activity (was leaking webtorrent/Blob errors)" - determinism_controls: - - "mediaUploader.uploadVideo replaced with a manually-resolved promise; captureVideoMetadata stubbed; jsdom DOM; no webtorrent, no network, no real Blob ops" - anti_cheat_rationale: - prevents: - - "over-mocking internal logic" - - "retry/sleep-based flake masking" - note: | - FLAKE_FIX + REFACTOR_TESTS, no assertion weakened. The subtest PASSED its asserts but - leaked async activity after the test ended, which node:test flags as a failure. Root - cause: it mocked modal.generateTorrentMetadata / resolveUploadIdentifier, but - handleVideoSelection drives the upload through this.mediaUploader.uploadVideo — a - DIFFERENT object with its OWN helpers — so the mediaUploader still ran real - createTorrentMetadata()/hashing on a synthetic {name:'video.mp4'} file ("invalid input - type" async), and captureVideoMetadata() ran URL.createObjectURL(file) ("must be an - instance of Blob"). Fixed by mocking at the correct DI boundary - (modal.mediaUploader.uploadVideo returns the controllable pending promise) and stubbing - captureVideoMetadata. The unit under test is the modal's videoUploadId zombie-guard, so - mocking the uploader boundary is correct isolation, not over-mocking (real upload/torrent - paths are covered by the mediaUploader / s3-upload suites). - mutation_verification: - - "Broke the guard (`if (this.videoUploadId !== currentUploadId) return` → `if (false)`) → the test FAILS ('State should remain idle after zombie completion', # fail 1). Reverted." - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` - -## 2026-07-05 — todo-11b: un-quarantine dm-block-filter (hermetic subscription; no force-exit) - -```yaml -test_integrity_note: - change_type: ["flake_fix"] - scenarios: - - id: SCN-dm-block-filter-hermetic - given: "The 'integration with loaded user block list' test loads standard + legacy block lists and filters incoming DMs" - when: "manager.loadBlocks() runs against stubbed fetchListIncrementally, with the live block-list subscription stubbed out" - then: "Both blocked contacts (standard + legacy) are filtered (dmMessages === [ALLOWED]) AND the process opens NO real relay sockets, so node:test exits naturally with the correct exit code" - observable_outcomes: - - "manager.isBlocked(legacy) && manager.isBlocked(standard) === true; dmMessages length 1 (ALLOWED only)" - - "process.getActiveResourcesInfo() shows no TCPSocketWrap after loadBlocks; node exits 0 on pass, 1 on a forced failure (verified — failures NOT masked)" - determinism_controls: - - "stubbed nostrClient.fetchListIncrementally; truthy stub nostrClient.pool; no-op nostrClient.getSubscriptionManager; window.nostr.nip04.decrypt stub. All restored in finally. No network." - anti_cheat_rationale: - prevents: - - "retry/sleep-based flake masking" - - "over-mocking internal logic" - note: | - FLAKE_FIX, no assertion changed. The file PASSED all subtests then hung: loadBlocks() - opens a LIVE block-list subscription through nostrClient's real SimplePool aimed at the - test's fake relay URLs, leaking TCP sockets + reconnect timers that outlive the run so - node:test never exits (surfaced only once the runner was given a per-file timeout). - Diagnosed with process.getActiveResourcesInfo() — 4 TCPSocketWrap + reconnect Timeouts; - NOT the decrypt-retry timer (verified absent). Nulling the pool did not help because - loadBlocks calls ensurePool() (userBlocks.js:967) to re-create+connect a real pool when - pool is falsy. Fix keeps the test hermetic: a TRUTHY stub pool (so ensurePool is skipped) - + a no-op getSubscriptionManager (so the subscription opens nothing). The block-merge - logic under test still runs fully through the stubbed fetchListIncrementally. A force - `process.exit` in an after() hook was explicitly REJECTED after verifying it masks real - failures (process.exitCode is not set when the hook's timer fires — a deliberately broken - assertion still exited 0 under that approach). Under this fix, the forced-failure control - correctly exits 1. - relaxation: - did_relax_any_assertion: false - if_true_explain_spec_basis: "" -``` +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. diff --git a/context/CONTEXT_2026-10-19_20-05-00_client-reliability.md b/context/CONTEXT_2026-10-19_20-05-00_client-reliability.md new file mode 100644 index 00000000..21978665 --- /dev/null +++ b/context/CONTEXT_2026-10-19_20-05-00_client-reliability.md @@ -0,0 +1,3 @@ +- Goal: classify and fix profile-modal-controller suite hang at current HEAD 9e6c310d (unchanged from filedAt 8b204687). +- Scope: owned test and narrow modal lifecycle helpers only. Preserve assertions; no retries, sleeps, timeout increases, quarantine or forced success exit. +- Done: deterministic regression, failing mutant, isolated natural exit, full 60s/file unit run, lint; build/browser if product changes; fleet commit, merge unstable, checks, queue and durable landing. diff --git a/decisions/DECISIONS_2026-10-19_20-10-00_client-reliability.md b/decisions/DECISIONS_2026-10-19_20-10-00_client-reliability.md new file mode 100644 index 00000000..9c461b7e --- /dev/null +++ b/decisions/DECISIONS_2026-10-19_20-10-00_client-reliability.md @@ -0,0 +1,9 @@ +# client-reliability-1: classify the profile-modal timeout + +- Product/test source unchanged between filedAt 8b204687 and startup HEAD 9e6c310d. +- Clean-environment 60s/file run reproduced timeout. Per-scenario tracing then completed all 26 scenarios naturally in 57041.55ms; no blocked await or retained resource was found. Timing near the bound varies with cold/warm JSDOM style caches. +- CPU profile of the Escape scenario points to CSS color/border normalization and computed-style resolution. The full markup has many controls; updateFocusTrap asks JSDOM for a full computed declaration for each control on repeated pane refreshes. +- Fix only the stylesheet-free test fixture: virtualize display/visibility, with explicit fixture CSS and a new native-JSDOM contract scenario. Preserve every existing assertion and real controller, focus events, DOM and pane operations. No timeout/retry/sleep/runner or product change. +- Regression compares fixture/native style filtering for visible, disabled, CSS-hidden, inherited-visibility-hidden, aria-hidden, and hidden-type controls, then checks actual Tab event focus wrapping. +- Limits: fixture does not model application CSS/layout. This suite never loaded those styles; browser and build checks are not required for this test-only fix. No release, browser, signer, network or wallet qualification claimed. +- Slowdown: initial cold worktree lacked dependencies; cached offline install resolved it. Native DOM assertion diffs made the first mutant diagnostic expensive; comparing control IDs yields a bounded useful failure. Tool improvement: show per-scenario start/end timestamps when a file exceeds its diagnostic bound, distinguishing slow progress from a blocked await. diff --git a/docs/agents/backlog/client-reliability-1.json b/docs/agents/backlog/client-reliability-1.json index 3047b67b..607c321e 100644 --- a/docs/agents/backlog/client-reliability-1.json +++ b/docs/agents/backlog/client-reliability-1.json @@ -1,7 +1,7 @@ { "id": "client-reliability-1", "area": "client-reliability", - "status": "open", + "status": "in-progress", "title": "Diagnose and fix profile-modal suite hang without weakening assertions", "blockedOn": null, "addedOn": "2026-10-19", diff --git a/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md b/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md new file mode 100644 index 00000000..d8b4ce41 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md @@ -0,0 +1,12 @@ +# client-reliability-1 initial evidence (startup HEAD 9e6c310d) + +- `npm ci --ignore-scripts --offline --cache /tmp/client-reliability-npm-cache`: exit 1, ENOTCACHED. Default-cache `npm ci --ignore-scripts --offline`: exit 0, 441 packages installed, zero audit vulnerabilities. +- GitHub PR-overlap API read: exit 6, DNS unavailable; no PR opened, fleet assignment reserves scope. +- Initial direct node run: exit 1, missing jsdom before install. +- `env -u NODE_TEST_CONTEXT UNIT_TEST_TIMEOUT_MS=60000 node scripts/run-unit-tests.mjs profile-modal-controller.test.mjs`: exit 1, timed out at 60000ms. +- Unbounded direct node baseline with temporary start/end tracing: exit 0 naturally, 26 tests passed, 0 failed/cancelled/skipped, 57041.55ms. Tracing removed. An earlier unbounded diagnostic was interrupted (130), no result claimed. +- CPU-profiled Escape scenario: exit 0, 1 test passed, 3703.35ms total. Largest sampled costs include CSS color serialization/border normalization; no client defect established. +- With final CSS fixture, direct node isolated suite: exit 0 naturally, 27 passed, 0 failed/cancelled/skipped, 2840.18ms. +- Deliberate product mutant removing display/visibility filtering: final regression exits 1, 1 failed/0 passed, hidden control IDs appear in actual focus list. Product restored byte-for-byte. Initial DOM-object diff diagnostic interrupted (130); replaced with control-ID diagnostics, no assertion relaxation. +- Initial sandbox lint: exit 2, git subprocess EPERM. Unrestricted lint then exposed forbidden direct style assignments in test fixture; corrected to fixture stylesheet classes. `npm run lint` unrestricted final run: exit 0, all 11 stages passed. +- Early full-unit diagnostic interrupted (130) before completion while final fixture was being prepared; no full pass claimed. Final committed/merged-tree checks follow in a separate log. diff --git a/tests/profile-modal-controller.test.mjs b/tests/profile-modal-controller.test.mjs index 4493a54b..bd095543 100644 --- a/tests/profile-modal-controller.test.mjs +++ b/tests/profile-modal-controller.test.mjs @@ -19,6 +19,7 @@ const defaultActorHex = 'a'.repeat(64); let dom; let container; +let nativeGetComputedStyle; async function waitForAnimationFrame(window, cycles = 1) { for (let i = 0; i < cycles; i += 1) { @@ -125,6 +126,21 @@ beforeEach(() => { global.requestAnimationFrame = window.requestAnimationFrame; global.cancelAnimationFrame = window.cancelAnimationFrame; + // This fixture loads markup without application stylesheets. Virtualize the + // two computed properties consumed by the focus trap rather than asking JSDOM + // to resolve every CSS property for every control on each pane refresh. + const focusStyles = document.createElement('style'); + focusStyles.textContent = ` + .focus-test-display-none { display: none; } + .focus-test-visibility-hidden { visibility: hidden; } + `; + document.head.appendChild(focusStyles); + nativeGetComputedStyle = window.getComputedStyle.bind(window); + window.getComputedStyle = (element) => ({ + display: element.matches('.focus-test-display-none, input[type="hidden"]') ? 'none' : 'block', + visibility: element.closest('.focus-test-visibility-hidden') ? 'hidden' : 'visible', + }); + window.confirm = () => true; global.confirm = window.confirm; @@ -181,6 +197,7 @@ afterEach(() => { delete window.__matchMediaMocks; } container = null; + nativeGetComputedStyle = null; }); function normalizeTestTag(value) { @@ -1062,6 +1079,51 @@ test('load() injects markup and caches expected elements', async () => { assert.ok(controller.walletController.walletStatusText instanceof window.HTMLElement); }); +test('focus trap excludes hidden and disabled controls with fixture and native styles', () => { + const panel = document.createElement('div'); + container.appendChild(panel); + const addControl = (id, configure = () => {}) => { + const control = document.createElement('input'); + control.id = id; + configure(control); + panel.appendChild(control); + return control; + }; + const visible = addControl('visible'); + addControl('disabled', (control) => { control.disabled = true; }); + addControl('display-none', (control) => { control.classList.add('focus-test-display-none'); }); + addControl('visibility-hidden', (control) => { control.classList.add('focus-test-visibility-hidden'); }); + addControl('aria-hidden', (control) => { control.setAttribute('aria-hidden', 'true'); }); + addControl('hidden-input', (control) => { control.type = 'hidden'; }); + const inherited = addControl('inherited-visibility'); + const hiddenParent = document.createElement('div'); + hiddenParent.classList.add('focus-test-visibility-hidden'); + panel.appendChild(hiddenParent); + hiddenParent.appendChild(inherited); + const last = addControl('last'); + + const controller = createController(); + controller.profileModalPanel = panel; + const fixtureGetComputedStyle = window.getComputedStyle; + try { + for (const resolveStyle of [fixtureGetComputedStyle, nativeGetComputedStyle]) { + window.getComputedStyle = resolveStyle; + controller.updateFocusTrap(); + assert.deepEqual(controller.focusableElements.map((control) => control.id), ['visible', 'last']); + last.focus(); + const tab = new window.KeyboardEvent('keydown', { + key: 'Tab', bubbles: true, cancelable: true, + }); + last.dispatchEvent(tab); + assert.equal(tab.defaultPrevented, true); + assert.strictEqual(document.activeElement, visible); + } + } finally { + window.getComputedStyle = fixtureGetComputedStyle; + controller.hide({ silent: true }); + } +}); + test('show()/hide() toggle panes, trap focus, and refresh the wallet pane', async () => { const controller = createController(); await controller.load(); diff --git a/todo/TODO_2026-10-19_20-05-00_client-reliability.md b/todo/TODO_2026-10-19_20-05-00_client-reliability.md new file mode 100644 index 00000000..f516d88c --- /dev/null +++ b/todo/TODO_2026-10-19_20-05-00_client-reliability.md @@ -0,0 +1,6 @@ +- [x] Read rules, mailbox, known issues and integrity standard; claim client-reliability-1. +- [ ] Install worktree dependencies and reproduce at actual boundary. +- [ ] Diagnose and make minimal deterministic fix; preserve expectations. +- [ ] Prove scenario regression with mutant and run required checks. +- [ ] Commit, merge unstable, validate, submit, verify landing and close. +- GitHub overlap check failed DNS; no PR being opened. Fleet reserves this scope. From be6058b0af2396784844531e343dde933ac944d6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Mon, 19 Oct 2026 16:07:46 -0400 Subject: [PATCH 2/6] client-reliability-1: preserve prior integrity notes alongside modal entry --- TEST_INTEGRITY.md | 594 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 594 insertions(+) diff --git a/TEST_INTEGRITY.md b/TEST_INTEGRITY.md index 58c1d470..70cdd1b6 100644 --- a/TEST_INTEGRITY.md +++ b/TEST_INTEGRITY.md @@ -1,3 +1,597 @@ +# Test Integrity Notes + +Machine-readable record of test-expectation changes made under the Spec +Correction Protocol (see CLAUDE.md → Dark Factory rules). Each entry justifies +why a previous expectation was wrong relative to intended behavior and confirms +the replacement is equally strict (or stricter). + +--- + +```yaml +test_integrity_note: + date: 2026-06-14 + file: tests/app-batch-fetch-profiles.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-profiles-batched + given: "Multiple relays configured (one fast, one failing) and uncached, valid pubkeys to fetch" + when: "batchFetchProfiles runs after profile fetches were migrated to the L1 SubscriptionManager" + then: "Profiles are fetched in ONE batched query across all relays (not one query per relay); the fast relay's newest profile still renders, cached profiles hydrate immediately, and a failing relay does not break the result" + observable_outcomes: + - "exactly one pool.list call, targeting all configured relays" + - "query authors = only uncached/valid pubkeys" + - "DOM updates: cached profile first, then newest fast-relay profile" + - "cache set with the newest (Fast) profile only" + determinism_controls: + - "mock pool.list with deterministic per-relay responses; fixed createdAt ordering" + anti_cheat_rationale: + prevents: + - "over-mocking internal logic" + - "hard-coded return value" + note: "Only the per-relay CALL-COUNT assertion changed (2 -> 1), because the + refactor intentionally batches the fetch. All behavioral assertions + (correct/newest profile rendered, cached hydration, failing relay + tolerated) are unchanged, and a new assertion verifies the single batched + query targets all relays — so coverage is equal or stronger." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-14 + files: + - tests/relay-subscribe-cap.test.mjs + - tests/user-blocks.test.mjs + - tests/subscriptions-manager.test.mjs + - tests/nostr/client.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-relay-reads-bounded + given: "A user's NIP-65 read set (in prod often ~20 relays, most dead) and a + cold login that must decrypt encrypted lists (blocks, subs, hashtags) via + the nip-07 extension" + when: "Reads/subscriptions are built via capReadRelays — the user's own + relays first, with RESERVED_DEFAULT_RELAY_SLOTS reserved for reliable + default aggregators, bounded to MAX_SUBSCRIBE_RELAYS=8; writes stay + uncapped" + then: "Every configured relay is still queried when the user's set is small + (data lives on the user's own relays); the distinct read fan-out never + exceeds 8 (so a large dead list can't storm the pool and starve nip-07 + decryption); at least the reserved reliable defaults remain reachable even + when the user supplied none; write reach is unchanged and defaults are NOT + injected into writes" + observable_outcomes: + - "read/subscribe relay set length <= 8" + - "writeRelays == full configured set; no default injected into writes" + - ">= RESERVED_DEFAULT_RELAY_SLOTS reliable defaults present in the read set" + - "user-blocks: every configured relay queried; fast-then-full background + ordering preserved; block list hydrates from the background relay payload" + - "subscriptions-manager: every configured relay queried even when one + rejects; only the newest event decrypted" + - "nostr/client: same events arriving from multiple seeded relays still + dedupe to the expected set; relay fan-out bounded" + determinism_controls: + - "deterministic per-relay mock pool.list responses; fixed created_at ordering" + - "capReadRelays is test-aware: the reserved core resolves to the test-relay + override under test, keeping harnesses isolated from prod default URLs" + - "tests set nostrClient.readRelays to the user's relays to mirror the + logged-in state applyRelayPreferences establishes in prod (the fetcher ranks + candidates by read preference)" + anti_cheat_rationale: + prevents: + - "hard-coded return value" + - "over-mocking internal logic" + - "retry/sleep-based flake masking" + note: "Only incidental EXACT relay-count assertions were loosened to BOUNDS + (<=8) because the storm fix intentionally adds a small reliable-default + backstop to the read set. Every behavioral assertion (correct/newest event + decrypted, block hydration from the right relay, dedup correctness, per-relay + coverage, writes uncapped) is unchanged or strengthened — coverage is equal + or stronger, and new assertions pin the reserved-defaults liveness guarantee." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "Exact-count assertions became bounded-count + assertions, but this reflects intended new behavior (reliable-default + backstop) rather than weakening a real correctness check; coverage of the + actual security/decrypt behavior is preserved." +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-23 + file: tests/nip71-mirror-flags.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-mirror-delete-unconditional + given: "A video that was shared to NIP-71 on one device/session, then deleted from another device (or after the per-video opt-in flag — browser-local localStorage — was cleared)" + when: "resolveDeleteSync runs during delete with FEATURE_NIP71_MIRROR on but the local enabled flag false/absent" + then: "The mirror teardown (NIP-09 + empty tombstone, idempotent) is still attempted, so the NIP-71 mirror is not orphaned on other apps; the feature flag still fully gates it" + observable_outcomes: + - "resolveDeleteSync({featureOn:true, enabled:false|undefined}) -> {action:'unshare'}" + - "resolveDeleteSync({featureOn:false, ...}) -> {action:'none'} (feature flag still gates)" + determinism_controls: + - "pure function; explicit input matrix" + anti_cheat_rationale: + prevents: + - "hard-coded return value" + note: "The previous expectation (enabled:false -> none) encoded the bug: it + gated teardown on a browser-local flag, orphaning the mirror across devices. + The corrected spec is STRICTER — teardown fires whenever the feature is on — + and adds the enabled:undefined case. The feature-flag gate assertion is + retained, so no real gate was weakened." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-23 + file: tests/nostr-delete-flow.test.mjs + change_type: ["spec_correction", "refactor_tests"] + scenarios: + - id: SCN-delete-surfaces-relay-failures + given: "A logged-in delete where one write relay rejects the kind-5 deletion" + when: "deleteAllVersions publishes to the WRITE set via getDeletePublishRelays() (the afb6200b relay-scope fix)" + then: "The delete summary surfaces the failing relay" + - id: SCN-buffered-feed-tombstone-guard + given: "A delete event seeds a tombstone, then an OLDER non-deleted event for the same root arrives on the live stream" + when: "events flow through the real VideoEventBuffer (the path subscribeVideos builds via the SubscriptionManager), not the removed direct pool.sub() injection" + then: "The older event is tombstone-guarded (marked deleted, kept out of activeMap) and never surfaces as a LIVE video; a deletion event surfaces marked deleted:true so the UI removes the card" + observable_outcomes: + - "deleteSummary.summary.failed identifies wss://relay.fail (writeRelays now configured so the injected failure is exercised)" + - "no callback video with the deleted/older event id and deleted:false" + - "client.activeMap cleared for the root; tombstone advanced; stored event marked deleted" + determinism_controls: + - "videoEventVerifier virtualizes the off-thread signature worker (all events valid)" + - "buffer.scheduleFlush(true) awaited for synchronous commit" + - "real NostrClient + real VideoEventBuffer (no mocked tombstone guard)" + anti_cheat_rationale: + prevents: + - "over-mocking internal logic" + - "snapshot rubber-stamping" + note: "Two corrections. (1) The file used bare assert + top-level await, so the + unit runner (which requires a node:test import) SILENTLY EXCLUDED it — it had + rotted and was failing on main unnoticed. Converted to node:test so it runs + in CI. (2) The relay-failure test set only client.relays, but deletes moved to + the write set (getDeletePublishRelays); it now sets writeRelays so the + injected failure is actually exercised — the assertion was previously passing + vacuously. (3) The subscription assertions drove the REMOVED pool.sub() path + and expected 'no callback at all' for deleted events; the current + VideoEventBuffer intentionally surfaces deleted events marked deleted:true so + the UI can remove the card. Rewritten to drive the real buffer + real client + and assert the stricter, accurate invariant (never surfaces as LIVE; guarded + and removed from activeMap). Coverage is stronger: the buffer's own unit test + mocks the client's tombstone guard, whereas this exercises the real guard." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-23 + file: tests/watch-history.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-watch-history-partial-acceptance-durable + given: "A logged-in user snapshots watch history to 3 write relays where 2 accept and 1 rejects (single attempt, no escalation)" + when: "watchHistoryService.snapshot runs after the 565a9618 fix (a snapshot is durable as soon as ANY relay accepts; reads take the newest event per d-tag)" + then: "The snapshot SUCCEEDS — it does not throw, the result is ok and not retryable, no republish is scheduled, the month result is flagged partial with the accepting/rejecting relays captured, and the pointer queue is emptied (the pointer was durably published)" + - id: SCN-watch-history-logged-in-not-session + given: "A logged-in user (nostrClient.pubkey set) records a watch and the entry is published and re-loaded" + when: "publishView resolves the owning actor as the logged-in pubkey and sets the session flag only when nobody is logged in" + then: "The round-tripped entry is NOT flagged session=true (it is the logged-in user's history, not a local-only session entry); the queue files under the logged-in pubkey so getQueuedPointers(actor) sees it" + observable_outcomes: + - "snapshot() resolves (does not throw); result.ok === true; result.retryable === false" + - "scheduleWatchHistoryRepublish NOT called" + - "result.results[0].partial === true; relayStatus.pointer shows relay.one/two accepted, relay.three rejected" + - "getQueuedPointers(actor).length === 0 after a successful snapshot" + - "resolvedItems[0].session !== true for a logged-in user" + determinism_controls: + - "poolHarness resolver with a fixed accept/reject plan per relay url" + - "session crypto + recordVideoView virtualized; stable relay set" + - "logged-in pubkey aligned with the queried actor (matches the convention used by the other tests in the file)" + anti_cheat_rationale: + prevents: + - "snapshot rubber-stamping" + - "hard-coded return value" + note: "Three corrections in this file, all matching shipped behavior. (1) The + partial-acceptance test asserted the INVERSE of intended behavior — that + snapshot() THROWS on partial acceptance and retries until EVERY relay accepts — + encoding exactly the all-relays false-failure bug 565a9618 fixed. Rewritten to + the success path (no throw, partial flag, per-relay status, queue drained, no + spurious republish). (2) testWatchHistoryServiceIntegration set the logged-in + pubkey to a value DIFFERENT from the actor it queried; since watch history now + files under the logged-in pubkey, the queue assertions looked at the wrong key. + Aligned pubkey to the queried actor (the convention every other test uses) — a + setup fix, no assertion weakened. (3) The same test asserted session===true for + a logged-in watch, which is the pre-fix bug (logged-in watches were wrongly + flagged session by comparing against the view actor); corrected to session!==true + per the current rule that session marks only logged-out, local-only entries. + With these the full file is green and was REMOVED from the run-unit-tests + QUARANTINE." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-23 + files: + - tests/zap-shared-state.test.mjs + - tests/video-modal-zap.test.mjs + change_type: ["spec_correction", "refactor_tests"] + scenarios: + - id: SCN-platform-fee-unparseable-override + given: "An unparseable platform-fee override value (e.g. 'not-a-number') with PLATFORM_FEE_PERCENT configured > 0" + when: "resolvePlatformFeePercent runs" + then: "It falls back to the configured platform default (getDefaultPlatformFeePercent), NOT to 0 — a junk override cannot silently disable/bypass the platform fee" + - id: SCN-zap-completion-state-resets-then-completes + given: "A zap whose receipts validate to a warning (awaiting validated receipt)" + when: "sendZap runs" + then: "The modal completion state is reset to false at the start of the send and set true at the terminal path, so two completion-state changes are recorded and the end state is true" + - id: SCN-zap-receipts-render-into-controller-element + given: "VideoModal.renderZapReceipts called with one validated (or one failed) receipt" + when: "rendering delegates to the inner zapController" + then: "Exactly one entry is rendered into the controller's modalZapReceipts element" + observable_outcomes: + - "resolvePlatformFeePercent('not-a-number') === getDefaultPlatformFeePercent() (> 0)" + - "completedStates grows by 2 (false then true) on a completing send; end state true" + - "controller.modalZapReceipts.children.length === 1 per rendered receipt" + determinism_controls: + - "pure fee function with explicit inputs; configured PLATFORM_FEE_PERCENT read via getDefaultPlatformFeePercent (not hard-coded)" + - "modal stub records setZapCompleted/resetZapForm calls; DummyElement DOM" + anti_cheat_rationale: + prevents: + - "hard-coded return value" + - "over-mocking internal logic" + note: "Three corrections, all to match shipped behavior. (1) The fee-fallback test + asserted 0; the function correctly falls back to the configured platform default + so a garbage override cannot zero out (bypass) the fee — corrected to assert + against getDefaultPlatformFeePercent and that the result is > 0 (STRICTER: pins + the no-bypass guarantee). (2) The completion-count test asserted +1; sendZap + legitimately resets completed=false at the start of every send and sets true at + the terminal path (+2), with the end-state==true assertion retained as the real + invariant. (3) The receipt-render tests set modalZapReceipts on the outer + VideoModal, but rendering was refactored to delegate to the inner zapController + which owns that element — set/assert on the controller's element (a harness fix, + no assertion weakened). NOTE: these accompany three PRODUCTION bug fixes found + via the same tests — NWC getPublicKey hex->bytes (connection was broken), the + in-flight 'Sending…' status mis-toned as warning, and the success path not using + the holistic resetZapForm." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-23 + file: tests/video-modal-zap.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-zap-paid-but-receipt-unconfirmed-is-success + given: "A zap share whose PAYMENT succeeded (NWC returned a preimage) but whose kind-9735 zap receipt could not be validated on the advertised relays" + when: "sendZap finishes" + then: "It is reported as a SUCCESS (Sent N sats) with a soft note that the on-relay receipt couldn't be confirmed — NOT as a warning/error. The form resets and the attempt is marked completed." + observable_outcomes: + - "last zap status tone === 'success' (was 'warning')" + - "status message matches /sent 750 sats/i and /couldn't confirm the zap receipt/i (was /awaiting validated zap receipt/i)" + - "receipts rendered in partial mode; form reset; completedStates +2 ending true" + determinism_controls: + - "injected splitAndZap returns a paid receipt (status:success, payment.preimage) with validation.status:'failed'" + anti_cheat_rationale: + prevents: + - "snapshot rubber-stamping" + - "hard-coded return value" + note: "The previous expectation asserted the BUG: a paid share whose 9735 receipt + wasn't found was surfaced as a 'warning' error ('Awaiting validated zap receipt') + and routed through notifyError — telling users a SUCCESSFUL zap had failed + (confirmed live: the platform fee landed but the UI showed a red error). The NWC + payment preimage is proof the invoice was paid; the 9735 receipt is supplementary + and is commonly not seen by a static client (relay coverage/timing). Corrected to + assert success-with-a-soft-note. Genuine PAYMENT failures (status !== 'success') + still go through the failure/retry path, unchanged." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-23 + file: tests/zap-split.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-zap-receipt-found-by-indexed-tags + given: "A published kind-9735 zap receipt for a video zap (the zap request carries #p recipient + #e event tags)" + when: "validateZapReceipt queries relays for the receipt" + then: "It filters by the reliably-indexed #e + #p tags (not #bolt11, which most relays don't index), then confirms via author pubkey + bolt11 + description hash" + observable_outcomes: + - "the relay query filter has filters[0]['#e'] === [eventId] and ['#p'] === [recipient]" + - "filters[0]['#bolt11'] is absent" + - "a matching receipt still validates as status:'passed'" + determinism_controls: + - "mock SimplePool.list captures the filter; fixed zap request tags + test bolt11 with matching description hash" + anti_cheat_rationale: + prevents: + - "snapshot rubber-stamping" + - "over-mocking internal logic" + note: "The previous test asserted filters[0]['#bolt11'][0] === invoice — encoding the + bug. Relays generally do not index arbitrary tags like bolt11, so that query found + nothing and a successfully-paid zap was reported as having no receipt. Corrected to + assert the reliably-indexed #e/#p anchors (added an #e tag to the request to model a + real video zap). The receipt-matching assertions (passed status, correct event, + checked relays) are unchanged/retained — coverage is stronger, not weaker." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-24 + file: tests/view-counter.test.mjs + change_type: ["spec_correction", "flake_fix"] + scenarios: + - id: SCN-view-counter-legacy-path + given: "The view-counter unit harness mocks the legacy per-pointer view APIs (list/count/subscribe) on the singleton client" + when: "A pointer is subscribed and hydrated" + then: "viewCounter exercises the legacy per-pointer hydrate/subscribe path (the one viewCounter.js explicitly designates 'for tests / mocks'), so the mocked APIs actually fire" + - id: SCN-view-counter-dedupe-window-aligned + given: "Two views by the same viewer within one fixed dedupe window" + when: "Counted" + then: "They dedupe to one (floor(created_at/window) bucket, matching the replaceable view-event d-tag)" + observable_outcomes: + - "hydration triggers a list()/count() call (legacy path) instead of the batched SubscriptionManager path silently taking over" + - "two same-pubkey views in one aligned bucket yield total === 1" + determinism_controls: + - "nostrClient.getSubscriptionManager pinned to () => null so the path is deterministic" + - "the dedupe test's base timestamp is aligned to a window-bucket boundary (was Date.now(), which made the +window/2 offset straddle a boundary depending on time of day)" + anti_cheat_rationale: + prevents: + - "over-mocking internal logic" + - "retry/sleep-based flake masking" + note: "The batched SubscriptionManager path was added later (relay-storm work); the + test mocks the legacy APIs but never pinned getSubscriptionManager, so the real + singleton's manager took over and the mocks never fired. Pinning it to null restores + the intended (and code-designated 'tests/mocks') legacy-path coverage. The dedupe + assertion (count === 1) is unchanged — only its setup was made deterministic." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +```yaml +test_integrity_note: + date: 2026-06-24 + file: tests/view-counter.test.mjs + change_type: ["spec_correction"] + scenarios: + - id: SCN-hydrate-root-timestamp + given: "A latest video revision whose root event is fetched by hydrateVideoHistory" + when: "History is hydrated, then the timestamp-sync step runs" + then: "rootCreatedAtByRoot caches the earliest (root) created_at, AND the active video's rootCreatedAt FIELD is set by syncActiveVideoRootTimestamp" + observable_outcomes: + - "nostrClient.rootCreatedAtByRoot.get(rootId) === root.created_at (hydrateVideoHistory's direct output)" + - "after syncActiveVideoRootTimestamp(...), latestVideo.rootCreatedAt === root.created_at" + determinism_controls: + - "pool.get returns a fixed root event; rootCreatedAtByRoot seeded empty" + anti_cheat_rationale: + prevents: + - "snapshot rubber-stamping" + note: "hydrateVideoHistory's contract is to populate the per-root created_at MAP; the + video OBJECT's rootCreatedAt field is applied by a separate util + (syncActiveVideoRootTimestamp) after a refactor moved that unit boundary. The test + previously expected hydrateVideoHistory to mutate the passed object directly, which it + no longer does. Corrected to assert the map (its real output) and to drive the sync + util for the field — stronger, not weaker (it now exercises the full real pipeline)." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +--- + +## 2026-06-25 — todo-11b triage: un-quarantine nostr-boost-actions + admin-list-store + +```yaml +test_integrity_note: + change_type: ["spec_correction", "refactor_tests"] + scenarios: + - id: SCN-repost-optional-relay + given: "A repost is built with no relay hint available (buildRepostEvent, nostrEventSchemas.js)" + when: "buildRepostEvent({ eventId, authorPubkey, targetKind }) is called without eventRelay" + then: "It returns a VALID repost (kind 16 for a non-kind-1 target, two-element ['e', id] tag, ['p', author], ['k', kind]) instead of throwing" + - id: SCN-community-blacklist-merge-batched + given: "An admin community-blacklist sources list references two curator kind-30000 lists, each with p-tag members" + when: "loadAdminState() runs and fetches curator lists via the BATCHED SubscriptionManager path" + then: "adminState.blacklist merges the curator members with the direct entry (deduped, guards respected)" + observable_outcomes: + - "buildRepostEvent output shape (kind/tags) with no relay — asserted exactly, stricter than the old throws-check" + - "adminState.blacklist === [direct, communityMemberOne, communityMemberTwo]" + determinism_controls: + - "pure builder (no IO) for repost; in-memory list registry + mocked pool.list AND getSubscriptionManager for the admin store" + anti_cheat_rationale: + prevents: + - "hard-coded return value" + - "over-mocking internal logic" + note: | + nostr-boost-actions: STALE SPEC. The test required buildRepostEvent to throw + /missing-event-relay/ when no relay hint is given, but a relay hint is a NIP-18 + SHOULD (not MUST). The builder intentionally degrades to a two-element `e` tag so a + user can always repost. Replaced the throws-assertion with an EXACT assertion of the + valid output (kind 16 + e/p/k tags) — strictly stronger. + + admin-list-store: STALE HARNESS, no production change, no assertion weakened. Three + fidelity fixes so the test exercises the real batched community-blacklist path: + (1) curator lists are now fetched via the batched SubscriptionManager (cold-start + relay-storm fix), which the harness didn't mock — added a getSubscriptionManager().list + mock serving the same registry; (2) curator coordinates now use REAL 64-char hex + pubkeys (the old mock-"hex" started with "npub", so parseCommunityBlacklistReferences' + isHexPubkey()/npub branch decoded them twice and the batched filter no longer matched); + (3) createListEvent now emits a `d` tag (real kind-30000 lists always have one; + selectNewestEventsForReferences matches curator events to references by their d tag). + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-07-05 — todo-11b final: un-quarantine nwc-client (+ user-blocks / nostr-publish-rejection hangs) + +```yaml +test_integrity_note: + change_type: ["spec_correction", "refactor_tests", "flake_fix"] + scenarios: + - id: SCN-nwc-unsupported-encryption-scheme + given: "An NWC wallet whose info event (kind 13194) advertises ONLY an encryption scheme bitvid does not implement (nip44_v3)" + when: "ensureEncryptionForContext(context) negotiates a scheme" + then: "It rejects with message /Wallet advertises unsupported encryption schemes: nip44_v3\\./ AND error.code === 'UNSUPPORTED_ENCRYPTION'" + - id: SCN-nwc-mock-toolkit-selection + given: "Each nwc-client section installs a per-section window.NostrTools mock to drive encryption-scheme selection" + when: "The nostr-tools bootstrap has installed its frozen canonical toolkit on the global scope" + then: "Removing scope.__BITVID_CANONICAL_NOSTR_TOOLS__ lets readToolkitFromScope fall through to the section mock, so the wiring under test runs against the mock (not real @noble against fake keys)" + observable_outcomes: + - "ensureEncryptionForContext rejection message + error.code (UNSUPPORTED_ENCRYPTION) — asserted exactly" + - "nip44_v2 section still resolves via the mock's nip44 (assert.match on the mock ciphertext shape)" + determinism_controls: + - "per-section in-memory window.NostrTools mocks; canonical-toolkit removal is deterministic (one-time, nothing re-installs it); no network" + anti_cheat_rationale: + prevents: + - "over-mocking internal logic" + - "hard-coded return value" + note: | + nwc-client: TWO issues. (1) MOCK SHADOW (not a spec change): the bootstrap installs a + frozen canonical toolkit that readToolkitFromScope() prefers over window.NostrTools, so + the per-section mocks never took effect and real @noble ran against the fake test keys + ("bad point: is not on curve"). Deleting the scope canonical lets the resolver use each + section's mock — the tests exercise the NWC client's encryption-SELECTION wiring, not + @noble crypto (which is real elsewhere; the parseNwcUri hex-vs-bytes production bug is + guarded by tests/nwc-parse-uri.test.mjs). (2) SPEC CORRECTION: one section asserted a + rejection when the toolkit "lacks nip44" by omitting nip44 from the mock. That precondition + is architecturally unreachable — the bootstrap's mergeWithCanonical ALWAYS backfills the + canonical nip44 (even an explicit `nip44: null` is overwritten by `!merged.nip44`), so + nip44_v2 is never an unsupported scheme. Replaced with a wallet advertising a scheme bitvid + genuinely cannot provide (nip44_v3), exercising the SAME UNSUPPORTED_ENCRYPTION rejection + via a reachable scenario, and strengthened the check to also assert error.code — strictly + stronger than the old message-only match. + + user-blocks + nostr-publish-rejection: FLAKE_FIX (no assertion changed). Both are + bare-assert files that PASS all assertions then hang on lingering handles after completion + (user-blocks: a never-resolving decrypt's background retry timer that manager.reset() doesn't + cancel; nostr-publish-rejection: js/app.js + subscriptions + userBlocks module-load + timers/connection managers). Applied the repo's established post-completion + `setTimeout(() => process.exit(0), 50)` exit after cleanup. No behavior or expectation was + altered — the process simply exits once the assertions have all run. + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-07-05 — todo-11: uploadModal-reset zombie-callback test made deterministic (correct mock boundary) + +```yaml +test_integrity_note: + change_type: ["flake_fix", "refactor_tests"] + scenarios: + - id: SCN-upload-zombie-callback-guard + given: "An upload is in progress (videoUploadState.status === 'uploading') via a controllable pending mediaUploader.uploadVideo" + when: "resetUploads() runs mid-upload (bumping videoUploadId), then the in-flight upload later resolves (a 'zombie' completion)" + then: "The stale completion is ignored — status stays 'idle' and results.videoUrl stays '' — because handleVideoSelection guards on videoUploadId !== currentUploadId" + observable_outcomes: + - "videoUploadState.status: 'uploading' after start, 'idle' after reset, still 'idle' after the zombie resolve" + - "results.videoUrl.value === '' after the zombie resolve" + - "test exits 0 with no async-after-test activity (was leaking webtorrent/Blob errors)" + determinism_controls: + - "mediaUploader.uploadVideo replaced with a manually-resolved promise; captureVideoMetadata stubbed; jsdom DOM; no webtorrent, no network, no real Blob ops" + anti_cheat_rationale: + prevents: + - "over-mocking internal logic" + - "retry/sleep-based flake masking" + note: | + FLAKE_FIX + REFACTOR_TESTS, no assertion weakened. The subtest PASSED its asserts but + leaked async activity after the test ended, which node:test flags as a failure. Root + cause: it mocked modal.generateTorrentMetadata / resolveUploadIdentifier, but + handleVideoSelection drives the upload through this.mediaUploader.uploadVideo — a + DIFFERENT object with its OWN helpers — so the mediaUploader still ran real + createTorrentMetadata()/hashing on a synthetic {name:'video.mp4'} file ("invalid input + type" async), and captureVideoMetadata() ran URL.createObjectURL(file) ("must be an + instance of Blob"). Fixed by mocking at the correct DI boundary + (modal.mediaUploader.uploadVideo returns the controllable pending promise) and stubbing + captureVideoMetadata. The unit under test is the modal's videoUploadId zombie-guard, so + mocking the uploader boundary is correct isolation, not over-mocking (real upload/torrent + paths are covered by the mediaUploader / s3-upload suites). + mutation_verification: + - "Broke the guard (`if (this.videoUploadId !== currentUploadId) return` → `if (false)`) → the test FAILS ('State should remain idle after zombie completion', # fail 1). Reverted." + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-07-05 — todo-11b: un-quarantine dm-block-filter (hermetic subscription; no force-exit) + +```yaml +test_integrity_note: + change_type: ["flake_fix"] + scenarios: + - id: SCN-dm-block-filter-hermetic + given: "The 'integration with loaded user block list' test loads standard + legacy block lists and filters incoming DMs" + when: "manager.loadBlocks() runs against stubbed fetchListIncrementally, with the live block-list subscription stubbed out" + then: "Both blocked contacts (standard + legacy) are filtered (dmMessages === [ALLOWED]) AND the process opens NO real relay sockets, so node:test exits naturally with the correct exit code" + observable_outcomes: + - "manager.isBlocked(legacy) && manager.isBlocked(standard) === true; dmMessages length 1 (ALLOWED only)" + - "process.getActiveResourcesInfo() shows no TCPSocketWrap after loadBlocks; node exits 0 on pass, 1 on a forced failure (verified — failures NOT masked)" + determinism_controls: + - "stubbed nostrClient.fetchListIncrementally; truthy stub nostrClient.pool; no-op nostrClient.getSubscriptionManager; window.nostr.nip04.decrypt stub. All restored in finally. No network." + anti_cheat_rationale: + prevents: + - "retry/sleep-based flake masking" + - "over-mocking internal logic" + note: | + FLAKE_FIX, no assertion changed. The file PASSED all subtests then hung: loadBlocks() + opens a LIVE block-list subscription through nostrClient's real SimplePool aimed at the + test's fake relay URLs, leaking TCP sockets + reconnect timers that outlive the run so + node:test never exits (surfaced only once the runner was given a per-file timeout). + Diagnosed with process.getActiveResourcesInfo() — 4 TCPSocketWrap + reconnect Timeouts; + NOT the decrypt-retry timer (verified absent). Nulling the pool did not help because + loadBlocks calls ensurePool() (userBlocks.js:967) to re-create+connect a real pool when + pool is falsy. Fix keeps the test hermetic: a TRUTHY stub pool (so ensurePool is skipped) + + a no-op getSubscriptionManager (so the subscription opens nothing). The block-merge + logic under test still runs fully through the stubbed fetchListIncrementally. A force + `process.exit` in an after() hook was explicitly REJECTED after verifying it masks real + failures (process.exitCode is not set when the hook's timer fires — a deliberately broken + assertion still exited 0 under that approach). Under this fix, the forced-failure control + correctly exits 1. + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + + +--- + # client-reliability-1 ```yaml From dbdac18df1a4957cf15f47afdb205e28d7cb1724 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Mon, 19 Oct 2026 16:09:26 -0400 Subject: [PATCH 3/6] client-reliability-1: preserve native hidden-attribute behavior in style fixture --- TEST_INTEGRITY.md | 4 +++- test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md | 1 + tests/profile-modal-controller.test.mjs | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/TEST_INTEGRITY.md b/TEST_INTEGRITY.md index 70cdd1b6..c6567add 100644 --- a/TEST_INTEGRITY.md +++ b/TEST_INTEGRITY.md @@ -599,7 +599,7 @@ test_integrity_note: change_type: ["flake_fix", "new_tests"] scenarios: - id: SCN-profile-focus-style-contract - given: "modal controls include visible, disabled, CSS-hidden, inherited-hidden, aria-hidden, and hidden-input cases" + given: "modal controls include visible, disabled, CSS-hidden, inherited-hidden, aria-hidden, hidden-input, and hidden-attribute cases" when: "the real controller collects focus targets using fixture and native JSDOM styles, and the user tabs from the last control" then: "both resolvers retain only visible enabled controls and Tab wraps to the first" observable_outcomes: @@ -622,3 +622,5 @@ 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. diff --git a/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md b/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md index d8b4ce41..6596361f 100644 --- a/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md +++ b/test_logs/TEST_LOG_2026-10-19_20-10-00_client-reliability.md @@ -10,3 +10,4 @@ - Deliberate product mutant removing display/visibility filtering: final regression exits 1, 1 failed/0 passed, hidden control IDs appear in actual focus list. Product restored byte-for-byte. Initial DOM-object diff diagnostic interrupted (130); replaced with control-ID diagnostics, no assertion relaxation. - Initial sandbox lint: exit 2, git subprocess EPERM. Unrestricted lint then exposed forbidden direct style assignments in test fixture; corrected to fixture stylesheet classes. `npm run lint` unrestricted final run: exit 0, all 11 stages passed. - Early full-unit diagnostic interrupted (130) before completion while final fixture was being prepared; no full pass claimed. Final committed/merged-tree checks follow in a separate log. +- Merged-tree isolated check at d0224078: exit 0, 27/27, 2700.15ms; lint 11/11 exit 0. Full diagnostic stopped before completion (130) for a fixture correction: preserve native `[hidden]` file-input display filtering. Added this control to the same contract scenario. Corrected isolated suite exits 0, 27/27. Fixture mutant omitting `[hidden]` filtering exits 1, 1 failed/0 passed; fixture restored. The full suite will restart on the final committed tree. diff --git a/tests/profile-modal-controller.test.mjs b/tests/profile-modal-controller.test.mjs index bd095543..ceb31b76 100644 --- a/tests/profile-modal-controller.test.mjs +++ b/tests/profile-modal-controller.test.mjs @@ -137,7 +137,7 @@ beforeEach(() => { document.head.appendChild(focusStyles); nativeGetComputedStyle = window.getComputedStyle.bind(window); window.getComputedStyle = (element) => ({ - display: element.matches('.focus-test-display-none, input[type="hidden"]') ? 'none' : 'block', + display: element.matches('.focus-test-display-none, [hidden], input[type="hidden"]') ? 'none' : 'block', visibility: element.closest('.focus-test-visibility-hidden') ? 'hidden' : 'visible', }); @@ -1095,6 +1095,7 @@ test('focus trap excludes hidden and disabled controls with fixture and native s addControl('visibility-hidden', (control) => { control.classList.add('focus-test-visibility-hidden'); }); addControl('aria-hidden', (control) => { control.setAttribute('aria-hidden', 'true'); }); addControl('hidden-input', (control) => { control.type = 'hidden'; }); + addControl('hidden-attribute', (control) => { control.hidden = true; }); const inherited = addControl('inherited-visibility'); const hiddenParent = document.createElement('div'); hiddenParent.classList.add('focus-test-visibility-hidden'); From 6da037f59bace7dbf08cf09bacb541f39fe69a7d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Mon, 19 Oct 2026 16:13:49 -0400 Subject: [PATCH 4/6] client-reliability-1: record checked fix and newly exposed full-gate blocker --- docs/TODO.md | 2 ++ docs/agents/backlog/client-reliability-1.json | 4 ++-- ...LOG_2026-10-19_20-13-00_client-reliability-final.md | 10 ++++++++++ todo/TODO_2026-10-19_20-13-00_client-reliability.md | 6 ++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md create mode 100644 todo/TODO_2026-10-19_20-13-00_client-reliability.md diff --git a/docs/TODO.md b/docs/TODO.md index e7802af7..e88843ea 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -9,3 +9,5 @@ ## client-reliability - 2026-10-19: client-reliability-1 assigned: reproduce/classify and fix profile-modal suite hang without weakening assertions. Initial evidence: three isolated 60s timeouts (one clean environment), full baseline stopped at file 220/425; read docs/agents/prompts/client-reliability.md and claim the item. Scope excludes app.js and unrelated subsystems. + +- 2026-10-19: client-reliability-1 fix committed at dbdac18d (test-only fixture, original assertions preserved; native focus contract and both mutants fail when broken). Isolated27/27 natural exit2.44s, lint11/11. Full60s/file run passed226/425 files including profile-modal; file227 tests/race/nostr-client-init-race.test.mjs timed out;198 later files untested. Await dispatch routing for this unowned blocker before normal landing/close. No product/browser/build qualification or landing claimed. See test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md. diff --git a/docs/agents/backlog/client-reliability-1.json b/docs/agents/backlog/client-reliability-1.json index 607c321e..012f2bd6 100644 --- a/docs/agents/backlog/client-reliability-1.json +++ b/docs/agents/backlog/client-reliability-1.json @@ -1,9 +1,9 @@ { "id": "client-reliability-1", "area": "client-reliability", - "status": "in-progress", + "status": "blocked", "title": "Diagnose and fix profile-modal suite hang without weakening assertions", - "blockedOn": null, + "blockedOn": "dispatch: unowned tests/race/nostr-client-init-race.test.mjs retains background relay work; full gate fails after 226/425 passed; fix dbdac18d passes isolated27/27 and lint11/11", "addedOn": "2026-10-19", "filedAt": "8b204687d2c0020c0fe97cedb3e9f8661a8d709a" } diff --git a/test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md b/test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md new file mode 100644 index 00000000..389baee1 --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md @@ -0,0 +1,10 @@ +# client-reliability-1 final-tree validation: dbdac18df1a4957cf15f47afdb205e28d7cb1724 + +- Branch contains unstable e106a15e, merged at d0224078; final `git merge unstable` reported already up to date. +- `env -u NODE_TEST_CONTEXT NODE_ENV=test node --import ./tests/test-helpers/setup-localstorage.mjs --test-reporter=spec tests/profile-modal-controller.test.mjs`: exit 0 naturally; 27 tests passed, 0 failed/cancelled/skipped/todo; 2442.95ms. +- `npm run lint` (unrestricted because sandbox git subprocess EPERM): exit 0, all 11 lint stages passed. +- `env -u NODE_TEST_CONTEXT UNIT_TEST_TIMEOUT_MS=60000 npm test`: exit 1. 227/425 files started; first 226 exited 0 including tests/profile-modal-controller.test.mjs; file 227 tests/race/nostr-client-init-race.test.mjs aborted after 60000ms. 198 files not run. Full suite is NOT green. Runner reports file outcomes, not an aggregate scenario count. +- Product filtering mutant: exit 1, 1 failed/0 passed. Fixture hidden-attribute mutant: exit 1, 1 failed/0 passed. Both restored. No product source changes remain. +- Not verified: later 198 unit files, browser behavior, full application CSS/layout, signer/wallet/network behavior, build, release/deployment. Test-only change; no browser/build qualification claimed. +- No queue submission while full gate is blocked. Dispatch notified via mailbox 20261019T201304-client-reliability-a7c828.md of the newly exposed unowned race-suite timeout; waiting for routing/scope decision. +- Read-only isolated race diagnostic: `env -u NODE_TEST_CONTEXT NODE_ENV=test timeout 10s node --import ./tests/test-helpers/setup-localstorage.mjs --test-reporter=spec tests/race/nostr-client-init-race.test.mjs`: exit124; its single test reports PASS in60.91ms, then process retains background relay retries. This is a retained-resource/harness-cleanup boundary, distinct from the profile suite's synchronous style cost. No race or Nostr source changed. diff --git a/todo/TODO_2026-10-19_20-13-00_client-reliability.md b/todo/TODO_2026-10-19_20-13-00_client-reliability.md new file mode 100644 index 00000000..aac61f10 --- /dev/null +++ b/todo/TODO_2026-10-19_20-13-00_client-reliability.md @@ -0,0 +1,6 @@ +- [x] Claim/revalidate client-reliability-1 at startup HEAD9e6c310d. +- [x] Classify timeout as expensive JSDOM computed styles; baseline26/26 natural completion57.04s. +- [x] Implement test-only fixture; preserve all assertions; native-JSDOM contract and two failing mutants. +- [x] Commit, merge unstable and verify isolated27/27 and lint11/11 on dbdac18d. +- [ ] Full unit gate:226 files passed; file227 Nostr-client race timed out60s;198 untested. +- [ ] Dispatch routing of unowned race-suite blocker, then normal full gate, queue submission, durable landing and close. From 181eae95d9bb9626cfbca9904652731fe7cd49aa Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Mon, 19 Oct 2026 16:22:57 -0400 Subject: [PATCH 5/6] client-reliability-2: make concurrent-init test hermetic and close feed transport --- TEST_INTEGRITY.md | 39 +++++++ ...026-10-19_20-22-00_client-reliability-2.md | 5 + ...026-10-19_20-24-00_client-reliability-2.md | 8 ++ docs/agents/backlog/client-reliability-2.json | 2 +- ...026-10-19_20-24-00_client-reliability-2.md | 10 ++ tests/race/nostr-client-init-race.test.mjs | 102 ++++++++++++------ ...026-10-19_20-24-00_client-reliability-2.md | 6 ++ 7 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 context/CONTEXT_2026-10-19_20-22-00_client-reliability-2.md create mode 100644 decisions/DECISIONS_2026-10-19_20-24-00_client-reliability-2.md create mode 100644 test_logs/TEST_LOG_2026-10-19_20-24-00_client-reliability-2.md create mode 100644 todo/TODO_2026-10-19_20-24-00_client-reliability-2.md diff --git a/TEST_INTEGRITY.md b/TEST_INTEGRITY.md index c6567add..90ba84e7 100644 --- a/TEST_INTEGRITY.md +++ b/TEST_INTEGRITY.md @@ -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. diff --git a/context/CONTEXT_2026-10-19_20-22-00_client-reliability-2.md b/context/CONTEXT_2026-10-19_20-22-00_client-reliability-2.md new file mode 100644 index 00000000..d5a1e45b --- /dev/null +++ b/context/CONTEXT_2026-10-19_20-22-00_client-reliability-2.md @@ -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. diff --git a/decisions/DECISIONS_2026-10-19_20-24-00_client-reliability-2.md b/decisions/DECISIONS_2026-10-19_20-24-00_client-reliability-2.md new file mode 100644 index 00000000..84189538 --- /dev/null +++ b/decisions/DECISIONS_2026-10-19_20-24-00_client-reliability-2.md @@ -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. diff --git a/docs/agents/backlog/client-reliability-2.json b/docs/agents/backlog/client-reliability-2.json index 9547875c..1bbfcbd9 100644 --- a/docs/agents/backlog/client-reliability-2.json +++ b/docs/agents/backlog/client-reliability-2.json @@ -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", diff --git a/test_logs/TEST_LOG_2026-10-19_20-24-00_client-reliability-2.md b/test_logs/TEST_LOG_2026-10-19_20-24-00_client-reliability-2.md new file mode 100644 index 00000000..eadfb84c --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-19_20-24-00_client-reliability-2.md @@ -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. diff --git a/tests/race/nostr-client-init-race.test.mjs b/tests/race/nostr-client-init-race.test.mjs index 6248270f..a5f24ffe 100644 --- a/tests/race/nostr-client-init-race.test.mjs +++ b/tests/race/nostr-client-init-race.test.mjs @@ -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"); }); diff --git a/todo/TODO_2026-10-19_20-24-00_client-reliability-2.md b/todo/TODO_2026-10-19_20-24-00_client-reliability-2.md new file mode 100644 index 00000000..a8283652 --- /dev/null +++ b/todo/TODO_2026-10-19_20-24-00_client-reliability-2.md @@ -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. From 683cedf0bf8ad7c599dacf9d448cecb93ca84ce4 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Mon, 19 Oct 2026 16:29:08 -0400 Subject: [PATCH 6/6] client-reliability-1 client-reliability-2: record full 425-file validation and unblock landing --- docs/TODO.md | 2 ++ docs/agents/backlog/client-reliability-1.json | 4 ++-- ...LOG_2026-10-19_20-35-00_client-reliability-both.md | 11 +++++++++++ ...ODO_2026-10-19_20-35-00_client-reliability-both.md | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md create mode 100644 todo/TODO_2026-10-19_20-35-00_client-reliability-both.md diff --git a/docs/TODO.md b/docs/TODO.md index 60655790..a60966a0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -13,3 +13,5 @@ - 2026-10-19: client-reliability-1 assigned: reproduce/classify and fix profile-modal suite hang without weakening assertions. Initial evidence: three isolated 60s timeouts (one clean environment), full baseline stopped at file 220/425; read docs/agents/prompts/client-reliability.md and claim the item. Scope excludes app.js and unrelated subsystems. - 2026-10-19: client-reliability-1 fix committed at dbdac18d (test-only fixture, original assertions preserved; native focus contract and both mutants fail when broken). Isolated27/27 natural exit2.44s, lint11/11. Full60s/file run passed226/425 files including profile-modal; file227 tests/race/nostr-client-init-race.test.mjs timed out;198 later files untested. Await dispatch routing for this unowned blocker before normal landing/close. No product/browser/build qualification or landing claimed. See test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md. + +- 2026-10-19: client-reliability-1 and client-reliability-2 ready for normal landing at181eae95: profile27/27 and race1/1 natural exit; all425 unit files passed with60s/file bound; lint11/11. Four deliberately broken variants fail and are reverted. Dispatch routing unblocked the second item; no product Nostr/shared-runner changes. Remaining: durable normal queue landing, unstable verification, then backlog close. Browser/build and real IO unverified (test-only scope). See test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md. diff --git a/docs/agents/backlog/client-reliability-1.json b/docs/agents/backlog/client-reliability-1.json index 012f2bd6..607c321e 100644 --- a/docs/agents/backlog/client-reliability-1.json +++ b/docs/agents/backlog/client-reliability-1.json @@ -1,9 +1,9 @@ { "id": "client-reliability-1", "area": "client-reliability", - "status": "blocked", + "status": "in-progress", "title": "Diagnose and fix profile-modal suite hang without weakening assertions", - "blockedOn": "dispatch: unowned tests/race/nostr-client-init-race.test.mjs retains background relay work; full gate fails after 226/425 passed; fix dbdac18d passes isolated27/27 and lint11/11", + "blockedOn": null, "addedOn": "2026-10-19", "filedAt": "8b204687d2c0020c0fe97cedb3e9f8661a8d709a" } diff --git a/test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md b/test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md new file mode 100644 index 00000000..ee3c5ecd --- /dev/null +++ b/test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md @@ -0,0 +1,11 @@ +# Both reliability fixes: final validation at181eae95d9bb9626cfbca9904652731fe7cd49aa + +- `git merge unstable`: already up to date (contains dispatch routing and profile fix). +- `env -u NODE_TEST_CONTEXT NODE_ENV=test node --import ./tests/test-helpers/setup-localstorage.mjs --test-reporter=spec tests/race/nostr-client-init-race.test.mjs`: exit0 naturally; 1 passed/0 failed/cancelled/skipped, scenario10.32ms, total14.86ms. +- Same direct command for tests/profile-modal-controller.test.mjs: exit0 naturally;27 passed/0 failed/cancelled/skipped,2635.71ms. +- `npm run lint` unrestricted: exit0,11/11 stages. Sandbox git subprocess restrictions were the reason for unrestricted lint, not a code failure. +- `env -u NODE_TEST_CONTEXT UNIT_TEST_TIMEOUT_MS=60000 npm test`: exit0, all425/425 files completed,0 quarantined. Runner emits file outcomes, no aggregate scenario count. The previous profile file220 and race file227 both completed; all198 files beyond the prior blocker were reached. +- Profile mutants: product visibility exclusion removed ->1 failed; fixture hidden-attribute omission ->1 failed. Race mutants: historical early-return init ->1 failed; fixture connection failure ->1 failed. All exited1 and were reverted; no product Nostr files edited. See prior integrity/evidence entries. +- Scope: two test files plus notes/backlog only. No product changes, timeouts/retries/sleeps/forced exits/runner changes. No browser/build/real-signer/real-wallet/production or deploy verification claimed. +- Slowdown: baseline full-suite cutoffs hid the second resource leak; a scenario pass was insufficient to prove process completion. Improvement: a readable per-scenario reporter plus pending-resource diagnostics would distinguish style CPU cost from retained relay retries earlier. +- Normal landing queue submission follows; neither item is closed before durable landing plus unstable verification. diff --git a/todo/TODO_2026-10-19_20-35-00_client-reliability-both.md b/todo/TODO_2026-10-19_20-35-00_client-reliability-both.md new file mode 100644 index 00000000..9e2e5f31 --- /dev/null +++ b/todo/TODO_2026-10-19_20-35-00_client-reliability-both.md @@ -0,0 +1,5 @@ +- [x] client-reliability-1: preserve26 existing scenarios, add native style contract, mutation checks, profile27/27 natural exit. +- [x] client-reliability-2: deterministic pool barrier, successful IO fixture, subscription cleanup, preserve original assertions; race1/1 natural exit and both mutants fail. +- [x] Separate commits, merge unstable, lint11/11 and full425/425 file exits0 on181eae95. +- [ ] Unchanged normal queue gate and durable landing; verify unstable ancestry, then close both backlog items. +- Not verified: browser/build/application layout, real network/signers/wallets/production/deploy; test-only changes.