Files
bitvid/TEST_INTEGRITY.md

49 KiB

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).


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: ""

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."

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: ""

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: ""

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: ""

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: ""

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: ""

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: ""

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: ""

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

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)

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)

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)

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

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, 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:
    - "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:
    - "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:
      - "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: ""

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

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.


client-reliability-3

test_integrity_note:
  change_type: ["new_tests"]
  scenarios:
    - id: SCN-last-probe-owner-disposal
      given: "five real DOM cards register through actual gridHealth/cardObserver; four probes are active and one queued"
      when: "the feed is disposed/removed and active probes finish"
      then: "zero obsolete probes start"
    - id: SCN-shared-queued-owner
      given: "two cards share one queued torrent source"
      when: "one registration disposes while its DOM remains connected"
      then: "the live owner still probes and becomes healthy; disposed owner receives no stale UI/callback"
    - id: SCN-cancelled-replacement-inflight
      given: "last owner cancels queued work then a replacement requests the same source before old settlement"
      when: "old completion drains and another owner joins before a slot frees"
      then: "no unhealthy cancellation cache is written, only one new probe starts, and both replacements become healthy/cacheable"
    - id: SCN-probe-priority-concurrency
      given: "four active probes, queued mixed priorities and a cancelled source"
      when: "slots free one at a time"
      then: "four-slot maximum and stable priority order remain; cancelled work never starts"
    - id: SCN-hidden-healthy-reveal
      given: "a hide-all card is hidden pending liveness"
      when: "registration eagerly probes and returns healthy"
      then: "raw magnet/webseed bytes reach probe IO and the card reveals without an intersection"
    - id: SCN-active-shared-late-entry
      given: "an active source loses its owner, receives a late observer entry and gains a live owner"
      when: "the unchanged active probe completes"
      then: "no duplicate starts or disposed result writes; live owner becomes healthy"
    - id: SCN-repeat-disposal-reattach
      given: "a disposed container has a new queued registration"
      when: "the old disposer is called again"
      then: "new owner survives and becomes healthy"
    - id: SCN-ownerless-active-cache
      given: "an already active probe has no remaining owner"
      when: "it finishes before a later live card requests that source"
      then: "the active result remains cached; no new probe or stale UI write occurs"
    - id: SCN-existing-probe-cache-ttl
      given: "completed healthy and negative results"
      when: "cards request before, at, and after the existing TTL boundary"
      then: "both cached outcomes reuse until the existing inclusive TTL expires, then both probe again"
  observable_outcomes:
    - "actual probePeers start count/order, arguments and maximum active count"
    - "real DOM badge health/hidden state, observer disposal and callback counts"
    - "storage cache absence for cancellation and reuse/expiry for completed probes"
  determinism_controls:
    - "actual gridHealth/cardObserver and DOM, controllable observer and deferred probePeers IO"
    - "explicit viewport geometry, unique sources and isolated storage; fake Date.now only for TTL scenario"
    - "task-turn promise draining without wall-clock sleeps, polling or retries"
    - "native Chromium/Firefox observer smoke against built dist, external traffic blocked"
  anti_cheat_rationale:
    prevents:
      - "unconditional queue cancellation: shared-owner mutant fails"
      - "no-op cancellation, stale inflight deletion or cancelled cache poisoning"
      - "changed concurrency/priority or hidden eager path"
      - "stale result/listener/observer effects and repeated old disposal"
      - "cancelling or dropping cache from already active work"
      - "timeout/retry/sleep masking or hard-coded healthy results"
  relaxation:
    did_relax_any_assertion: false
    if_true_explain_spec_basis: ""

Grouped mutation coverage: last-owner cancellation mutant kills SCN-last-probe-owner-disposal; unconditional cancel kills SCN-shared-queued-owner; unconditional inflight deletion and cancelled-normalization mutants each kill SCN-cancelled-replacement-inflight; reversed queue priority kills SCN-probe-priority-concurrency; disabled eager path kills SCN-hidden-healthy-reveal; stale result guard and late-entry membership mutants each kill SCN-active-shared-late-entry; non-idempotent disposer kills SCN-repeat-disposal-reattach; owner-dependent active caching kills SCN-ownerless-active-cache; missing cache writes kills SCN-existing-probe-cache-ttl. All11 variants exit1 with1 failed scenario,0 cancelled, and are reverted. Browser checks corroborate the same disposal/shared-owner/hidden-reveal/source-visibility scenarios with native observers; no browser CPU or actual media fallback claim.

Additional client-reliability-3 scenarios (same invariant and controls):

test_integrity_note:
  change_type: ["new_tests"]
  scenarios:
    - id: SCN-shared-badge-listener-lifetime
      given: "two feed registrations share the app callback"
      when: "badge updates occur and registrations dispose one at a time"
      then: "each event notifies once, one surviving owner retains the callback, last disposal stops notifications"
    - id: SCN-checking-callback-disposal
      given: "a live card's checking callback synchronously disposes the feed"
      when: "badge notification returns to probe submission"
      then: "no probe is submitted after unregister"
  observable_outcomes:
    - "callback event count/state sequence and external probe start count"
  determinism_controls:
    - "real badge/observer path, synchronous disposal callback and deferred probe IO"
  anti_cheat_rationale:
    prevents:
      - "duplicated shared callbacks or dropping live callback owners"
      - "post-disposal work submitted after synchronous reentrant teardown"
  relaxation:
    did_relax_any_assertion: false
    if_true_explain_spec_basis: ""

