From fe653706a1d0c456545d5f1dfea4c294f3f7fdee Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:11:52 -0400 Subject: [PATCH] test(11b): un-quarantine nostr-boost-actions + admin-list-store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triaged two more silently-excluded test files back into CI. - nostr-boost-actions: STALE SPEC. Test required buildRepostEvent to throw /missing-event-relay/ with no relay hint, but a relay hint is a NIP-18 SHOULD (not MUST) — the builder degrades to a two-element `e` tag so a user can always repost. Replaced with an exact assertion of the valid output (kind 16 + e/p/k). - admin-list-store: STALE HARNESS (no production change, no assertion weakened). Community curator lists are fetched via the batched SubscriptionManager (cold-start relay-storm fix) the harness never mocked; the mock "hex" curator values started with "npub" so parseCommunityBlacklistReferences decoded them twice; and createListEvent omitted the `d` tag selectNewestEventsForReferences matches on. Fixed all three to be faithful to production. Removed both from the QUARANTINE map; TEST_INTEGRITY.md + TODO updated. Co-Authored-By: Claude Opus 4.8 --- TEST_INTEGRITY.md | 46 +++++++++++++++++++++++++++ scripts/run-unit-tests.mjs | 2 -- tests/admin-list-store.test.mjs | 51 ++++++++++++++++++++++++------ tests/nostr-boost-actions.test.mjs | 41 +++++++++++++++++------- todo/TODO_2026-06-20_pre-launch.md | 15 +++++++-- 5 files changed, 130 insertions(+), 25 deletions(-) diff --git a/TEST_INTEGRITY.md b/TEST_INTEGRITY.md index e77abb33..16d311e7 100644 --- a/TEST_INTEGRITY.md +++ b/TEST_INTEGRITY.md @@ -412,3 +412,49 @@ test_integrity_note: 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: "" +``` diff --git a/scripts/run-unit-tests.mjs b/scripts/run-unit-tests.mjs index 8c1bcfb7..c17d265a 100755 --- a/scripts/run-unit-tests.mjs +++ b/scripts/run-unit-tests.mjs @@ -16,8 +16,6 @@ process.env.NODE_ENV = "test"; const QUARANTINE = new Map([ ["tests/user-blocks.test.mjs", "HANG — triage todo-11b"], ["tests/nostr-count-fallback.test.mjs", "FAIL — triage todo-11b"], - ["tests/admin-list-store.test.mjs", "FAIL — triage todo-11b"], - ["tests/nostr-boost-actions.test.mjs", "FAIL — triage todo-11b"], ["tests/nwc-client.test.mjs", "FAIL — mocks nostr-tools but is shadowed by the frozen canonical toolkit the bootstrap installs (real @noble rejects the fake keys). The underlying production bug (parseNwcUri passed a hex secret to getPublicKey, which needs bytes — NWC connection broken) is FIXED and guarded by tests/nwc-parse-uri.test.mjs; this file needs a mock-injection rework. triage todo-11b (NWC / item #3)"], ["tests/nostr-publish-rejection.test.mjs", "FAIL — triage todo-11b"], ]); diff --git a/tests/admin-list-store.test.mjs b/tests/admin-list-store.test.mjs index c8f8beb4..0c062f2c 100644 --- a/tests/admin-list-store.test.mjs +++ b/tests/admin-list-store.test.mjs @@ -168,14 +168,16 @@ const communityMemberNpub = "npub1communityblacklist"; const communitySecondMemberNpub = "npub1communitytwo"; const communityCuratorOneNpub = "npub1communitycurator"; const communityCuratorTwoNpub = "npub1communitycurator2"; -const communityCuratorOneHex = mockNip19.decode(communityCuratorOneNpub).data; -const communityCuratorTwoHex = mockNip19.decode(communityCuratorTwoNpub).data; -const communityCuratorOneAuthorHex = mockNip19.decode( - communityCuratorOneHex -).data; -const communityCuratorTwoAuthorHex = mockNip19.decode( - communityCuratorTwoHex -).data; +// Curator coordinates in an `a` tag carry a REAL 64-char hex pubkey (kind:hex:dTag), +// which parseCommunityBlacklistReferences uses directly via isHexPubkey(). The old +// mock-"hex" values still started with "npub", so the parser's npub branch decoded +// them AGAIN (…-hex-hex), and the batched curator filter then didn't match the +// curator events' pubkeys. Use realistic hex so the test exercises the production +// path faithfully. +const communityCuratorOneHex = "c".repeat(64); +const communityCuratorTwoHex = "d".repeat(64); +const communityCuratorOneAuthorHex = communityCuratorOneHex; +const communityCuratorTwoAuthorHex = communityCuratorTwoHex; const editorsDTag = `${ADMIN_LIST_NAMESPACE}:${ADMIN_LIST_IDENTIFIERS.editors}`; const whitelistDTag = `${ADMIN_LIST_NAMESPACE}:${ADMIN_LIST_IDENTIFIERS.whitelist}`; const blacklistDTag = `${ADMIN_LIST_NAMESPACE}:${ADMIN_LIST_IDENTIFIERS.blacklist}`; @@ -202,12 +204,18 @@ function registerListEvent({ dTag, authorHex = null, event }) { } function createListEvent({ dTag, tags, pubkey, createdAt }) { + // Real kind-30000 list events are parameterized-replaceable and ALWAYS carry a + // `d` tag. The batched community fetch matches curator events to references by + // their own `d` tag (selectNewestEventsForReferences), so the helper must emit + // one (prepended, deduped against any caller-supplied d tag). + const callerTags = Array.isArray(tags) ? tags : []; + const hasDTag = callerTags.some((tag) => Array.isArray(tag) && tag[0] === "d"); return { kind: 30000, pubkey: pubkey || "", created_at: typeof createdAt === "number" ? createdAt : Math.floor(Date.now() / 1000), id: `${dTag}:${pubkey || "anon"}:${createdAt || 0}`, - tags: Array.isArray(tags) ? tags : [], + tags: hasDTag ? callerTags : [["d", dTag], ...callerTags], }; } @@ -294,6 +302,31 @@ nostrClient.pool = { }, }; +// Community curator blacklists are batched through the SubscriptionManager (one +// REQ instead of one-per-curator — the cold-start relay-storm fix), NOT pool.list. +// The harness previously only mocked pool.list, so the batched curator fetch +// returned nothing and the community-merge assertion failed. Serve the same +// registry here; selectNewestEventsForReferences matches events to references by +// (event.pubkey, dTag), so returning all events matching the batched filter's +// authors+#d is sufficient and still exercises real merge/dedupe/guard logic. +nostrClient.getSubscriptionManager = () => ({ + async list({ filters } = {}) { + const filter = Array.isArray(filters) && filters.length ? filters[0] : {}; + const wantAuthors = new Set( + Array.isArray(filter.authors) ? filter.authors : [] + ); + const wantDTags = new Set(Array.isArray(filter["#d"]) ? filter["#d"] : []); + const out = []; + for (const [key, ev] of listEventRegistry.entries()) { + const dTag = key.slice(key.indexOf("::") + 2); + if (wantDTags.size && !wantDTags.has(dTag)) continue; + if (wantAuthors.size && !wantAuthors.has(ev.pubkey)) continue; + out.push(ev); + } + return out; + }, +}); + if (!globalThis.window.nostr) { globalThis.window.nostr = {}; } diff --git a/tests/nostr-boost-actions.test.mjs b/tests/nostr-boost-actions.test.mjs index c44201d1..097d2b7d 100644 --- a/tests/nostr-boost-actions.test.mjs +++ b/tests/nostr-boost-actions.test.mjs @@ -104,19 +104,38 @@ function createPublishClient({ actorPubkey = "", sessionPubkey = "", failPublish assert.deepEqual(event.tags[2], ["p", "deadbeef"]); })(); -(function testBuildRepostEventRequiresRelay() { +(function testBuildRepostEventOmitsRelayHintWhenAbsent() { const createdAt = 1_700_000_001; - assert.throws( - () => - buildRepostEvent({ - pubkey: "actorpubkey", - created_at: createdAt, - eventId: "event456", - authorPubkey: "cafebabe", - targetKind: 30078, - }), - /missing-event-relay/, + // Spec correction (SCN-repost-optional-relay): a relay hint on the `e` tag is a + // NIP-18 SHOULD, not a MUST. With no relay available the builder must still emit + // a VALID repost — a two-element `e` tag (no relay) — rather than throwing, so a + // user can always repost even without a known relay hint. + const event = buildRepostEvent({ + pubkey: "actorpubkey", + created_at: createdAt, + eventId: "event456", + authorPubkey: "cafebabe", + targetKind: 30078, + }); + + // Non-kind-1 (addressable) target → generic repost kind 16 with a `k` tag. + assert.equal(event.kind, 16); + assert.equal(event.pubkey, "actorpubkey"); + assert.equal(event.created_at, createdAt); + assert.deepEqual( + event.tags.find((tag) => tag[0] === "e"), + ["e", "event456"], + "e tag omits the relay hint when none is provided (no throw)", + ); + assert.deepEqual( + event.tags.find((tag) => tag[0] === "p"), + ["p", "cafebabe"], + ); + assert.deepEqual( + event.tags.find((tag) => tag[0] === "k"), + ["k", "30078"], + "generic repost records the target kind", ); })(); diff --git a/todo/TODO_2026-06-20_pre-launch.md b/todo/TODO_2026-06-20_pre-launch.md index 4c3c61d2..8cff01c7 100644 --- a/todo/TODO_2026-06-20_pre-launch.md +++ b/todo/TODO_2026-06-20_pre-launch.md @@ -722,8 +722,11 @@ test below was failing on `main` unnoticed). logged-in pubkey now) → aligned; (3) same test asserted `session===true` for a logged-in watch (the pre-fix bug) → corrected to `session!==true`. The full 2669-line file passes via the CI runner and was removed from QUARANTINE. - - `nostr-boost-actions` — "Missing expected exception" (likely stale: expects a - throw the code no longer makes). + - `nostr-boost-actions` — ✅ DONE + UN-QUARANTINED 2026-06-25: STALE SPEC. The + test required `buildRepostEvent` to throw `/missing-event-relay/` with no relay + hint, but a relay hint is a NIP-18 SHOULD (not MUST) — the builder degrades to a + two-element `e` tag so a user can always repost. Replaced with an exact assertion + of the valid output (kind 16 + e/p/k tags). See TEST_INTEGRITY.md. - `view-counter` (#4) — ✅ DONE + UN-QUARANTINED 2026-06-24: the failures were (a) the deterministic-d-tag accuracy fix (no more random `d` tag, so no auto-`d` either), (b) the batched SubscriptionManager silently shadowing @@ -745,7 +748,13 @@ test below was failing on `main` unnoticed). frozen canonical toolkit the bootstrap installs; needs a mock-injection rework. - `nostr-count-fallback` — `TypeError: Cannot read 'has' of undefined` — likely a stale harness mock missing a field. - - `admin-list-store` — community-blacklist merge mismatch. + - `admin-list-store` — ✅ DONE + UN-QUARANTINED 2026-06-25: STALE HARNESS (no + production change, no assertion weakened). Community curator lists are fetched via + the BATCHED SubscriptionManager (cold-start relay-storm fix) which the harness + didn't mock; the mock-"hex" curator values started with "npub" so + parseCommunityBlacklistReferences decoded them twice; and createListEvent omitted + the `d` tag that selectNewestEventsForReferences matches on. Fixed all three to be + faithful to production. See TEST_INTEGRITY.md. - `nostr-publish-rejection` — rejection-path assertion. - `user-blocks` — HANGS (async leak); needs a deterministic rewrite.