b8a642b406
Two fixes needed to make the DM harness actually pass end-to-end:
1. Bind the loopback relay to 127.0.0.2 instead of 127.0.0.1.
Quartz's `RelayTag.parse` (called from `ChatMessageRelayListEvent.
relays()`) rejects any URL for which `isLocalHost()` returns true —
the substring check matches `"127.0.0.1"` among others. That means a
kind:10050 event whose `relay` tag reads `ws://127.0.0.1:8090/`
silently parses to an empty relay list, and `RecipientRelayFetcher`
reports `dmInbox == []`. Under strict mode `dm send` then correctly
refuses with `no_dm_relays` — hiding an otherwise-valid test setup.
`127.0.0.2` is the cleanest fix: still pure loopback (no network
traffic, no config needed), not matched by `isLocalHost()`, and
Linux binds any IP in 127.0.0.0/8 without setup. The Marmot harness
is unaffected because its tests never depend on parsing their own
kind:10051/10002 relay tags back out — they hit the bootstrap
fallback path.
marmot/setup.sh's `start_local_relay` now reads `${RELAY_HOST:-
127.0.0.1}` so both harnesses can share it; the DM harness sets
`RELAY_HOST=127.0.0.2` by default and exposes `--host` as an escape
hatch.
2. Replace dm-06 cursor-advance check with `--since` filter check.
The original assertion ("second no-flag `dm list` returns 0") was
wrong for the actual design: `filterGiftWrapsToPubkey` always
subtracts a 2-day lookback from `since` (required to catch NIP-59's
randomised `created_at`), so a repeat no-flag query legitimately
re-pulls everything in the last two days. The cursor advances the
scan window for efficiency, not for idempotency. dm-06 now verifies
`--since` actually filters: query with `--since <newest + 2 days +
1 hour>` — after the filter's lookback subtraction, the effective
floor lands past every message, and the list comes back empty.
Also: added `protoc` to the DM preflight checks so the missing-dep
error comes before 4 failed cargo retries.
Result on this machine: 6/6 pass, two clean runs in a row.
248 lines
9.5 KiB
Markdown
248 lines
9.5 KiB
Markdown
# amy CLI test harnesses
|
||
|
||
Shell-based end-to-end harnesses that drive the `amy` CLI binary against a
|
||
loopback `nostr-rs-relay`. Layout:
|
||
|
||
```
|
||
cli/tests/
|
||
├── lib.sh # shared logging, results, assertions
|
||
├── headless/ # shared bits used by every harness
|
||
│ └── helpers.sh
|
||
├── 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
|
||
│ ├── tests-create.sh # tests 01–05
|
||
│ ├── tests-manage.sh # tests 06–08, 11
|
||
│ ├── tests-extras.sh # tests 09, 10, 12, 13
|
||
│ └── patches/ # whitenoise-rs harness patches
|
||
└── dm/ # NIP-17 DM interop (amy ↔ amy)
|
||
├── dm-interop-headless.sh
|
||
├── setup.sh # preflight + identities
|
||
└── tests-dm.sh
|
||
```
|
||
|
||
The Marmot harnesses come in two flavours, same scenarios:
|
||
|
||
- **`marmot/marmot-interop.sh`** — interactive. Drives B/C via `wn` and
|
||
**prompts the human** to perform each Amethyst-side step in the mobile UI
|
||
(Identity A). Use this for final UI verification.
|
||
- **`marmot/marmot-interop-headless.sh`** — zero prompts. Drives A via the
|
||
`amy` CLI (`./gradlew :cli:installDist`) and B/C via `wn`. Runs every
|
||
scenario end-to-end and exits with a pass/fail summary. Use this for CI
|
||
and for iterating on the Nostr/Marmot plumbing without needing to touch a
|
||
phone.
|
||
|
||
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 whitenoise-rs required — only `amy` and the relay binary (which
|
||
is shared with the Marmot harness's checkout at
|
||
`marmot/state-headless/nostr-rs-relay/`).
|
||
|
||
Both Marmot harnesses validate Amethyst against **whitenoise-rs**
|
||
(https://github.com/marmot-protocol/whitenoise-rs), the reference Rust
|
||
implementation that powers the White Noise Flutter app. Every test records a
|
||
pass/fail/skip result into a tab-separated log, and the summary is printed at
|
||
the end of the run.
|
||
|
||
## What gets tested
|
||
|
||
| # | Test | Needs 3rd identity |
|
||
|---|---|---|
|
||
| 01 | KeyPackage publish & discovery (MIP-00) | – |
|
||
| 02 | Amethyst creates group, invites wn | – |
|
||
| 03 | wn creates group, invites Amethyst | – |
|
||
| 04 | 3-member group, add-after-create | yes |
|
||
| 05 | wn adds Amethyst to existing group | yes |
|
||
| 06 | Member removal + forward secrecy | yes |
|
||
| 07 | Group metadata rename round-trip (MIP-01) | – |
|
||
| 08 | Admin promote / demote | yes |
|
||
| 09 | Reply / react / unreact (inner event kinds 9, 7) | – |
|
||
| 10 | Concurrent commits race | – |
|
||
| 11 | Leave group | – |
|
||
| 12 | Offline catch-up / replay | – |
|
||
| 13 | KeyPackage rotation | – |
|
||
|
||
### DM (amy ↔ amy, NIP-17) — `dm/dm-interop-headless.sh`
|
||
|
||
| # | Test |
|
||
|---|---|
|
||
| dm-01 | Text round-trip A↔D (kind:14) |
|
||
| dm-02 | `dm list` returns prior exchange with `type:text` discriminator |
|
||
| dm-03 | Strict kind:10050 refuses sends to an inboxless recipient |
|
||
| dm-04 | `--allow-fallback` opts into the NIP-65 read / bootstrap chain |
|
||
| dm-05 | File message reference mode round-trip (kind:15 with manual key/nonce) |
|
||
| dm-06 | `dm list --since` filters out older messages (window-slide past the newest event returns 0) |
|
||
|
||
**Relay binding note:** the DM harness binds the loopback relay to
|
||
`127.0.0.2` (not `127.0.0.1`) because Quartz's `RelayTag.parse` rejects
|
||
localhost URLs via `isLocalHost()` — so `ws://127.0.0.1` in a kind:10050
|
||
event is silently stripped during recipient-relay resolution, which
|
||
would make strict-mode DM sends spuriously fail. `127.0.0.2` is still
|
||
pure loopback and isn't matched by that filter. Override with
|
||
`--host 127.0.0.5` etc. if `127.0.0.2` is taken.
|
||
|
||
**Note:** dm-05 validates the kind:15 wire format via reference mode
|
||
(caller supplies the URL + AES-GCM key/nonce). The upload-mode variant
|
||
(`dm send-file --file PATH --server URL`) needs a local Blossom server
|
||
and isn't scripted here — the upload classes are unit-tested on desktop
|
||
at `desktopApp/src/jvmTest/kotlin/.../service/upload/`.
|
||
| 14 | Push notifications (MIP-05) | opt-in via `--transponder` |
|
||
|
||
## Prerequisites
|
||
|
||
On the machine that runs the harness:
|
||
|
||
- **Rust 1.90+** — install via https://rustup.rs
|
||
- **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)
|
||
|
||
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)
|
||
|
||
## Quick start
|
||
|
||
```bash
|
||
cd tools/marmot-interop
|
||
./marmot-interop.sh
|
||
```
|
||
|
||
The script will, in order:
|
||
|
||
1. Verify `jq`, `git`, `cargo` etc. are present.
|
||
2. Clone `whitenoise-rs` into `state/whitenoise-rs/` and build `wn`/`wnd`
|
||
(release, `--features cli`). First build takes ~5 minutes; subsequent runs
|
||
reuse the binaries.
|
||
3. Launch two `wnd` daemons (one for Identity B, one for Identity C).
|
||
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,
|
||
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**
|
||
- prints a "DO THIS IN AMETHYST" prompt and waits for you to press `<Enter>`,
|
||
then verifies the Amethyst action via `wn`.
|
||
9. Stop the daemons and print a results table.
|
||
|
||
## 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 whitenoise-rs first.
|
||
--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.
|
||
```
|
||
|
||
Environment overrides:
|
||
|
||
```
|
||
WN_REPO=/some/path/whitenoise-rs # use an existing checkout
|
||
```
|
||
|
||
## Default relays
|
||
|
||
```
|
||
wss://relay.damus.io
|
||
wss://nos.lol
|
||
wss://relay.primal.net
|
||
```
|
||
|
||
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/whitenoise-rs
|
||
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`.
|
||
|
||
## How human interaction works
|
||
|
||
When the script needs you to do something in Amethyst, it prints a yellow
|
||
block like this:
|
||
|
||
```
|
||
---- DO THIS IN AMETHYST ----
|
||
In Amethyst:
|
||
1. Tap + -> Create Group
|
||
2. Name: Interop-02
|
||
3. Add member: npub1abc...
|
||
4. Tap Create / Send Invite
|
||
-----------------------------
|
||
[Press Enter to continue]
|
||
```
|
||
|
||
After you press Enter the script resumes. For UI-only verifications (e.g.
|
||
"does Amethyst show reaction 🌮?"), the script asks:
|
||
|
||
```
|
||
? Does Amethyst show the 🌮 reaction? [p]ass / [f]ail / [s]kip:
|
||
```
|
||
|
||
Pick `p`, `f`, or `s`.
|
||
|
||
## Output
|
||
|
||
- **`state/logs/run-<timestamp>.log`** — every step, assertion, and human
|
||
prompt, with the exact `wn` stdout that was parsed.
|
||
- **`state/results-<timestamp>.tsv`** — tab-separated `test_id \t status \t
|
||
note` lines. Easy to grep.
|
||
- **`state/run.env`** — persistent key/value state (npubs, group ids) so you
|
||
can kill and resume the harness mid-run without losing context.
|
||
- **Final summary table on stdout** — colored per-test status and totals.
|
||
|
||
## State cleanup
|
||
|
||
The harness leaves `state/` on disk so daemons and identities survive across
|
||
runs. To start completely fresh:
|
||
|
||
```bash
|
||
# stops daemons if still running; removes identities, groups, logs
|
||
./marmot-interop.sh --no-build # Ctrl-C when it waits for input, then:
|
||
rm -rf state/
|
||
```
|
||
|
||
Published KeyPackages on public relays will remain until they expire naturally
|
||
or are deleted via `wn keys delete-all --confirm`. Use a throwaway identity
|
||
for B/C if this matters to you.
|
||
|
||
## Known gaps
|
||
|
||
- **Test 10 (concurrent commits)** is inherently human-timing sensitive. It's
|
||
a best-effort race; expect occasional flakes.
|
||
- **Test 14 (push)** only exercises the harness side — full end-to-end
|
||
verification requires a running `transponder` instance and platform
|
||
registrations that are out of scope here.
|
||
- The harness assumes Amethyst exposes UI affordances for add-member,
|
||
remove-member, rename, promote/demote, and leave. If any of those is missing
|
||
from the current build, the corresponding test will fail with a clear note
|
||
rather than crash.
|
||
|
||
## Files
|
||
|
||
- `marmot-interop.sh` — main entry point; orchestrates preflight, daemons,
|
||
identities, relays, and runs the 13 tests in sequence.
|
||
- `lib.sh` — helpers (logging, prompts, polling, jq wrappers, result table).
|
||
- `state/` — runtime directory, gitignored. Contains `whitenoise-rs/` source
|
||
checkout, per-daemon data/log dirs, the session `run.env`, logs, and
|
||
results TSVs.
|