Duplicate wrapper and unconditional listener release variants each fail SCN-shared-badge-listener-lifetime. Removing the post-checking membership guard fails SCN-checking-callback-disposal (actual starts1 vs expected0). These3 additional variants exit1 with1 failed/0 cancelled and are reverted; total14 killed variants for11 lifecycle scenarios. Original source/priority/observer expectations remain unchanged.

client-reliability-4 — queued URL media ownership

test_integrity_note:
  change_type: ["new_tests"]
  scenarios:
    - id: SCN-url-last-owner
      given: "four cards, three active media and one queued"
      when: "last registration is disposed and an active slot frees"
      then: "queued promise settles null, no cache entry or obsolete media start"
    - id: SCN-url-shared-owner
      given: "two feeds share a queued event/source"
      when: "one feed disposes"
      then: "exactly one media probe serves the live badge; disposed connected badge stays checking"
    - id: SCN-url-last-shared-owner
      given: "two feeds share a queued event/source"
      when: "both feeds dispose"
      then: "queued promise settles null and never starts media"
    - id: SCN-url-cancel-rerequest
      given: "cancelled queued event/source has pending promise continuations"
      when: "a new feed requests the same source immediately"
      then: "new inflight record survives old settlement and live badge becomes healthy"
    - id: SCN-url-cancel-mirror
      given: "queued primary has a mirror"
      when: "last owner disposes"
      then: "cancelled work settles without mirror HEAD/media or stale UI/cache"
    - id: SCN-url-registration-reuse
      given: "hidden old feed queues an eager callback"
      when: "dispose, reuse container, deliver old callback and old disposer"
      then: "only the new event reaches onCheck and starts IO"
    - id: SCN-url-active-cache
      given: "active media belongs to a connected card"
      when: "dispose its registration and complete active media"
      then: "active result caches healthy, disposed badge stays checking, new card uses cache"
    - id: SCN-url-hidden-reveal
      given: "URL-only card is hidden pending health"
      when: "eager media confirmation succeeds"
      then: "card becomes visible with healthy badge"
    - id: SCN-url-media-limit
      given: "three active media probes plus FIFO queued work"
      when: "slot releases while a new card enters"
      then: "existing queued source runs first; active media stays three"
    - id: SCN-url-mirror-order
      given: "primary and two mirrors exist"
      when: "primary errors then first mirror succeeds"
      then: "primary then first mirror are requested; second mirror is untouched"
    - id: SCN-url-legacy-callback
      given: "connected badge lacks observer ownership"
      when: "legacy caller invokes controller"
      then: "media settles and healthy badge paints"
    - id: SCN-url-cross-controller
      given: "shared real inflight cache and two controllers"
      when: "creator feed disposes while other feed remains"
      then: "queued shared source starts once and other controller paints healthy"
  observable_outcomes:
    - "HEAD and native media IO starts, promise settlement, real cache/inflight records, connected badge and card visibility"
    - "Observer callback emissions after disposal and container reuse"
  determinism_controls:
    - "jsdom cards, controllable IntersectionObserver, explicit loadeddata/error events and stubbed fetch boundary; real observer/controller/cache"
    - "Unique event/source keys and task-turn drain; no live traffic, arbitrary sleeps, retries or relaxed assertions"
  anti_cheat_rationale:
    prevents:
      - "hard-coded healthy or cancelled results"
      - "testing a copied queue instead of real controller and observer"
      - "cancelling shared live work or caching cancellation as offline"
      - "stale connected DOM callbacks hidden by isConnected guards"
      - "retry/sleep-based flake masking"
  relaxation:
    did_relax_any_assertion: false
    if_true_explain_spec_basis: ""

All existing assertions preserved. tests/url-health-lifecycle.test.mjs adds12 scenarios. Mutations are temporary edits restored before gates. python3 /tmp/item4-mutants.py final run:13/13 variants naturally exit1 with named assertion failures (no timeouts); logs /tmp/item4-mutant-*.log, results /tmp/item4-mutants.json. No production test runner/gate changes.

Coverage grouping: no-unregister→last owner; cancel-shared→shared owner/cross-controller; reuse-cancelled-inflight→immediate retry/new inflight; cache-cancellation→last/shared owner/retry/mirror cache outcomes; mirror-fallthrough→cancelled primary; grouped stale guards→shared/active disposed UI (remove both redundant owner guards together; two equivalent variants); eager-callback-stale→registration reuse; no-eager-reveal→hidden reveal; concurrency-four→three-media cap/FIFO; reverse-mirrors→mirror ordering; break-legacy-consumer→callback compatibility; controller-local-records→shared cache ownership across controllers. Initial mutation pass exposed callback-emission and first-source gaps; new assertions were strengthened, then all mutants rejected. Initial no-unregister full-file run failed assertions but hung in another scenario awaiting its intentionally broken promise; final targeted last-owner variant naturally exits1. No timeout is counted as successful mutant evidence.