Files
bitvid/TEST_INTEGRITY.md
T
thePR0M3TH3ANandClaude Opus 4.8 567a12c758 tests(11): make uploadModal-reset zombie-callback test deterministic (correct mock boundary)
The "guard against zombie callbacks" subtest passed its asserts but leaked async
activity after the test ended (node:test flags that as 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"). Not intermittent — a consistent wrong-boundary mock.

Fix: mock at the real DI boundary — modal.mediaUploader.uploadVideo = () =>
delayedUpload (controllable pending promise) — and stub captureVideoMetadata.
Hermetic, deterministic (3/3 exit 0, # pass 3). Mutation-verified: breaking the
videoUploadId zombie guard makes the test fail. Production code untouched. See
TEST_INTEGRITY.md.

NOTE: the runner is fail-fast, so the full suite surfaces pre-existing failures one
at a time. tests/unit/hashChangeHandler.test.mjs is next (consistent AssertionError
on "loads default view if hash is empty" — likely the same stale-default bug the
sibling tests/app/hash-change-handler.test.mjs already had when TRENDING became the
logged-out default). Enumerating full scope next.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 11:29:36 -04:00

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