Ask a search relay whether it supports search before offering to drop it (#659)

This commit is contained in:
Coracle-Bot 2026-09-25 20:59:18 +00:00 committed by hodlbod
parent 1c934576fe
commit 653664751f
4 changed files with 86 additions and 4 deletions

View file

@ -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

View file

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

View file

@ -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[] = [
{

View file

@ -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<RoomListReader>) =>
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)
}
}