diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md index 30b7dc29..56fc0c7c 100644 --- a/e2e/USER_STORIES.md +++ b/e2e/USER_STORIES.md @@ -1724,6 +1724,18 @@ Acceptance: - The section fills from the relays that answer. One that takes the connection and then says nothing does not hold it empty. +### US-133 — Keep the search relays that work + +As alice, I want the search health check to leave alone the relays that do +support search, so that fixing it does not cost me the ones I chose. + +Acceptance: + +- The check reads each search relay's own metadata, so a relay that advertises + search is never offered for removal, and neither is one whose metadata never + arrives. +- A relay that answers and advertises no search is. + ### US-106 — Share text into the app As alice, I want to hand text to Flotilla and choose where it lands, so that I diff --git a/e2e/specs/notifications.spec.ts b/e2e/specs/notifications.spec.ts index 33e1d846..206fcc91 100644 --- a/e2e/specs/notifications.spec.ts +++ b/e2e/specs/notifications.spec.ts @@ -2,7 +2,15 @@ import {neventEncode, npubEncode} from "nostr-tools/nip19" import {DAY, HOUR, MINUTE} from "@welshman/lib" import {POLL, displayRelayUrl} from "@welshman/util" import {RelayMessageType} from "@welshman/net" -import {Classified, Comment, FollowList, Note, Poll, Thread} from "@welshman/domain" +import { + Classified, + Comment, + FollowList, + Note, + Poll, + SearchRelayList, + Thread, +} from "@welshman/domain" import type {Locator, Page} from "@playwright/test" import { composer, @@ -560,6 +568,54 @@ test("US-117 read the network feed when one relay never answers", async ({seed, await expect(network.getByText("Bob Barker")).toBeVisible() }) +// A url the container does not serve, so its nip-11 document never arrives and nothing knows what it supports. +const unreachableRelayUrl = "wss://unreachable.test/" + +test("US-133 keep the search relays that support search", async ({seed, as}) => { + const scenario = await seed(({relay, open, user}) => { + const space = relay("space") + const indexer = open("indexer") + const outbox = open("outbox") + + space.room("general", {name: "General"}) + space.join(user.alice, "general") + + indexer.relayList(user.alice, { + read: [space.url, indexer.url], + write: [space.url, indexer.url], + }) + + // No search relay of hers is a space, so the health check is the only thing asking for their metadata. + indexer.event(user.alice, () => + indexer + .kind(SearchRelayList) + .writer() + .setUrls([indexer.url, outbox.url, unreachableRelayUrl]) + .renderTemplate(), + ) + }) + + const indexer = scenario.open("indexer") + const outbox = scenario.open("outbox") + + const page = await as(users.alice, "/home", { + relayInfo: {[indexer.url]: {supported_nips: [1, 50]}}, + }) + + const healthCheck = page.getByRole("group", {name: "Invalid Search Relays"}) + + await expect(healthCheck).toBeVisible() + + await healthCheck.getByRole("button", {name: "Fix"}).click() + + const review = dialog(page, "Review changes") + + // Outbox answered and advertised no search; the other two are a relay that does and one nobody can ask. + await expect(review.getByText(displayRelayUrl(outbox.url))).toBeVisible() + await expect(review.getByText(displayRelayUrl(indexer.url))).toHaveCount(0) + await expect(review.getByText(displayRelayUrl(unreachableRelayUrl))).toHaveCount(0) +}) + test("US-106 share text into the app", async ({seed, as}) => { const shared = "the offsite is moving to the 14th" diff --git a/src/app/healthChecks.ts b/src/app/healthChecks.ts index 82584773..7e075410 100644 --- a/src/app/healthChecks.ts +++ b/src/app/healthChecks.ts @@ -109,13 +109,15 @@ export class HealthChecks { ) this.pending = projection( - derived([this.context.$, forceHealthChecks], ([$context, $forceHealthChecks]) => - this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)), + derived( + [this.context.$, forceHealthChecks, app.use(Relays).index.$], + ([$context, $forceHealthChecks]) => + this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)), ), ) } - private supportsSearch = (url: string) => this.app.use(Relays).get(url)?.hasNip(50) + private supportsSearch = (url: string) => this.app.use(Relays).get(url)?.hasNip(50) ?? true private checks: HealthCheck[] = [ { diff --git a/src/app/sync.ts b/src/app/sync.ts index 7393f1ff..951a133d 100644 --- a/src/app/sync.ts +++ b/src/app/sync.ts @@ -36,6 +36,7 @@ import { MessagingRelayLists, RelayLists, RoomLists, + SearchRelayLists, Sync, makeRoomKey, } from "@welshman/app" @@ -53,6 +54,7 @@ import { relays, roomLists, router, + searchRelayLists, } from "@app/core" import {LIVEKIT_PARTICIPANTS} from "@app/call" import {REACTION_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content" @@ -100,6 +102,8 @@ const userFollowList = deriveUserItem(FollowLists) const userMessagingRelayList = deriveUserItem(MessagingRelayLists) +const userSearchRelayList = deriveUserItem(SearchRelayLists) + const getSpaceUrls = ($roomList: Maybe) => PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? []) @@ -125,9 +129,16 @@ const syncRelays = () => { } }) + const unsubscribeSearchRelayUrls = userSearchRelayList.subscribe($searchRelayList => { + for (const url of $searchRelayList?.urls() ?? []) { + relays.get().load(url) + } + }) + return () => { unsubscribePage() unsubscribeSpaceUrls() + unsubscribeSearchRelayUrls() } } @@ -213,6 +224,7 @@ const syncUserData = () => { roomLists.get().load($pubkey) muteLists.get().load($pubkey) profiles.get().load($pubkey) + searchRelayLists.get().load($pubkey) app.get().use(Settings).load($pubkey) } }