mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-09-14 00:55:08 +00:00
test: run relay-backed tests against geode instead of external relays
Every test that used to open a socket to something outside the repo now talks to geode, the relay this project ships, either in-process or as the embedded `amy serve`. - amethyst: the Android instrumented EventSyncTest dialed vitor.nostr1.com, pyramid.fiatjaf.com and the nos.lol / nostr.mom defaults and asserted nothing. Replaced by a JVM unit test on geode's InProcessRelays that preloads a source relay and asserts what lands on the outbox, inbox and DM relays, plus a NIP-42 variant with the source behind FullAuthPolicy to cover the RelayAuthenticator wiring. Wires :geode, its testFixtures and the JVM SQLite driver into amethyst's unit-test classpath, the same way quartz's jvmAndroidTest already does. - cli/tests: the cache, dm and marmot headless harnesses cloned and cargo-built nostr-rs-relay on first run. They now boot `amy serve` (geode) from the amy binary they already build, via a shared start_local_relay / stop_local_relay in headless/helpers.sh. Rust is no longer needed for the cache and dm suites at all. - cli/tests/marmot/marmot-interop.sh: the interactive harness defaulted to relay.damus.io / nos.lol / primal / bitcoiner.social / nostr.mom, with `--local-relays` pointing at MDK's docker stack. It now boots the embedded relay on 0.0.0.0 by default (the phone reaches it over the LAN) and keeps the public set behind an explicit `--public-relays`. - docs: cli/tests/README.md, CONTRIBUTING.md, cli/DEVELOPMENT.md and cli/ROADMAP.md no longer describe a loopback nostr-rs-relay. The quartz prodbench probes (NegentropyStallRepro, CursorTerminationProbe, NegentropyMultiRelayLiveTest, ProductionReceiverBenchmark, ...) are left as they are: they are opt-in diagnostics of production relay behaviour, gated behind PROD_RELAY_BENCH / NEG_MULTI / NEG_STALL_REPRO, and would measure nothing against a local relay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PguqnDbP2v11dtANs9xdxc
This commit is contained in:
+5
-3
@@ -265,9 +265,11 @@ front:
|
||||
sequentially: `for peer in aioquic picoquic quic-go quinn; do
|
||||
quic/interop/run-matrix.sh -s $peer; done`. Plan at
|
||||
`quic/interop/plans/2026-05-06-interop-runner.md`.
|
||||
- **CLI suites** ([`cli/tests/README.md`](cli/tests/README.md)): headless
|
||||
variants need only `cargo` + a loopback `nostr-rs-relay`; the interactive
|
||||
Marmot variant prompts a human to drive the Android UI.
|
||||
- **CLI suites** ([`cli/tests/README.md`](cli/tests/README.md)): every
|
||||
relay-backed suite boots the embedded `amy serve` relay (geode) — no
|
||||
external relay binary; only the Marmot suites additionally need `cargo`
|
||||
for MDK's `wn`/`wnd`. The interactive Marmot variant prompts a human to
|
||||
drive the Android UI.
|
||||
|
||||
If a change is documentation-only, UI-only, build-script-only, or otherwise
|
||||
cannot affect wire bytes / decoded audio / MLS state / DM envelopes, skip
|
||||
|
||||
@@ -598,6 +598,15 @@ dependencies {
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.secp256k1.kmp.jni.jvm)
|
||||
|
||||
// In-process Nostr relay (geode) so unit tests that drive a real
|
||||
// NostrClient talk to an embedded relay instead of a public one. Same
|
||||
// wiring quartz uses for its jvmAndroidTest source set: the engine, its
|
||||
// testFixtures (RelayClientTest base, preload/publish helpers) and the
|
||||
// JVM SQLite driver the in-memory EventStore needs on a host JVM.
|
||||
testImplementation(project(":geode"))
|
||||
testImplementation(testFixtures(project(":geode")))
|
||||
testImplementation(libs.androidx.sqlite.bundled.jvm)
|
||||
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.junit.ktx)
|
||||
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.defaults.Constants
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DefaultContentTypeInterceptor
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class EventSyncTest {
|
||||
companion object {
|
||||
val vitor = "wss://vitor.nostr1.com".normalizeRelayUrl()
|
||||
val fiatjaf = "wss://pyramid.fiatjaf.com".normalizeRelayUrl()
|
||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
val rootClient =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/v1.05"))
|
||||
.build()
|
||||
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSync() =
|
||||
runBlocking {
|
||||
val sync =
|
||||
EventSync(
|
||||
accountPubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
relayDb = {
|
||||
listOf(Constants.mom, Constants.nos)
|
||||
},
|
||||
outboxTargets = { setOf(vitor) },
|
||||
inboxTargets = { setOf(vitor) },
|
||||
dmTargets = { setOf(vitor) },
|
||||
clientBuilder = {
|
||||
NostrClient(socketBuilder, appScope)
|
||||
},
|
||||
scope = appScope,
|
||||
)
|
||||
|
||||
sync.runSync()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFiatjafSync() =
|
||||
runBlocking {
|
||||
val sync =
|
||||
EventSync(
|
||||
accountPubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
relayDb = { listOf(fiatjaf) },
|
||||
outboxTargets = { setOf(vitor) },
|
||||
inboxTargets = { setOf(vitor) },
|
||||
dmTargets = { setOf(vitor) },
|
||||
clientBuilder = {
|
||||
val newClient = NostrClient(socketBuilder, appScope)
|
||||
val logger = RelayLogger(newClient, debugSending = true, debugReceiving = false)
|
||||
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
|
||||
// Authenticates with relays.
|
||||
val auth =
|
||||
RelayAuthenticator(
|
||||
newClient,
|
||||
appScope,
|
||||
signWithAllLoggedInUsers = { _, authTemplate, _ ->
|
||||
listOf(signer.sign(authTemplate))
|
||||
},
|
||||
)
|
||||
|
||||
newClient
|
||||
},
|
||||
scope = appScope,
|
||||
)
|
||||
|
||||
sync.runSync()
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
|
||||
|
||||
import com.vitorpamplona.geode.InProcessRelays
|
||||
import com.vitorpamplona.geode.RelayEngine
|
||||
import com.vitorpamplona.geode.testing.RelayClientTest
|
||||
import com.vitorpamplona.geode.testing.preload
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.FullAuthPolicy
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Drives [EventSync] end to end against geode's in-process relays: one
|
||||
* "source" relay that already holds the account's history and three empty
|
||||
* destination relays (outbox / inbox / DM). No network, no public relay —
|
||||
* every relay is a [RelayEngine] inside this JVM, so the assertions are on
|
||||
* what actually landed in each destination store, not on "it didn't crash".
|
||||
*
|
||||
* The second scenario gates the source relay behind NIP-42 ([FullAuthPolicy])
|
||||
* to cover the [RelayAuthenticator] wiring the sync screen relies on when a
|
||||
* user's relay demands AUTH before serving REQs.
|
||||
*/
|
||||
class EventSyncTest : RelayClientTest() {
|
||||
private val account = NostrSignerSync(KeyPair())
|
||||
private val other = NostrSignerSync(KeyPair())
|
||||
|
||||
private val source: NormalizedRelayUrl = RelayUrlNormalizer.normalize("ws://source.relay/")
|
||||
private val outbox: NormalizedRelayUrl = RelayUrlNormalizer.normalize("ws://outbox.relay/")
|
||||
private val inbox: NormalizedRelayUrl = RelayUrlNormalizer.normalize("ws://inbox.relay/")
|
||||
private val dm: NormalizedRelayUrl = RelayUrlNormalizer.normalize("ws://dm.relay/")
|
||||
|
||||
/** Separate hub so only the source relay demands AUTH; destinations stay open. */
|
||||
private val authHub = InProcessRelays(defaultPolicy = { FullAuthPolicy(source) })
|
||||
|
||||
@After
|
||||
fun tearDownAuthHub() {
|
||||
authHub.close()
|
||||
}
|
||||
|
||||
private fun note(
|
||||
author: NostrSignerSync,
|
||||
content: String,
|
||||
tagged: HexKey? = null,
|
||||
): Event = author.sign(eventTemplate<Event>(1, content) { tagged?.let { pTag(it) } })
|
||||
|
||||
private fun legacyDm(
|
||||
author: NostrSignerSync,
|
||||
recipient: HexKey,
|
||||
): Event = author.sign(eventTemplate<Event>(4, "ciphertext") { pTag(recipient) })
|
||||
|
||||
private val mine = List(3) { note(account, "mine $it") }
|
||||
private val mentions = List(2) { note(other, "hey $it", tagged = account.pubKey) }
|
||||
private val dmToMe = legacyDm(other, account.pubKey)
|
||||
private val noise = note(other, "unrelated")
|
||||
|
||||
private fun corpus(): List<Event> = mine + mentions + dmToMe + noise
|
||||
|
||||
private fun eventSync(builder: WebsocketBuilder): EventSync =
|
||||
EventSync(
|
||||
accountPubKey = account.pubKey,
|
||||
relayDb = { listOf(source) },
|
||||
outboxTargets = { setOf(outbox) },
|
||||
inboxTargets = { setOf(inbox) },
|
||||
dmTargets = { setOf(dm) },
|
||||
clientBuilder = { NostrClient(builder, scope) },
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
/**
|
||||
* Publishes are fire-and-forget on the client side, so the destination
|
||||
* store can lag `runSync` returning by a few ticks. Poll instead of
|
||||
* asserting a snapshot.
|
||||
*/
|
||||
private suspend fun RelayEngine.awaitCount(
|
||||
filter: Filter,
|
||||
expected: Int,
|
||||
): Int =
|
||||
withTimeoutOrNull(10_000) {
|
||||
while (store.count(filter) < expected) delay(25)
|
||||
store.count(filter)
|
||||
} ?: store.count(filter)
|
||||
|
||||
private suspend fun assertRouted(hubOfTargets: InProcessRelays) {
|
||||
val outboxRelay = hubOfTargets.getOrCreate(outbox)
|
||||
val inboxRelay = hubOfTargets.getOrCreate(inbox)
|
||||
val dmRelay = hubOfTargets.getOrCreate(dm)
|
||||
|
||||
assertEquals(
|
||||
"every event authored by the account lands on the outbox relay",
|
||||
mine.size,
|
||||
outboxRelay.awaitCount(Filter(authors = listOf(account.pubKey)), mine.size),
|
||||
)
|
||||
assertEquals(
|
||||
"non-DM mentions land on the inbox relay",
|
||||
mentions.size,
|
||||
inboxRelay.awaitCount(Filter(tags = mapOf("p" to listOf(account.pubKey))), mentions.size),
|
||||
)
|
||||
assertEquals(
|
||||
"the kind-4 DM lands on the DM relay",
|
||||
1,
|
||||
dmRelay.awaitCount(Filter(kinds = listOf(4)), 1),
|
||||
)
|
||||
|
||||
// Routing is exclusive per rule: nothing leaks across destinations and the
|
||||
// unrelated note never leaves the source.
|
||||
assertEquals("outbox holds only the account's events", mine.size, outboxRelay.store.count(Filter()))
|
||||
assertEquals("inbox holds only the mentions", mentions.size, inboxRelay.store.count(Filter()))
|
||||
assertEquals("dm relay holds only the DM", 1, dmRelay.store.count(Filter()))
|
||||
assertEquals("noise stays on the source", 0, outboxRelay.store.count(Filter(ids = listOf(noise.id))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncRoutesEventsFromSourceToOutboxInboxAndDmRelays() =
|
||||
runBlocking {
|
||||
hub.getOrCreate(source).preload(corpus())
|
||||
|
||||
val sync = eventSync(hub)
|
||||
withTimeout(30_000) { sync.runSync() }
|
||||
|
||||
val done = sync.syncState.value
|
||||
assertTrue("sync should finish in Done, got $done", done is EventSync.SyncState.Done)
|
||||
done as EventSync.SyncState.Done
|
||||
assertEquals(
|
||||
"mine + mentions + dm match a routing rule; noise does not",
|
||||
mine.size + mentions.size + 1,
|
||||
done.totalEventsReceived,
|
||||
)
|
||||
|
||||
assertRouted(hub)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncReadsFromAuthRequiredSourceOnceAuthenticated() =
|
||||
runBlocking {
|
||||
authHub.getOrCreate(source).preload(corpus())
|
||||
|
||||
// Source demands NIP-42 before serving REQs; destinations are the open hub.
|
||||
val router =
|
||||
object : WebsocketBuilder {
|
||||
override fun build(
|
||||
url: NormalizedRelayUrl,
|
||||
out: WebSocketListener,
|
||||
): WebSocket = if (url == source) authHub.build(url, out) else hub.build(url, out)
|
||||
}
|
||||
|
||||
val authSigner = NostrSignerSync(KeyPair())
|
||||
var authenticator: RelayAuthenticator? = null
|
||||
val sync =
|
||||
EventSync(
|
||||
accountPubKey = account.pubKey,
|
||||
relayDb = { listOf(source) },
|
||||
outboxTargets = { setOf(outbox) },
|
||||
inboxTargets = { setOf(inbox) },
|
||||
dmTargets = { setOf(dm) },
|
||||
clientBuilder = {
|
||||
val client = NostrClient(router, scope)
|
||||
authenticator =
|
||||
RelayAuthenticator(client = client, scope = scope) { _, template, _ ->
|
||||
listOf(authSigner.sign(template))
|
||||
}
|
||||
client
|
||||
},
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
try {
|
||||
withTimeout(30_000) { sync.runSync() }
|
||||
} finally {
|
||||
authenticator?.destroy()
|
||||
}
|
||||
|
||||
val done = sync.syncState.value
|
||||
assertTrue("sync should finish in Done, got $done", done is EventSync.SyncState.Done)
|
||||
assertEquals(
|
||||
"the auth-gated source still yields every routed event",
|
||||
mine.size + mentions.size + 1,
|
||||
(done as EventSync.SyncState.Done).totalEventsReceived,
|
||||
)
|
||||
|
||||
assertRouted(hub)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -348,7 +348,7 @@ Amy-specific layer still needs its own coverage:
|
||||
| Error / exit-code contract (bad args → 2, timeout → 124, `rejected` → 1) | `ExitCodeContractTest` — table-driven tests invoking `runCli(argv)` with captured stdout/stderr. |
|
||||
| JSON output shape (keys and types under `--json`) | `JsonContractTest` — runs commands under `--json` and asserts on the parsed object. The default text render has no shape contract and isn't asserted on. |
|
||||
| File layout on disk (`identity.json`, `shared/events.db`, `marmot/groups/*.mls`, …) | Structural assertions after a command sequence. |
|
||||
| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`: each spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple accounts sharing one store and drives a scenario through them. Nine suites today — see [`cli/tests/README.md`](./tests/README.md). |
|
||||
| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`: each spins up the embedded `amy serve` relay (geode) and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple accounts sharing one store and drives a scenario through them. Nine suites today — see [`cli/tests/README.md`](./tests/README.md). |
|
||||
|
||||
The JVM suite drives `runCli` **in-process** through the shared
|
||||
`amy(vararg argv)` harness in `CliResult.kt`: it captures stdout/stderr,
|
||||
|
||||
+4
-4
@@ -180,11 +180,11 @@ move anything, re-audit — you're probably duplicating logic.
|
||||
9. **Test suite** — largely in place, two layers:
|
||||
- **Shell harnesses** under `cli/tests/` — ten suites: `blossom`
|
||||
(live servers), `cache`, `clink`, `dm`, `git` (NIP-34 vs `amy serve`),
|
||||
`marmot` (vs whitenoise-rs), `nests` (manual audio-rooms matrix), `pow`,
|
||||
`marmot` (vs MDK), `nests` (manual audio-rooms matrix), `pow`,
|
||||
`relaygroup`, `sync`, plus the shared `headless/` helpers. See
|
||||
`cli/tests/README.md`.
|
||||
None run in CI yet (the relay-backed ones need Rust + a ~3 min
|
||||
cold `nostr-rs-relay` build).
|
||||
`cli/tests/README.md`. Every relay-backed suite runs against the
|
||||
embedded `amy serve` relay (geode) — no external relay binary.
|
||||
None run in CI yet (the Marmot ones need Rust for MDK's `wn`).
|
||||
- **JVM unit suite** at `cli/src/test/kotlin/` — `Args` parsing,
|
||||
exit-code contract, and `--json` shape tests driving `runCli`
|
||||
in-process via the `amy.home` isolation seam.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
marmot/state/
|
||||
marmot/state-headless/
|
||||
dm/state-dm-headless/
|
||||
cache/state-cache-headless/
|
||||
nests/state/
|
||||
clink/state-clink-headless/
|
||||
relaygroup/state-relaygroup-headless/
|
||||
|
||||
+46
-39
@@ -1,21 +1,24 @@
|
||||
# amy CLI test harnesses
|
||||
|
||||
Shell-based end-to-end harnesses that drive the `amy` CLI binary — against a
|
||||
loopback `nostr-rs-relay`, an embedded `amy serve` relay, live public servers,
|
||||
or no relay at all, depending on the suite. Eleven directories:
|
||||
Shell-based end-to-end harnesses that drive the `amy` CLI binary — against an
|
||||
embedded relay (`amy serve`, i.e. **geode**, the relay this repo ships), live
|
||||
public servers, or no relay at all, depending on the suite. No suite depends on
|
||||
an external relay binary or a Rust toolchain for its relay: every relay-backed
|
||||
harness boots geode from the `amy` binary it already built, so the relay under
|
||||
test is the same server code that runs in production. Eleven directories:
|
||||
|
||||
```
|
||||
cli/tests/
|
||||
├── lib.sh # shared logging, results, assertions
|
||||
├── headless/ # shared bits used by every harness
|
||||
│ └── helpers.sh
|
||||
│ └── helpers.sh # amy wrappers, assertions, embedded relay boot
|
||||
├── blossom/ # Blossom blob lifecycle vs LIVE public servers
|
||||
│ └── blossom-live.sh
|
||||
├── cache/ # local-store-as-cache semantics (profile show
|
||||
│ └── cache-headless.sh # cache/refresh, store stat) vs nostr-rs-relay
|
||||
│ └── cache-headless.sh # cache/refresh, store stat) vs embedded `amy serve`
|
||||
├── clink/ # CLINK pointer decode — local-only, no relay
|
||||
│ └── clink-headless.sh
|
||||
├── dm/ # NIP-17 DM interop (amy ↔ amy)
|
||||
├── dm/ # NIP-17 DM interop (amy ↔ amy) vs embedded `amy serve`
|
||||
│ ├── dm-interop-headless.sh
|
||||
│ ├── setup.sh # preflight + identities
|
||||
│ └── tests-dm.sh
|
||||
@@ -24,7 +27,7 @@ cli/tests/
|
||||
├── marmot/ # Marmot / MLS group-messaging interop
|
||||
│ ├── marmot-interop.sh # interactive — prompts Amethyst Android UI
|
||||
│ ├── marmot-interop-headless.sh # zero-prompt
|
||||
│ ├── setup.sh # preflight + wn + relay + identities
|
||||
│ ├── setup.sh # preflight + wn + identities
|
||||
│ ├── tests-create.sh # tests 01–05
|
||||
│ ├── tests-manage.sh # tests 06–08, 11
|
||||
│ ├── tests-extras.sh # tests 09, 10, 12, 13
|
||||
@@ -60,7 +63,7 @@ Suite notes:
|
||||
and mined-nonce round-trips through `pow check`.
|
||||
- **`cache/cache-headless.sh`** proves the local store is the source of
|
||||
truth for reads: `profile show` served from cache vs `--refresh`, and
|
||||
`store stat` reporting the right histogram, vs a loopback nostr-rs-relay.
|
||||
`store stat` reporting the right histogram, vs the embedded `amy serve` relay.
|
||||
- **`relaygroup/relaygroup-headless.sh`** runs NIP-29 create/message/join/
|
||||
list/browse against an embedded relay (`amy serve`, which boots geode) —
|
||||
no external relay binary. geode doesn't sign 39000-39003, so browse/info
|
||||
@@ -124,10 +127,8 @@ The Marmot harnesses come in two flavours, same scenarios:
|
||||
A third, slimmer harness covers the NIP-17 DM surface:
|
||||
|
||||
- **`dm/dm-interop-headless.sh`** — two `amy` processes (Identity A and
|
||||
Identity D) exchange NIP-17 DMs through the loopback nostr-rs-relay.
|
||||
No MDK required — only `amy` and the relay binary (which
|
||||
is shared with the Marmot harness's checkout at
|
||||
`marmot/state-headless/nostr-rs-relay/`).
|
||||
Identity D) exchange NIP-17 DMs through the embedded `amy serve` relay.
|
||||
No MDK, no Rust — only `amy`.
|
||||
|
||||
A harness covers Blossom blob storage (BUD-01/02/04/09) against **live**
|
||||
public servers rather than a loopback relay:
|
||||
@@ -212,15 +213,18 @@ at `desktopApp/src/jvmTest/kotlin/.../service/upload/`.
|
||||
|
||||
On the machine that runs the harness:
|
||||
|
||||
- **Rust 1.90+** — install via https://rustup.rs
|
||||
- **Rust 1.90+** — install via https://rustup.rs (for MDK's `wn`/`wnd` only;
|
||||
the relay is `amy serve`, no Rust needed for it)
|
||||
- **git**, **curl**, **jq** — package manager
|
||||
- **~5 GB disk** for the first-run build of `wn` + `wnd`
|
||||
- Public internet access (for the default relay set and fetching crates)
|
||||
- Internet access for fetching crates on the first build. Test traffic
|
||||
stays on the machine unless you pass `--public-relays`.
|
||||
|
||||
On the Android side:
|
||||
|
||||
- Amethyst installed on an **emulator** or a **physical device**
|
||||
- The device must reach the same relays the harness uses (see below)
|
||||
- The device must reach the harness's embedded relay over the network
|
||||
(see below), or the public relays when running with `--public-relays`
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -240,9 +244,11 @@ The script will, in order:
|
||||
4. Create Nostr identities for B and C, persist their npubs in `state/run.env`.
|
||||
5. Ask you to paste **your Amethyst account npub** (Identity A). This is
|
||||
cached for subsequent runs.
|
||||
6. Add the default public relays to both daemons and run a sanity check
|
||||
(publish a KP from B, fetch it from C).
|
||||
7. Print an **Amethyst setup checklist** — add the same relays to Amethyst,
|
||||
6. Boot the embedded relay (`amy serve`, i.e. geode, on `0.0.0.0:8080`),
|
||||
add it to both daemons and run a sanity check (publish a KP from B,
|
||||
fetch it from C). With `--public-relays` the default public set is used
|
||||
instead and the relay is not started.
|
||||
7. Print an **Amethyst setup checklist** — add the same relay to Amethyst,
|
||||
publish a KP, verify you are logged in with A.
|
||||
8. Run all 13 tests sequentially. Each test either:
|
||||
- runs `wn` commands fully automatically and asserts on JSON output, **or**
|
||||
@@ -253,9 +259,10 @@ The script will, in order:
|
||||
## Command-line flags
|
||||
|
||||
```
|
||||
--local-relays Use ws://localhost:8080 instead of the default public relays.
|
||||
Required if the public relays reject kinds 444/445/30443.
|
||||
Run 'just docker-up' inside the mdk checkout first.
|
||||
--public-relays Use the public relay set below instead of the embedded relay.
|
||||
The only mode whose test traffic leaves the machine; the
|
||||
public relays may reject kinds 444/445/30443.
|
||||
--port N Port for the embedded relay (default 8080).
|
||||
--transponder Run Test 14 (push notifications via the transponder service).
|
||||
--no-build Fail instead of rebuilding wn/wnd. Useful when iterating.
|
||||
-h, --help Show help.
|
||||
@@ -267,31 +274,31 @@ Environment overrides:
|
||||
WN_REPO=/some/path/mdk # use an existing checkout
|
||||
```
|
||||
|
||||
## Default relays
|
||||
## Relays
|
||||
|
||||
By default the harness owns the only relay: `amy serve` (geode) bound to
|
||||
`0.0.0.0:8080`. The `wn` daemons reach it on loopback; Amethyst reaches it
|
||||
over the network:
|
||||
|
||||
- **Android emulator:** add `ws://10.0.2.2:8080` to Settings → Relays,
|
||||
Settings → Key Package Relays and Settings → DM Inbox Relays.
|
||||
- **Physical device on same Wi-Fi:** add `ws://<laptop-LAN-ip>:8080`.
|
||||
|
||||
With `--public-relays` the daemons are bootstrapped on
|
||||
|
||||
```
|
||||
wss://relay.damus.io
|
||||
wss://nos.lol
|
||||
wss://relay.primal.net
|
||||
wss://nostr.bitcoiner.social
|
||||
wss://nostr.mom
|
||||
```
|
||||
|
||||
These are known to accept kind 1059 (gift wraps) and kind 30000+ (addressable
|
||||
events). If the **sanity check fails** — meaning C cannot read the KeyPackage
|
||||
that B just published — the harness warns you and continues. In that case
|
||||
re-run with `--local-relays` after starting the Docker stack:
|
||||
|
||||
```bash
|
||||
cd state/mdk
|
||||
just docker-up
|
||||
cd ../..
|
||||
./marmot-interop.sh --local-relays
|
||||
```
|
||||
|
||||
For Amethyst with `--local-relays`:
|
||||
|
||||
- **Android emulator:** add `ws://10.0.2.2:8080` to Settings → Relays and
|
||||
Settings → Key Package Relays.
|
||||
- **Physical device on same Wi-Fi:** add `ws://<laptop-LAN-ip>:8080`.
|
||||
instead and Amethyst is left on its own relay set, so the run surfaces
|
||||
real-world discovery failures (A's inbox behind NIP-42, whitelists, kinds the
|
||||
public relays drop). If the **sanity check fails** in that mode — meaning C
|
||||
cannot read the KeyPackage that B just published — the harness warns you and
|
||||
continues; re-run without `--public-relays` to rule the relays out.
|
||||
|
||||
## How human interaction works
|
||||
|
||||
|
||||
Vendored
+9
-11
@@ -3,8 +3,8 @@
|
||||
# cache-headless.sh — verifies the file-backed event store is the
|
||||
# source of truth for `amy` reads.
|
||||
#
|
||||
# Two amy identities (A and B) talk to a local nostr-rs-relay. We
|
||||
# assert that:
|
||||
# Two amy identities (A and B) talk to a local embedded relay
|
||||
# (`amy serve`, i.e. geode). We assert that:
|
||||
#
|
||||
# 1. After A runs `amy create`, A's local store contains the bootstrap
|
||||
# events (kind:0 / 3 / 10002 / 10050 / 10051 …).
|
||||
@@ -39,10 +39,11 @@ RESULTS_FILE="$STATE_DIR/results-$RUN_TS.tsv"
|
||||
|
||||
AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
||||
|
||||
# Reuse the relay binary the marmot harness builds.
|
||||
# Loopback relay = `amy serve` (geode), booted from $AMY_BIN by
|
||||
# start_local_relay in headless/helpers.sh. 127.0.0.2 rather than
|
||||
# 127.0.0.1 so Quartz's isLocalHost() filter doesn't strip it out of the
|
||||
# published relay lists (see the DM harness for the full note).
|
||||
RELAY_HOST="${RELAY_HOST:-127.0.0.2}"
|
||||
RELAY_REPO="${RELAY_REPO:-$TESTS_DIR/marmot/state-headless/nostr-rs-relay}"
|
||||
RELAY_BIN="$RELAY_REPO/target/release/nostr-rs-relay"
|
||||
RELAY_DATA="$STATE_DIR/relay"
|
||||
RELAY_PORT="${RELAY_PORT:-8092}"
|
||||
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"
|
||||
@@ -72,14 +73,11 @@ mkdir -p "$STATE_DIR" "$LOG_DIR"
|
||||
|
||||
# shellcheck source=../lib.sh
|
||||
source "$TESTS_DIR/lib.sh"
|
||||
# shellcheck source=../marmot/setup.sh — provides start_local_relay / stop_local_relay
|
||||
source "$TESTS_DIR/marmot/setup.sh"
|
||||
# shellcheck source=../headless/helpers.sh
|
||||
# shellcheck source=../headless/helpers.sh — amy wrappers + start_local_relay / stop_local_relay
|
||||
source "$TESTS_DIR/headless/helpers.sh"
|
||||
|
||||
# Keep the dm setup's preflight (just checks for amy + the relay) but
|
||||
# define our own identity bootstrap so we don't pull in DM-specific
|
||||
# wiring.
|
||||
# Keep the dm setup's preflight (just checks for amy) but define our own
|
||||
# identity bootstrap so we don't pull in DM-specific wiring.
|
||||
# shellcheck source=../dm/setup.sh
|
||||
source "$TESTS_DIR/dm/setup.sh"
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
# dm-interop-headless.sh — zero-prompt NIP-17 DM interop harness.
|
||||
#
|
||||
# Two `amy` processes (Identity A and Identity D) talk to each other
|
||||
# through a local nostr-rs-relay on ws://127.0.0.1:$RELAY_PORT. No
|
||||
# whitenoise-rs, no Marmot, no public internet traffic.
|
||||
# through a local embedded relay (`amy serve`, i.e. geode) on
|
||||
# ws://127.0.0.2:$RELAY_PORT. No MDK, no Marmot, no Rust toolchain, no
|
||||
# public internet traffic.
|
||||
#
|
||||
# Usage: ./dm-interop-headless.sh [--port N] [--no-build]
|
||||
#
|
||||
@@ -26,16 +27,14 @@ RESULTS_FILE="$STATE_DIR/results-$RUN_TS.tsv"
|
||||
|
||||
AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
||||
|
||||
# Share the nostr-rs-relay checkout with the Marmot harness to avoid
|
||||
# rebuilding it twice. Override RELAY_REPO / RELAY_DATA if you want full
|
||||
# isolation between runs.
|
||||
# Loopback relay = `amy serve` (geode), booted from $AMY_BIN by
|
||||
# start_local_relay in headless/helpers.sh. Override RELAY_DATA if you
|
||||
# want full isolation between runs.
|
||||
# Bind the loopback relay to 127.0.0.2 rather than 127.0.0.1 so Quartz's
|
||||
# `isLocalHost()` filter doesn't silently strip it out of the kind:10050
|
||||
# inbox events during recipient-relay resolution. 127.0.0.2 is still pure
|
||||
# loopback — no network traffic, no config needed.
|
||||
RELAY_HOST="${RELAY_HOST:-127.0.0.2}"
|
||||
RELAY_REPO="${RELAY_REPO:-$TESTS_DIR/marmot/state-headless/nostr-rs-relay}"
|
||||
RELAY_BIN="$RELAY_REPO/target/release/nostr-rs-relay"
|
||||
RELAY_DATA="$STATE_DIR/relay"
|
||||
RELAY_PORT="${RELAY_PORT:-8090}"
|
||||
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"
|
||||
@@ -68,12 +67,9 @@ mkdir -p "$STATE_DIR" "$LOG_DIR"
|
||||
# shellcheck source=../lib.sh
|
||||
source "$TESTS_DIR/lib.sh"
|
||||
|
||||
# Reuse start_local_relay / stop_local_relay from the Marmot harness's
|
||||
# setup.sh — the relay lifecycle is identical. preflight() there also
|
||||
# builds whitenoise-rs, which we don't need; setup.sh in this dir
|
||||
# defines a slimmer preflight_dm().
|
||||
# shellcheck source=../marmot/setup.sh
|
||||
source "$TESTS_DIR/marmot/setup.sh"
|
||||
# setup.sh in this dir defines the slim preflight_dm() (amy only, no
|
||||
# MDK); the relay lifecycle (start_local_relay / stop_local_relay, the
|
||||
# embedded `amy serve`) comes from the shared headless helpers.
|
||||
# shellcheck source=setup.sh
|
||||
source "$SCRIPT_DIR/setup.sh"
|
||||
# shellcheck source=../headless/helpers.sh
|
||||
|
||||
+7
-29
@@ -3,20 +3,21 @@
|
||||
# setup.sh — amy-only preflight + identity bootstrap for the
|
||||
# NIP-17 DM interop harness. Much slimmer than the Marmot setup:
|
||||
#
|
||||
# - Builds `amy` (same retry-on-503 logic as setup.sh).
|
||||
# - Builds nostr-rs-relay if missing.
|
||||
# - Builds `amy` (same retry-on-503 logic as setup.sh). The loopback
|
||||
# relay is `amy serve` (geode), so amy is the only binary needed.
|
||||
# - Bootstraps two fresh amy identities (A and D), each with its own
|
||||
# `--data-dir`, both pointed at the loopback relay.
|
||||
# - Publishes kind:10050 (plus NIP-65) for both so NIP-17's strict
|
||||
# recipient-inbox routing has something to resolve to.
|
||||
#
|
||||
# The heavy `start_local_relay` / `stop_local_relay` helpers live in
|
||||
# the Marmot harness's setup.sh and are sourced by the top-level harness.
|
||||
# The `start_local_relay` / `stop_local_relay` helpers (embedded
|
||||
# `amy serve`) live in ../headless/helpers.sh and are sourced by the
|
||||
# top-level harness.
|
||||
|
||||
# --- preflight (amy + relay only, no wn / Marmot patches) -------------------
|
||||
# --- preflight (amy only, no wn / Marmot patches, no Rust) -------------------
|
||||
preflight_dm() {
|
||||
banner "Preflight (DM harness)"
|
||||
for cmd in jq git cargo; do
|
||||
for cmd in jq git curl; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
fail_msg "missing required tool: $cmd"
|
||||
exit 1
|
||||
@@ -43,29 +44,6 @@ preflight_dm() {
|
||||
[[ -x "$AMY_BIN" ]] || { fail_msg "amy still missing after build"; exit 1; }
|
||||
info "amy: $AMY_BIN"
|
||||
|
||||
# nostr-rs-relay (same build path as the Marmot harness).
|
||||
if [[ ! -x "$RELAY_BIN" ]]; then
|
||||
if [[ "$NO_BUILD" -eq 1 ]]; then
|
||||
fail_msg "nostr-rs-relay not found at $RELAY_BIN and --no-build set"; exit 1
|
||||
fi
|
||||
if [[ ! -d "$RELAY_REPO/.git" ]]; then
|
||||
step "cloning nostr-rs-relay into $RELAY_REPO"
|
||||
git clone --depth 1 https://github.com/scsibug/nostr-rs-relay "$RELAY_REPO" \
|
||||
2>&1 | tee -a "$LOG_FILE"
|
||||
fi
|
||||
local attempt max=4
|
||||
for attempt in $(seq 1 $max); do
|
||||
step "building nostr-rs-relay (attempt $attempt/$max, ~3 min first run)"
|
||||
( cd "$RELAY_REPO" && cargo build --release --bin nostr-rs-relay ) \
|
||||
2>&1 | tee -a "$LOG_FILE"
|
||||
[[ -x "$RELAY_BIN" ]] && break
|
||||
[[ "$attempt" -lt "$max" ]] && warn "nostr-rs-relay build failed — retrying"
|
||||
done
|
||||
[[ -x "$RELAY_BIN" ]] || {
|
||||
fail_msg "nostr-rs-relay still missing after $max attempts"; exit 1
|
||||
}
|
||||
fi
|
||||
info "relay bin: $RELAY_BIN"
|
||||
}
|
||||
|
||||
# --- amy identity wrappers ---------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# tests-dm.sh — NIP-17 DM interop tests for two `amy` clients.
|
||||
#
|
||||
# Identity A (sender) and Identity D (recipient) each live in their own
|
||||
# --data-dir and share one loopback nostr-rs-relay. Tests cover:
|
||||
# --data-dir and share one loopback embedded relay (`amy serve` / geode). Tests cover:
|
||||
#
|
||||
# dm-01 text round-trip (both directions)
|
||||
# dm-02 dm list surfaces prior exchange with type:text discriminator
|
||||
|
||||
@@ -66,5 +66,91 @@ assert_eq() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- embedded relay (amy serve → geode) --------------------------------------
|
||||
# Every relay-backed harness talks to ONE loopback relay, and that relay is
|
||||
# `amy serve` — i.e. geode, the relay this repo ships — booted from the amy
|
||||
# binary the harness already built. No Rust toolchain, no clone, no cargo
|
||||
# build, no external relay binary: the relay under test is part of the
|
||||
# product, so a harness run exercises the same server code `amy serve`
|
||||
# and the standalone geode distribution run in production.
|
||||
#
|
||||
# Callers set (before sourcing or at least before calling):
|
||||
# AMY_BIN amy launcher (built via `./gradlew :cli:installDist`)
|
||||
# RELAY_HOST host clients connect to (most harnesses use 127.0.0.2 —
|
||||
# see the isLocalHost() note at the top of each script)
|
||||
# RELAY_BIND optional bind address; defaults to $RELAY_HOST. Set to
|
||||
# 0.0.0.0 when a device on the LAN must reach the relay.
|
||||
# RELAY_PORT listen port
|
||||
# RELAY_URL ws://$RELAY_HOST:$RELAY_PORT
|
||||
# RELAY_DATA scratch dir for the relay's own $HOME, pid file and logs
|
||||
#
|
||||
# The relay process runs as its own amy account ("relay") inside its own
|
||||
# $HOME under $RELAY_DATA, so its identity and store never mix with the
|
||||
# test identities. The store is in-memory (amy serve's default): every run
|
||||
# starts from an empty relay, matching the state wipe the harnesses do.
|
||||
start_local_relay() {
|
||||
banner "Starting embedded relay (amy serve / geode) on $RELAY_URL"
|
||||
local relay_home="$RELAY_DATA/home"
|
||||
local bind="${RELAY_BIND:-$RELAY_HOST}"
|
||||
mkdir -p "$relay_home" "$RELAY_DATA/logs"
|
||||
|
||||
[[ -x "$AMY_BIN" ]] || { fail_msg "amy not found at $AMY_BIN — build it with ./gradlew :cli:installDist"; exit 1; }
|
||||
|
||||
# Abort early if something else is already bound to the port — failing
|
||||
# with a clear error beats a mysterious-looking daemon stall later.
|
||||
# bash's /dev/tcp probe needs no `ss`/`lsof`; a refused connect on
|
||||
# loopback returns immediately.
|
||||
if (exec 3<>"/dev/tcp/$RELAY_HOST/$RELAY_PORT") 2>/dev/null; then
|
||||
fail_msg "port $RELAY_PORT already in use on $RELAY_HOST — pass --port N or free it"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# `amy serve` resolves its admin pubkey from the account, so the relay
|
||||
# needs an identity of its own. Idempotent across --reuse-state runs.
|
||||
if [[ ! -d "$relay_home/.amy/relay" ]]; then
|
||||
HOME="$relay_home" "$AMY_BIN" --account relay --secret-backend plaintext --json init \
|
||||
>"$RELAY_DATA/logs/init.log" 2>&1 \
|
||||
|| { fail_msg "amy init failed for the relay account (see $RELAY_DATA/logs/init.log)"; exit 1; }
|
||||
fi
|
||||
|
||||
nohup env HOME="$relay_home" "$AMY_BIN" --account relay --secret-backend plaintext \
|
||||
serve --host "$bind" --port "$RELAY_PORT" \
|
||||
>"$RELAY_DATA/logs/stdout.log" 2>"$RELAY_DATA/logs/stderr.log" &
|
||||
echo "$!" > "$RELAY_DATA/pid"
|
||||
step "relay pid $(cat "$RELAY_DATA/pid"); waiting for $RELAY_URL …"
|
||||
|
||||
# Readiness = the NIP-11 document answers on the same port. geode serves
|
||||
# it on a plain GET with `Accept: application/nostr+json` (anything else
|
||||
# gets a 426 hint, which curl -f would treat as failure).
|
||||
local deadline=$(( $(date +%s) + 60 ))
|
||||
while [[ $(date +%s) -lt $deadline ]]; do
|
||||
if curl -sSf -m 1 -H 'Accept: application/nostr+json' \
|
||||
"http://$RELAY_HOST:$RELAY_PORT/" >/dev/null 2>&1; then
|
||||
info "relay up"
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$(cat "$RELAY_DATA/pid")" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
fail_msg "relay never came up (see $RELAY_DATA/logs/stderr.log)"
|
||||
tail -n 40 "$RELAY_DATA/logs/stderr.log" 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
stop_local_relay() {
|
||||
local pid_file="$RELAY_DATA/pid"
|
||||
[[ -f "$pid_file" ]] || return 0
|
||||
local pid; pid=$(cat "$pid_file" 2>/dev/null || echo "")
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
info "stopping relay pid $pid"
|
||||
kill "$pid" 2>/dev/null || true
|
||||
sleep 1
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$pid_file"
|
||||
}
|
||||
|
||||
# --- wn-side pollers (delegates to lib.sh) -----------------------------------
|
||||
# Both exist in lib.sh already; this file only adds headless-specific niceties.
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# marmot-interop-headless.sh — zero-prompt, zero-internet interop harness.
|
||||
#
|
||||
# Drives Identity A via the `amy` CLI (./gradlew :cli:installDist) and
|
||||
# Identities B/C via MDK's `wn`/`wnd`. Spins up a local
|
||||
# nostr-rs-relay on ws://127.0.0.1:$RELAY_PORT so nothing ever leaves the
|
||||
# machine. Matches the 13 test scenarios in marmot-interop.sh but without
|
||||
# Identities B/C via MDK's `wn`/`wnd`. Spins up a local embedded relay
|
||||
# (`amy serve`, i.e. geode) on ws://127.0.0.2:$RELAY_PORT so nothing ever
|
||||
# leaves the machine. Matches the 13 test scenarios in marmot-interop.sh but without
|
||||
# any human prompts — all checks run to completion and the exit code
|
||||
# reflects pass/fail totals.
|
||||
#
|
||||
@@ -37,9 +37,10 @@ WN_BIN="$WN_REPO/target/release/wn"
|
||||
WND_BIN="$WN_REPO/target/release/wnd"
|
||||
AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
||||
|
||||
# Local relay wiring — cloned + built during preflight, started on
|
||||
# $RELAY_PORT. The harness never touches the public internet for test
|
||||
# traffic; wn/wnd/amy all point at this one loopback endpoint.
|
||||
# Local relay wiring — the embedded `amy serve` (geode), started on
|
||||
# $RELAY_PORT by start_local_relay (../headless/helpers.sh). The harness
|
||||
# never touches the public internet for test traffic; wn/wnd/amy all
|
||||
# point at this one loopback endpoint.
|
||||
#
|
||||
# Bind to 127.0.0.2 rather than 127.0.0.1: Quartz's RelayUrlNormalizer
|
||||
# strips literal 127.0.0.1 / localhost / 192.168.* out of NIP-17 inbox
|
||||
@@ -48,8 +49,6 @@ AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
||||
# Amethyst's public defaults instead of the loopback. 127.0.0.2 is
|
||||
# still pure loopback (no network traffic) but isn't on the strip list.
|
||||
RELAY_HOST="${RELAY_HOST:-127.0.0.2}"
|
||||
RELAY_REPO="${RELAY_REPO:-$STATE_DIR/nostr-rs-relay}"
|
||||
RELAY_BIN="$RELAY_REPO/target/release/nostr-rs-relay"
|
||||
RELAY_DATA="$STATE_DIR/relay"
|
||||
RELAY_PORT="${RELAY_PORT:-8080}"
|
||||
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"
|
||||
@@ -73,7 +72,7 @@ BLOSSOM_PID=""
|
||||
|
||||
NO_BUILD=0
|
||||
# Every run starts from empty stores. wnd already wipes B's and C's data dirs
|
||||
# on each start, but A's amy home and the relay's SQLite file used to survive,
|
||||
# on each start, but A's amy home and the relay's state used to survive,
|
||||
# and the leftovers are not inert: a KeyPackage A published in an earlier run
|
||||
# is still on the relay for B to invite with, an old group's kind:445 events
|
||||
# still arrive and fail to decrypt, and A's cursors still say it has seen them.
|
||||
@@ -119,8 +118,8 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
if [[ $RESET_STATE -eq 1 && -d "$STATE_DIR" ]]; then
|
||||
# Keep the relay checkout + its build (minutes to rebuild) and the log and
|
||||
# results history; drop everything that holds protocol state.
|
||||
# Keep the log and results history; drop everything that holds protocol
|
||||
# state (the relay is in-memory, so wiping its dir just drops its identity).
|
||||
#
|
||||
# run.env counts as protocol state: it is where tests hand each other group
|
||||
# ids. Leaving it behind a wipe leaves ids naming groups nobody is in any
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
# Sequential, all-or-nothing. Script drives the `wn` side automatically and
|
||||
# prompts the human operator at each step that requires Amethyst UI action.
|
||||
#
|
||||
# Usage: ./marmot-interop.sh [--local-relays] [--transponder] [--no-build]
|
||||
# Usage: ./marmot-interop.sh [--public-relays] [--port N] [--transponder] [--no-build]
|
||||
#
|
||||
# By default the harness boots its own relay — `amy serve`, i.e. geode — bound
|
||||
# to 0.0.0.0:$RELAY_PORT so the wn daemons reach it on loopback and the phone
|
||||
# reaches it over the LAN (ws://<laptop-ip>:PORT, or ws://10.0.2.2:PORT from an
|
||||
# emulator). Pass --public-relays to run the old real-world path against the
|
||||
# public relay set instead; that is the only mode that touches the internet.
|
||||
#
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../.." && pwd)"
|
||||
STATE_DIR="$SCRIPT_DIR/state"
|
||||
LOG_DIR="$STATE_DIR/logs"
|
||||
B_DIR="$STATE_DIR/B"
|
||||
@@ -27,6 +34,17 @@ RESULTS_FILE="$STATE_DIR/results-$RUN_TS.tsv"
|
||||
WN_REPO="${WN_REPO:-$STATE_DIR/mdk}"
|
||||
WN_BIN=""
|
||||
WND_BIN=""
|
||||
AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
||||
|
||||
# Embedded relay (default mode). Bound on every interface so a device on the
|
||||
# same network can reach it; the daemons connect over loopback. Loopback
|
||||
# `ws://` relays are only accepted by MDK behind this explicit opt-in.
|
||||
RELAY_HOST="127.0.0.1"
|
||||
RELAY_BIND="0.0.0.0"
|
||||
RELAY_PORT="${RELAY_PORT:-8080}"
|
||||
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"
|
||||
RELAY_DATA="$STATE_DIR/relay"
|
||||
export WN_ALLOW_LOOPBACK_RELAYS=1
|
||||
B_NPUB=""
|
||||
B_HEX=""
|
||||
C_NPUB=""
|
||||
@@ -34,6 +52,7 @@ C_HEX=""
|
||||
A_NPUB=""
|
||||
A_HEX=""
|
||||
|
||||
# Only used with --public-relays.
|
||||
DEFAULT_RELAYS=(
|
||||
"wss://relay.damus.io"
|
||||
"wss://nos.lol"
|
||||
@@ -41,7 +60,7 @@ DEFAULT_RELAYS=(
|
||||
"wss://nostr.bitcoiner.social"
|
||||
"wss://nostr.mom"
|
||||
)
|
||||
USE_LOCAL_RELAYS=0
|
||||
USE_PUBLIC_RELAYS=0
|
||||
ENABLE_TRANSPONDER=0
|
||||
NO_BUILD=0
|
||||
|
||||
@@ -50,7 +69,9 @@ usage() {
|
||||
marmot-interop.sh — Amethyst <-> MDK interop harness
|
||||
|
||||
Options:
|
||||
--local-relays Use ws://localhost:8080 instead of public relays (requires 'just docker-up')
|
||||
--public-relays Use the public relay set instead of the embedded relay
|
||||
(amy serve / geode, the default). Only mode that leaves the machine.
|
||||
--port N Port for the embedded relay (default 8080)
|
||||
--transponder Run Test 14 (MIP-05 push notifications)
|
||||
--no-build Don't rebuild wn/wnd if binaries are missing
|
||||
-h, --help Show this help
|
||||
@@ -62,8 +83,10 @@ EOF
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--local-relays) USE_LOCAL_RELAYS=1 ;;
|
||||
--transponder) ENABLE_TRANSPONDER=1 ;;
|
||||
--public-relays) USE_PUBLIC_RELAYS=1 ;;
|
||||
--local-relays) printf '%s\n' "note: --local-relays is now the default (embedded amy serve relay); flag ignored" >&2 ;;
|
||||
--port) RELAY_PORT="$2"; RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"; shift ;;
|
||||
--transponder) ENABLE_TRANSPONDER=1 ;;
|
||||
--no-build) NO_BUILD=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) printf 'unknown flag: %s\n' "$1" >&2; usage; exit 2 ;;
|
||||
@@ -77,6 +100,8 @@ mkdir -p "$STATE_DIR" "$LOG_DIR" "$B_DIR/logs" "$C_DIR/logs"
|
||||
|
||||
# shellcheck source=../lib.sh
|
||||
source "$TESTS_DIR/lib.sh"
|
||||
# shellcheck source=../headless/helpers.sh — start_local_relay / stop_local_relay (embedded amy serve)
|
||||
source "$TESTS_DIR/headless/helpers.sh"
|
||||
|
||||
# --- preflight ---------------------------------------------------------------
|
||||
preflight() {
|
||||
@@ -88,6 +113,26 @@ preflight() {
|
||||
printf ' %s: %s\n' "$cmd" "$(command -v "$cmd")" >>"$LOG_FILE"
|
||||
done
|
||||
|
||||
# The embedded relay is `amy serve`, so amy has to exist unless the run
|
||||
# goes to the public relays. Same transient-503 retry as the headless
|
||||
# harness: one bad jitpack/dl.google.com roll must not abort the run.
|
||||
if [[ "$USE_PUBLIC_RELAYS" -ne 1 && ! -x "$AMY_BIN" ]]; then
|
||||
if [[ "$NO_BUILD" -eq 1 ]]; then
|
||||
fail_msg "amy not found at $AMY_BIN and --no-build set"; exit 1
|
||||
fi
|
||||
local attempt max_attempts=4
|
||||
for attempt in $(seq 1 $max_attempts); do
|
||||
step "building :cli:installDist (attempt $attempt/$max_attempts)"
|
||||
if ( cd "$REPO_ROOT" && ./gradlew :cli:installDist ) 2>&1 | tee -a "$LOG_FILE" \
|
||||
&& [[ -x "$AMY_BIN" ]]; then
|
||||
break
|
||||
fi
|
||||
[[ "$attempt" -lt "$max_attempts" ]] && warn "gradle build failed (likely transient jitpack/Google 503) — retrying"
|
||||
done
|
||||
[[ -x "$AMY_BIN" ]] || { fail_msg "amy still missing after build"; exit 1; }
|
||||
printf ' amy: %s\n' "$AMY_BIN" >>"$LOG_FILE"
|
||||
fi
|
||||
|
||||
WN_BIN="$WN_REPO/target/release/wn"
|
||||
WND_BIN="$WN_REPO/target/release/wnd"
|
||||
|
||||
@@ -285,7 +330,7 @@ discover_a_relays() {
|
||||
if [[ -n "$kp_event_id" && "$kp_event_id" != "null" ]]; then
|
||||
info "wn_b found A's KeyPackage (kind:30443) — discovery plane is working"
|
||||
else
|
||||
warn "wn_b could NOT find A's KeyPackage. wn is bootstrapped on ${DEFAULT_RELAYS[*]}."
|
||||
warn "wn_b could NOT find A's KeyPackage. wn is bootstrapped on ${RELAY_LIST[*]}."
|
||||
warn "Either Amethyst never published a KeyPackage, or it's only on relays wn can't reach."
|
||||
warn "All later tests will fail. Fix this before continuing (tap KP publish in Amethyst settings)."
|
||||
fi
|
||||
@@ -300,7 +345,7 @@ discover_a_relays() {
|
||||
|
||||
if ! command -v sqlite3 >/dev/null 2>&1; then
|
||||
warn "sqlite3 not installed — skipping wn user_relays cache probe."
|
||||
warn "If Test 03 fails with 'no invite arrived', install sqlite3 or rerun with --local-relays."
|
||||
warn "If Test 03 fails with 'no invite arrived', install sqlite3 or rerun without --public-relays."
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -383,12 +428,7 @@ discover_a_relays() {
|
||||
# --- relays ------------------------------------------------------------------
|
||||
configure_relays() {
|
||||
banner "Configuring relays"
|
||||
local relays=()
|
||||
if [[ "$USE_LOCAL_RELAYS" -eq 1 ]]; then
|
||||
relays=( "ws://localhost:8080" )
|
||||
else
|
||||
relays=( "${DEFAULT_RELAYS[@]}" )
|
||||
fi
|
||||
local relays=( "${RELAY_LIST[@]}" )
|
||||
# Each relay × 3 types × 2 daemons produces a lot of repetitive "ok"
|
||||
# lines — the happy path doesn't need any of it on screen. Quiet the
|
||||
# per-add logging into $LOG_FILE and only surface real failures as
|
||||
@@ -527,27 +567,28 @@ configure_relays() {
|
||||
info "sanity kinds 10050/1059/445 ok (B->C welcome + message round-trip)"
|
||||
else
|
||||
warn "kind:445 failed — C never decrypted sanity-ping (relays may be dropping group messages)"
|
||||
warn "Consider rerunning with --local-relays."
|
||||
warn "Consider rerunning without --public-relays (the embedded relay accepts every kind)."
|
||||
fi
|
||||
# best-effort cleanup so re-runs don't accumulate dead sanity groups
|
||||
wn_c groups leave "$sanity_c_gid" >/dev/null 2>&1 || true
|
||||
wn_b groups leave "$sanity_gid" >/dev/null 2>&1 || true
|
||||
else
|
||||
warn "kind:10050/1059 failed — C never received welcome; relays likely dropping gift wraps or inbox lists"
|
||||
warn "Consider rerunning with --local-relays (requires 'just docker-up' in the mdk checkout)."
|
||||
warn "Consider rerunning without --public-relays (the embedded relay accepts every kind)."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
instruct_amethyst_setup() {
|
||||
if [[ "$USE_LOCAL_RELAYS" -eq 1 ]]; then
|
||||
# Offline/sandbox path: we own the only relay, so the harness DOES
|
||||
# need to dictate Amethyst's relay config — nothing is discoverable
|
||||
# via the public network.
|
||||
prompt_human "Configure Amethyst to match this --local-relays harness:
|
||||
if [[ "$USE_PUBLIC_RELAYS" -ne 1 ]]; then
|
||||
# Offline/sandbox path (default): we own the only relay — the embedded
|
||||
# `amy serve` (geode) on 0.0.0.0:$RELAY_PORT — so the harness DOES need
|
||||
# to dictate Amethyst's relay config; nothing is discoverable via the
|
||||
# public network.
|
||||
prompt_human "Configure Amethyst to use this harness's embedded relay (amy serve / geode):
|
||||
1. Settings -> Relays: add as READ+WRITE
|
||||
ws://10.0.2.2:8080 (Android emulator)
|
||||
ws://<your-LAN-ip>:8080 (physical device on same Wi-Fi)
|
||||
ws://10.0.2.2:$RELAY_PORT (Android emulator)
|
||||
ws://<your-LAN-ip>:$RELAY_PORT (physical device on same Wi-Fi)
|
||||
2. Settings -> Key Package Relays: add the SAME URL
|
||||
3. Settings -> DM Inbox Relays (NIP-17/kind:10050): add the SAME URL
|
||||
4. Trigger key-package publish (toggle KP relay on/off if needed)
|
||||
@@ -555,7 +596,7 @@ instruct_amethyst_setup() {
|
||||
return
|
||||
fi
|
||||
|
||||
# Public-relay path: the harness should behave like any real Nostr
|
||||
# --public-relays path: the harness should behave like any real Nostr
|
||||
# client — discover A's advertised relays via kind:10002 / 10050 /
|
||||
# 10051 and publish there, rather than forcing A to adopt the
|
||||
# harness's own relay set. That lets the tests surface real-world
|
||||
@@ -1301,6 +1342,7 @@ main() {
|
||||
local rc=$?
|
||||
trap - EXIT INT TERM HUP
|
||||
stop_daemons
|
||||
stop_local_relay
|
||||
print_summary
|
||||
exit "$rc"
|
||||
}
|
||||
@@ -1311,6 +1353,12 @@ main() {
|
||||
banner "Amethyst <-> MDK interop harness ($RUN_TS)"
|
||||
|
||||
preflight
|
||||
if [[ "$USE_PUBLIC_RELAYS" -eq 1 ]]; then
|
||||
RELAY_LIST=( "${DEFAULT_RELAYS[@]}" )
|
||||
else
|
||||
RELAY_LIST=( "$RELAY_URL" )
|
||||
start_local_relay
|
||||
fi
|
||||
start_daemon B "$B_DIR" "$B_SOCKET"
|
||||
start_daemon C "$C_DIR" "$C_SOCKET"
|
||||
ensure_identity B
|
||||
@@ -1327,7 +1375,7 @@ main() {
|
||||
# plane, then summarise what wn sees. Surfaces up front the kind of
|
||||
# failure (A's 10050 unreachable from wn, missing KP list, etc.) that
|
||||
# would otherwise bite as a silent Test 03 timeout.
|
||||
if [[ "$USE_LOCAL_RELAYS" -ne 1 ]]; then
|
||||
if [[ "$USE_PUBLIC_RELAYS" -eq 1 ]]; then
|
||||
discover_a_relays
|
||||
fi
|
||||
|
||||
|
||||
@@ -147,29 +147,8 @@ preflight() {
|
||||
info "wn: $WN_BIN ($(git -C "$WN_REPO" rev-parse --short HEAD 2>/dev/null || echo unknown))"
|
||||
info "wnd: $WND_BIN"
|
||||
|
||||
# Clone/build nostr-rs-relay — the harness's single loopback relay.
|
||||
if [[ ! -x "$RELAY_BIN" ]]; then
|
||||
if [[ "$NO_BUILD" -eq 1 ]]; then
|
||||
fail_msg "nostr-rs-relay not found at $RELAY_BIN and --no-build set"; exit 1
|
||||
fi
|
||||
if [[ ! -d "$RELAY_REPO/.git" ]]; then
|
||||
step "cloning nostr-rs-relay into $RELAY_REPO"
|
||||
git clone --depth 1 https://github.com/scsibug/nostr-rs-relay "$RELAY_REPO" \
|
||||
2>&1 | tee -a "$LOG_FILE"
|
||||
fi
|
||||
local attempt max=4
|
||||
for attempt in $(seq 1 $max); do
|
||||
step "building nostr-rs-relay (attempt $attempt/$max, ~3 min first run)"
|
||||
( cd "$RELAY_REPO" && cargo build --release --bin nostr-rs-relay ) \
|
||||
2>&1 | tee -a "$LOG_FILE"
|
||||
[[ -x "$RELAY_BIN" ]] && break
|
||||
[[ "$attempt" -lt "$max" ]] && warn "nostr-rs-relay build failed (likely transient 503 from crates.io) — retrying"
|
||||
done
|
||||
[[ -x "$RELAY_BIN" ]] || {
|
||||
fail_msg "nostr-rs-relay still missing after $max build attempts"; exit 1
|
||||
}
|
||||
fi
|
||||
info "relay bin: $RELAY_BIN"
|
||||
# The loopback relay is `amy serve` (geode) — see start_local_relay in
|
||||
# ../headless/helpers.sh. Nothing to clone or build beyond amy itself.
|
||||
}
|
||||
|
||||
# --- local QUIC broker -------------------------------------------------------
|
||||
@@ -262,75 +241,8 @@ stop_quic_broker() {
|
||||
}
|
||||
|
||||
# --- local relay -------------------------------------------------------------
|
||||
# Start nostr-rs-relay on $RELAY_PORT with a minimal config. Every test
|
||||
# runs against this one loopback endpoint — no external network traffic.
|
||||
start_local_relay() {
|
||||
banner "Starting local nostr-rs-relay on $RELAY_URL"
|
||||
mkdir -p "$RELAY_DATA" "$RELAY_DATA/logs"
|
||||
|
||||
# Render a minimal config file each run so port/limits come from the
|
||||
# harness rather than whatever was left on disk from a previous session.
|
||||
cat >"$RELAY_DATA/config.toml" <<EOF
|
||||
[info]
|
||||
relay_url = "$RELAY_URL"
|
||||
name = "amethyst-headless-harness"
|
||||
description = "Loopback relay for marmot-interop-headless.sh — do not use for anything real."
|
||||
|
||||
[database]
|
||||
data_directory = "$RELAY_DATA"
|
||||
|
||||
[network]
|
||||
address = "${RELAY_BIND:-${RELAY_HOST:-127.0.0.1}}"
|
||||
port = $RELAY_PORT
|
||||
|
||||
[options]
|
||||
reject_future_seconds = 3600
|
||||
|
||||
[limits]
|
||||
# Keep kind:444 / 445 / 1059 / 30443 wide open — the whole point is
|
||||
# exercising Marmot traffic the public relays reject.
|
||||
max_event_bytes = 524288
|
||||
max_ws_message_bytes = 1048576
|
||||
max_ws_frame_bytes = 1048576
|
||||
EOF
|
||||
|
||||
# Abort early if something else is already bound to the port — failing
|
||||
# with a clear error beats a mysterious-looking daemon stall later.
|
||||
if ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$RELAY_PORT\$"; then
|
||||
fail_msg "port $RELAY_PORT already in use — pass --port N or free it"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
nohup "$RELAY_BIN" --db "$RELAY_DATA" --config "$RELAY_DATA/config.toml" \
|
||||
>"$RELAY_DATA/logs/stdout.log" 2>"$RELAY_DATA/logs/stderr.log" &
|
||||
echo "$!" > "$RELAY_DATA/pid"
|
||||
step "relay pid $(cat "$RELAY_DATA/pid"); waiting for $RELAY_URL …"
|
||||
|
||||
local deadline=$(( $(date +%s) + 20 ))
|
||||
while [[ $(date +%s) -lt $deadline ]]; do
|
||||
if curl -sSf -m 1 "http://${RELAY_HOST:-127.0.0.1}:$RELAY_PORT/" >/dev/null 2>&1; then
|
||||
info "relay up"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
fail_msg "relay never came up (see $RELAY_DATA/logs/stderr.log)"
|
||||
tail -n 40 "$RELAY_DATA/logs/stderr.log" 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
stop_local_relay() {
|
||||
local pid_file="$RELAY_DATA/pid"
|
||||
[[ -f "$pid_file" ]] || return 0
|
||||
local pid; pid=$(cat "$pid_file" 2>/dev/null || echo "")
|
||||
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
info "stopping relay pid $pid"
|
||||
kill "$pid" 2>/dev/null || true
|
||||
sleep 1
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$pid_file"
|
||||
}
|
||||
# start_local_relay / stop_local_relay live in ../headless/helpers.sh: the
|
||||
# relay is the embedded `amy serve` (geode), shared by every harness.
|
||||
|
||||
# --- daemons -----------------------------------------------------------------
|
||||
start_daemon() {
|
||||
@@ -480,9 +392,9 @@ configure_relays() {
|
||||
|
||||
step "publishing A's KeyPackage"
|
||||
amy_a marmot key-package publish >>"$LOG_FILE" 2>&1 || warn "amy marmot key-package publish failed"
|
||||
# Give nostr-rs-relay a breath to fsync the kind:10002 / 10050 / 30443
|
||||
# Give the relay a breath to ingest the kind:10002 / 10050 / 30443
|
||||
# writes and push them out on the discovery subscription so that the
|
||||
# first `wn keys check` that follows actually sees them instead of
|
||||
# racing the relay's WAL flush.
|
||||
# racing the relay's ingest queue.
|
||||
sleep 2
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user