test(cli): move tests/ out of tools/, separate marmot vs dm

`tools/` is for libraries (arti-build); shell-based interop harnesses
that drive the `amy` binary belong with the CLI module. New layout:

  cli/tests/
  ├── lib.sh                       # shared logging + result tracking
  ├── headless/helpers.sh          # shared amy_a / amy_json / assertions
  ├── marmot/                      # MLS group-messaging interop (vs whitenoise-rs)
  │   ├── marmot-interop.sh
  │   ├── marmot-interop-headless.sh
  │   ├── setup.sh
  │   ├── tests-create.sh
  │   ├── tests-manage.sh
  │   ├── tests-extras.sh
  │   └── patches/
  └── dm/                          # NIP-17 DM interop (amy ↔ amy)
      ├── dm-interop-headless.sh
      ├── setup.sh
      └── tests-dm.sh

Per-suite changes:
- Each suite owns its own setup.sh; previously the Marmot setup was at
  headless/setup.sh and the DM setup at headless/setup-dm.sh, which
  implied shared infra they don't actually share.
- `cli/tests/lib.sh` and `cli/tests/headless/helpers.sh` are the only
  shared bits across suites.
- The DM harness still reuses Marmot's `start_local_relay` /
  `stop_local_relay` (sourced via `../marmot/setup.sh`) — same relay
  binary, no duplication.
- All sourcing paths updated; REPO_ROOT now climbs three levels (was
  two when the harness lived under `tools/`); patches/ is now a sibling
  of marmot/setup.sh rather than under marmot/headless/.
- `.gitignore` updated to cover both suites' state dirs.

