Files
amethyst/cli/tests/README.md
T
Claude b8a642b406 fix(cli/tests/dm): bind relay to 127.0.0.2 + dm-06 → --since filter
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.
2026-04-23 21:32:39 +00:00

248 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 0105
│ ├── tests-manage.sh # tests 0608, 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.