cli/ROADMAP.md and cli/tests/README.md updated to point at the new paths.
No behavioural change — every test runs the same code; only the file
layout and source paths moved.
This commit is contained in:
Claude
2026-04-23 20:37:22 +00:00
parent 112801ba82
commit b0698e0a66
19 changed files with 87 additions and 59 deletions
+5 -5
View File
@@ -95,11 +95,11 @@ move anything, re-audit — you're probably duplicating logic.
8. **Distribution** — Homebrew + Scoop + `.deb` in the same release 8. **Distribution** — Homebrew + Scoop + `.deb` in the same release
pipeline as desktop. Plan: `cli/plans/2026-04-21-cli-distribution.md`. pipeline as desktop. Plan: `cli/plans/2026-04-21-cli-distribution.md`.
9. **Test suite** — end-to-end against a local relay. Marmot interop is 9. **Test suite** — end-to-end against a local relay. Marmot interop is
covered by `tools/marmot-interop/marmot-interop-headless.sh`; NIP-17 covered by `cli/tests/marmot/marmot-interop-headless.sh`; NIP-17 DM
DM interop between two `amy` clients is covered by interop between two `amy` clients is covered by
`tools/marmot-interop/dm-interop-headless.sh` (text + file + strict `cli/tests/dm/dm-interop-headless.sh` (text + file + strict 10050 +
10050 + fallback + cursor-advance). Neither runs in CI yet (both fallback + cursor-advance). Neither runs in CI yet (both need Rust +
need Rust + ~3 min cold relay build). ~3 min cold relay build).
10. **Everything else in the matrix.** 10. **Everything else in the matrix.**
--- ---
+3
View File
@@ -0,0 +1,3 @@
marmot/state/
marmot/state-headless/
dm/state-dm-headless/
@@ -1,22 +1,45 @@
# Interop Test Harnesses # amy CLI test harnesses
Two flavours of the Marmot harness, same scenarios: Shell-based end-to-end harnesses that drive the `amy` CLI binary against a
loopback `nostr-rs-relay`. Layout:
- **`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). cli/tests/
Use this for final UI verification. ├── lib.sh # shared logging, results, assertions
- **`marmot-interop-headless.sh`** — zero prompts. Drives A via the `amy` CLI ├── headless/ # shared bits used by every harness
(`./gradlew :cli:installDist`) and B/C via `wn`. Runs every scenario │ └── helpers.sh
end-to-end and exits with a pass/fail summary. Use this for CI and for ├── marmot/ # Marmot / MLS group-messaging interop
iterating on the Nostr/Marmot plumbing without needing to touch a phone. │ ├── 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: A third, slimmer harness covers the NIP-17 DM surface:
- **`dm-interop-headless.sh`** — two `amy` processes (Identity A and - **`dm/dm-interop-headless.sh`** — two `amy` processes (Identity A and
Identity D) exchange NIP-17 DMs through the loopback nostr-rs-relay. Identity D) exchange NIP-17 DMs through the loopback nostr-rs-relay.
No whitenoise-rs required — only `amy` and the relay binary (which No whitenoise-rs required — only `amy` and the relay binary (which
is shared with the Marmot harness's checkout at is shared with the Marmot harness's checkout at
`state-headless/nostr-rs-relay/`). `marmot/state-headless/nostr-rs-relay/`).
Both Marmot harnesses validate Amethyst against **whitenoise-rs** Both Marmot harnesses validate Amethyst against **whitenoise-rs**
(https://github.com/marmot-protocol/whitenoise-rs), the reference Rust (https://github.com/marmot-protocol/whitenoise-rs), the reference Rust
@@ -42,7 +65,7 @@ the end of the run.
| 12 | Offline catch-up / replay | | | 12 | Offline catch-up / replay | |
| 13 | KeyPackage rotation | | | 13 | KeyPackage rotation | |
### DM (amy ↔ amy, NIP-17) — `dm-interop-headless.sh` ### DM (amy ↔ amy, NIP-17) — `dm/dm-interop-headless.sh`
| # | Test | | # | Test |
|---|---| |---|---|
@@ -11,7 +11,8 @@
set -uo pipefail set -uo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../.." && pwd)"
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
STATE_DIR="$SCRIPT_DIR/state-dm-headless" STATE_DIR="$SCRIPT_DIR/state-dm-headless"
LOG_DIR="$STATE_DIR/logs" LOG_DIR="$STATE_DIR/logs"
A_DIR="$STATE_DIR/A" A_DIR="$STATE_DIR/A"
@@ -26,7 +27,7 @@ AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
# Share the nostr-rs-relay checkout with the Marmot harness to avoid # Share the nostr-rs-relay checkout with the Marmot harness to avoid
# rebuilding it twice. Override RELAY_REPO / RELAY_DATA if you want full # rebuilding it twice. Override RELAY_REPO / RELAY_DATA if you want full
# isolation between runs. # isolation between runs.
RELAY_REPO="${RELAY_REPO:-$SCRIPT_DIR/state-headless/nostr-rs-relay}" RELAY_REPO="${RELAY_REPO:-$TESTS_DIR/marmot/state-headless/nostr-rs-relay}"
RELAY_BIN="$RELAY_REPO/target/release/nostr-rs-relay" RELAY_BIN="$RELAY_REPO/target/release/nostr-rs-relay"
RELAY_DATA="$STATE_DIR/relay" RELAY_DATA="$STATE_DIR/relay"
RELAY_PORT="${RELAY_PORT:-8090}" RELAY_PORT="${RELAY_PORT:-8090}"
@@ -54,22 +55,23 @@ mkdir -p "$STATE_DIR" "$LOG_DIR" "$A_DIR" "$D_DIR"
: >"$LOG_FILE" : >"$LOG_FILE"
: >"$RESULTS_FILE" : >"$RESULTS_FILE"
# Reuse the logging + result helpers from the Marmot harness. lib.sh # Reuse the logging + result helpers shared between every harness.
# also declares wn-specific helpers; they're harmless when unused. # lib.sh declares wn-specific helpers too — harmless when unused.
# shellcheck source=lib.sh # shellcheck source=../lib.sh
source "$SCRIPT_DIR/lib.sh" source "$TESTS_DIR/lib.sh"
# Reuse start_local_relay / stop_local_relay from setup.sh. preflight() # Reuse start_local_relay / stop_local_relay from the Marmot harness's
# there also builds whitenoise-rs, which we don't need — setup-dm.sh # 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(). # defines a slimmer preflight_dm().
# shellcheck source=headless/setup.sh # shellcheck source=../marmot/setup.sh
source "$SCRIPT_DIR/headless/setup.sh" source "$TESTS_DIR/marmot/setup.sh"
# shellcheck source=headless/setup-dm.sh # shellcheck source=setup.sh
source "$SCRIPT_DIR/headless/setup-dm.sh" source "$SCRIPT_DIR/setup.sh"
# shellcheck source=headless/helpers.sh # shellcheck source=../headless/helpers.sh
source "$SCRIPT_DIR/headless/helpers.sh" source "$TESTS_DIR/headless/helpers.sh"
# shellcheck source=headless/tests-dm.sh # shellcheck source=tests-dm.sh
source "$SCRIPT_DIR/headless/tests-dm.sh" source "$SCRIPT_DIR/tests-dm.sh"
cleanup() { cleanup() {
local rc=$? local rc=$?
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/setup-dm.sh — amy-only preflight + identity bootstrap for the # setup.sh — amy-only preflight + identity bootstrap for the
# NIP-17 DM interop harness. Much slimmer than the Marmot setup: # NIP-17 DM interop harness. Much slimmer than the Marmot setup:
# #
# - Builds `amy` (same retry-on-503 logic as setup.sh). # - Builds `amy` (same retry-on-503 logic as setup.sh).
@@ -11,7 +11,7 @@
# recipient-inbox routing has something to resolve to. # recipient-inbox routing has something to resolve to.
# #
# The heavy `start_local_relay` / `stop_local_relay` helpers live in # The heavy `start_local_relay` / `stop_local_relay` helpers live in
# headless/setup.sh and are sourced by the top-level harness. # the Marmot harness's setup.sh and are sourced by the top-level harness.
# --- preflight (amy + relay only, no wn / Marmot patches) ------------------- # --- preflight (amy + relay only, no wn / Marmot patches) -------------------
preflight_dm() { preflight_dm() {
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/tests-dm.sh — NIP-17 DM interop tests for two `amy` clients. # tests-dm.sh — NIP-17 DM interop tests for two `amy` clients.
# #
# Identity A (sender) and Identity D (recipient) each live in their own # 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 nostr-rs-relay. Tests cover:
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/helpers.sh — thin wrappers that keep the per-test code tight. # helpers.sh — thin wrappers that keep the per-test code tight.
# --- amy wrapper ------------------------------------------------------------- # --- amy wrapper -------------------------------------------------------------
amy_a() { "$AMY_BIN" --data-dir "$A_DIR" "$@"; } amy_a() { "$AMY_BIN" --data-dir "$A_DIR" "$@"; }
@@ -14,7 +14,8 @@
set -uo pipefail set -uo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../.." && pwd)"
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
STATE_DIR="$SCRIPT_DIR/state-headless" STATE_DIR="$SCRIPT_DIR/state-headless"
LOG_DIR="$STATE_DIR/logs" LOG_DIR="$STATE_DIR/logs"
A_DIR="$STATE_DIR/A" A_DIR="$STATE_DIR/A"
@@ -66,19 +67,19 @@ mkdir -p "$STATE_DIR" "$LOG_DIR" "$A_DIR" "$B_DIR/logs" "$C_DIR/logs"
: >"$RESULTS_FILE" : >"$RESULTS_FILE"
# Reuse colours / logging / dump_daemon_diagnostics from the interactive harness. # Reuse colours / logging / dump_daemon_diagnostics from the interactive harness.
# shellcheck source=lib.sh # shellcheck source=../lib.sh
source "$SCRIPT_DIR/lib.sh" source "$TESTS_DIR/lib.sh"
# shellcheck source=headless/setup.sh # shellcheck source=setup.sh
source "$SCRIPT_DIR/headless/setup.sh" source "$SCRIPT_DIR/setup.sh"
# shellcheck source=headless/helpers.sh # shellcheck source=../headless/helpers.sh
source "$SCRIPT_DIR/headless/helpers.sh" source "$TESTS_DIR/headless/helpers.sh"
# shellcheck source=headless/tests-create.sh # shellcheck source=tests-create.sh
source "$SCRIPT_DIR/headless/tests-create.sh" source "$SCRIPT_DIR/tests-create.sh"
# shellcheck source=headless/tests-manage.sh # shellcheck source=tests-manage.sh
source "$SCRIPT_DIR/headless/tests-manage.sh" source "$SCRIPT_DIR/tests-manage.sh"
# shellcheck source=headless/tests-extras.sh # shellcheck source=tests-extras.sh
source "$SCRIPT_DIR/headless/tests-extras.sh" source "$SCRIPT_DIR/tests-extras.sh"
# Make sure Ctrl+C / SIGTERM / SIGHUP all run the full cleanup path — # Make sure Ctrl+C / SIGTERM / SIGHUP all run the full cleanup path —
# otherwise wnd is nohup'd and keeps running after the script dies, # otherwise wnd is nohup'd and keeps running after the script dies,
@@ -10,6 +10,7 @@
set -uo pipefail set -uo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
STATE_DIR="$SCRIPT_DIR/state" STATE_DIR="$SCRIPT_DIR/state"
LOG_DIR="$STATE_DIR/logs" LOG_DIR="$STATE_DIR/logs"
B_DIR="$STATE_DIR/B" B_DIR="$STATE_DIR/B"
@@ -75,8 +76,8 @@ mkdir -p "$STATE_DIR" "$LOG_DIR" "$B_DIR/logs" "$C_DIR/logs"
: >"$LOG_FILE" : >"$LOG_FILE"
: >"$RESULTS_FILE" : >"$RESULTS_FILE"
# shellcheck source=lib.sh # shellcheck source=../lib.sh
source "$SCRIPT_DIR/lib.sh" source "$TESTS_DIR/lib.sh"
# --- preflight --------------------------------------------------------------- # --- preflight ---------------------------------------------------------------
preflight() { preflight() {
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/setup.sh — preflight + daemon lifecycle + identity bootstrap. # setup.sh — preflight + daemon lifecycle + identity bootstrap.
# Sourced from marmot-interop-headless.sh. # Sourced from marmot-interop-headless.sh.
# --- preflight --------------------------------------------------------------- # --- preflight ---------------------------------------------------------------
@@ -90,7 +90,7 @@ preflight() {
if [[ ! -f "$marker" ]]; then if [[ ! -f "$marker" ]]; then
step "patching whitenoise-rs: $name" step "patching whitenoise-rs: $name"
if ( cd "$WN_REPO" && patch -p1 --forward --reject-file=- \ if ( cd "$WN_REPO" && patch -p1 --forward --reject-file=- \
<"$SCRIPT_DIR/headless/patches/$name" >>"$LOG_FILE" 2>&1 ); then <"$SCRIPT_DIR/patches/$name" >>"$LOG_FILE" 2>&1 ); then
touch "$marker" touch "$marker"
# Invalidate the previous build so the patched source is picked up. # Invalidate the previous build so the patched source is picked up.
rm -f "$WN_BIN" "$WND_BIN" rm -f "$WN_BIN" "$WND_BIN"
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/tests-create.sh — tests 01..05. # tests-create.sh — tests 01..05.
# Focus: KeyPackage discovery, group creation, invites, initial messages. # Focus: KeyPackage discovery, group creation, invites, initial messages.
test_01_keypackage_discovery() { test_01_keypackage_discovery() {
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/tests-extras.sh — tests 09, 10, 12, 13. # tests-extras.sh — tests 09, 10, 12, 13.
# Focus: reactions/replies, concurrent commits, offline catchup, KP rotation. # Focus: reactions/replies, concurrent commits, offline catchup, KP rotation.
test_09_reply_react_unreact() { test_09_reply_react_unreact() {
@@ -1,6 +1,6 @@
# shellcheck shell=bash # shellcheck shell=bash
# #
# headless/tests-manage.sh — tests 06, 07, 08, 11. # tests-manage.sh — tests 06, 07, 08, 11.
# Focus: removal, metadata rename, admin promote/demote, leave. # Focus: removal, metadata rename, admin promote/demote, leave.
test_06_member_removal() { test_06_member_removal() {
-2
View File
@@ -1,2 +0,0 @@
state/
state-headless/