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:
@@ -0,0 +1,220 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# dm-01 text round-trip (both directions)
|
||||
# dm-02 dm list surfaces prior exchange with type:text discriminator
|
||||
# dm-03 strict kind:10050 routing refuses sends to inboxless recipient
|
||||
# dm-04 --allow-fallback opts into the NIP-65 read / bootstrap chain
|
||||
# dm-05 file message reference mode round-trip (kind:15)
|
||||
# dm-06 cursor advance (subsequent no-flag `dm list` is empty)
|
||||
|
||||
# Wrap amy_json around either data-dir so the per-test code stays tight.
|
||||
amy_json_for() {
|
||||
local dir="$1"; shift
|
||||
local out
|
||||
if ! out=$("$AMY_BIN" --data-dir "$dir" "$@" 2>>"$LOG_FILE"); then
|
||||
fail_msg "amy --data-dir $dir $*: exit $? (see $LOG_FILE)"
|
||||
printf '%s\n' "$out" >>"$LOG_FILE"
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
amy_json_a() { amy_json_for "$A_DIR" "$@"; }
|
||||
amy_json_d() { amy_json_for "$D_DIR" "$@"; }
|
||||
|
||||
test_01_dm_text_round_trip() {
|
||||
banner "DM-01 — text round-trip A↔D (kind:14)"
|
||||
local id="dm-01 text round-trip"
|
||||
|
||||
# A → D
|
||||
local out_send
|
||||
out_send=$(amy_json_a dm send "$D_NPUB" "hello from A") || {
|
||||
record_result "$id" fail "A send failed"; return
|
||||
}
|
||||
local event_id recipients_ok
|
||||
event_id=$(printf '%s' "$out_send" | jq -r '.event_id')
|
||||
recipients_ok=$(printf '%s' "$out_send" \
|
||||
| jq -r '[.recipients[] | select(.relay_source == "kind_10050")] | length')
|
||||
info "A sent kind:14 event_id=$event_id (to ${recipients_ok} kind:10050 inboxes)"
|
||||
if [[ -z "$event_id" || "$event_id" == "null" ]]; then
|
||||
record_result "$id" fail "A send response missing event_id"; return
|
||||
fi
|
||||
|
||||
# D awaits the text
|
||||
if ! amy_json_d dm await --peer "$A_NPUB" --match "hello from A" --timeout 30 >/dev/null; then
|
||||
record_result "$id" fail "D never received A's text"; return
|
||||
fi
|
||||
info "D received A's text"
|
||||
|
||||
# D → A
|
||||
amy_json_d dm send "$A_NPUB" "reply from D" >/dev/null || {
|
||||
record_result "$id" fail "D send failed"; return
|
||||
}
|
||||
if ! amy_json_a dm await --peer "$D_NPUB" --match "reply from D" --timeout 30 >/dev/null; then
|
||||
record_result "$id" fail "A never received D's reply"; return
|
||||
fi
|
||||
info "A received D's reply"
|
||||
|
||||
record_result "$id" pass
|
||||
}
|
||||
|
||||
test_02_dm_list_surfaces_history() {
|
||||
banner "DM-02 — dm list returns text messages with type discriminator"
|
||||
local id="dm-02 dm list text history"
|
||||
|
||||
local out
|
||||
out=$(amy_json_a dm list --peer "$D_NPUB" --timeout 5) || {
|
||||
record_result "$id" fail "amy dm list failed"; return
|
||||
}
|
||||
local text_count reply_present
|
||||
text_count=$(printf '%s' "$out" \
|
||||
| jq -r '[.messages[] | select(.type == "text")] | length')
|
||||
reply_present=$(printf '%s' "$out" \
|
||||
| jq -r '[.messages[] | select(.content == "reply from D")] | length')
|
||||
info "A sees $text_count text message(s) with D; reply_present=$reply_present"
|
||||
if [[ "$reply_present" == "1" ]]; then
|
||||
record_result "$id" pass
|
||||
else
|
||||
record_result "$id" fail "D's reply missing from A's dm list"
|
||||
fi
|
||||
}
|
||||
|
||||
test_03_dm_send_rejects_no_inbox() {
|
||||
banner "DM-03 — strict kind:10050 refuses sends to inboxless recipient"
|
||||
local id="dm-03 strict no_dm_relays"
|
||||
|
||||
# Generate a throwaway identity but do NOT publish its kind:10050.
|
||||
local tmpdir; tmpdir=$(mktemp -d "${STATE_DIR}/ghost.XXXXXX")
|
||||
local ghost_out ghost_npub
|
||||
ghost_out=$("$AMY_BIN" --data-dir "$tmpdir" init) || {
|
||||
record_result "$id" fail "ghost init failed"; rm -rf "$tmpdir"; return
|
||||
}
|
||||
ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub')
|
||||
info "ghost: $ghost_npub (no relays advertised)"
|
||||
|
||||
# A sends without --allow-fallback; amy should refuse.
|
||||
local raw rc
|
||||
raw=$("$AMY_BIN" --data-dir "$A_DIR" dm send "$ghost_npub" "should be rejected" 2>&1)
|
||||
rc=$?
|
||||
rm -rf "$tmpdir"
|
||||
if [[ "$rc" -ne 0 ]] && printf '%s' "$raw" | grep -q '"error":"no_dm_relays"'; then
|
||||
info "amy refused with no_dm_relays as expected (exit $rc)"
|
||||
record_result "$id" pass
|
||||
else
|
||||
fail_msg "expected no_dm_relays error; got rc=$rc raw=$raw"
|
||||
record_result "$id" fail "send to inboxless recipient did not refuse"
|
||||
fi
|
||||
}
|
||||
|
||||
test_04_dm_send_allow_fallback() {
|
||||
banner "DM-04 — --allow-fallback opts into NIP-65 read / bootstrap chain"
|
||||
local id="dm-04 allow-fallback"
|
||||
|
||||
# Same ghost pattern as dm-03. With --allow-fallback, resolution should
|
||||
# fall through kind:10050 → NIP-65 read → bootstrap. Our bootstrap set
|
||||
# always includes the loopback relay via the shared RelayConfig, so the
|
||||
# publish should succeed even though the ghost has no 10050.
|
||||
local tmpdir; tmpdir=$(mktemp -d "${STATE_DIR}/ghost.XXXXXX")
|
||||
local ghost_out ghost_npub
|
||||
ghost_out=$("$AMY_BIN" --data-dir "$tmpdir" init) || {
|
||||
record_result "$id" fail "ghost init failed"; rm -rf "$tmpdir"; return
|
||||
}
|
||||
ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub')
|
||||
rm -rf "$tmpdir"
|
||||
|
||||
local out source
|
||||
out=$(amy_json_a dm send "$ghost_npub" "hi via fallback" --allow-fallback) || {
|
||||
record_result "$id" fail "send with --allow-fallback failed"; return
|
||||
}
|
||||
# The ghost's own wrap will land in the recipients list; its source
|
||||
# should be bootstrap (no 10050, no 10002) — which confirms the
|
||||
# fallback actually fired.
|
||||
source=$(printf '%s' "$out" \
|
||||
| jq -r --arg pk "${ghost_npub}" \
|
||||
'.recipients[] | select(.pubkey != $pk) | .relay_source' \
|
||||
| head -1)
|
||||
info "relay_source for ghost wrap: $source"
|
||||
if printf '%s' "$out" | jq -e '[.recipients[] | .relay_source] | index("bootstrap")' >/dev/null \
|
||||
|| printf '%s' "$out" | jq -e '[.recipients[] | .relay_source] | index("nip65_read")' >/dev/null; then
|
||||
record_result "$id" pass
|
||||
else
|
||||
record_result "$id" fail "no recipient reported a fallback relay_source"
|
||||
fi
|
||||
}
|
||||
|
||||
test_05_dm_file_reference_round_trip() {
|
||||
banner "DM-05 — file message reference mode (kind:15) round-trip"
|
||||
local id="dm-05 file reference round-trip"
|
||||
|
||||
# 64 hex chars = 32-byte AES-GCM key; 32 hex chars = 16-byte nonce.
|
||||
local key="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
local nonce="fedcba9876543210fedcba9876543210"
|
||||
local url="https://example.test/blob/test-dm-05.bin"
|
||||
local mime="application/octet-stream"
|
||||
|
||||
amy_json_a dm send-file "$D_NPUB" "$url" \
|
||||
--key "$key" --nonce "$nonce" --mime-type "$mime" --size 1024 >/dev/null || {
|
||||
record_result "$id" fail "A send-file failed"; return
|
||||
}
|
||||
info "A published kind:15 for $url"
|
||||
|
||||
# D awaits — `dm await --match` searches URL for kind:15.
|
||||
if ! amy_json_d dm await --peer "$A_NPUB" --match "$url" --timeout 30 >/dev/null; then
|
||||
record_result "$id" fail "D never received the file message"; return
|
||||
fi
|
||||
|
||||
# Verify the recovered JSON carries all crypto material.
|
||||
local raw
|
||||
raw=$(amy_json_d dm list --peer "$A_NPUB" --timeout 5) || {
|
||||
record_result "$id" fail "D dm list failed"; return
|
||||
}
|
||||
local fields
|
||||
fields=$(printf '%s' "$raw" | jq -r \
|
||||
'[.messages[] | select(.type == "file" and .url == "'"$url"'")][0]')
|
||||
if [[ -z "$fields" || "$fields" == "null" ]]; then
|
||||
record_result "$id" fail "D did not decode a kind:15 matching the URL"; return
|
||||
fi
|
||||
|
||||
local got_key got_nonce got_mime
|
||||
got_key=$(printf '%s' "$fields" | jq -r '.decryption_key')
|
||||
got_nonce=$(printf '%s' "$fields" | jq -r '.decryption_nonce')
|
||||
got_mime=$(printf '%s' "$fields" | jq -r '.mime_type')
|
||||
info "D decoded key=${got_key:0:16}… nonce=${got_nonce:0:16}… mime=$got_mime"
|
||||
assert_eq "$got_key" "$key" "$id" "decryption_key mismatch" || return
|
||||
assert_eq "$got_nonce" "$nonce" "$id" "decryption_nonce mismatch" || return
|
||||
assert_eq "$got_mime" "$mime" "$id" "mime_type mismatch" || return
|
||||
|
||||
record_result "$id" pass
|
||||
}
|
||||
|
||||
test_06_dm_list_cursor_advance() {
|
||||
banner "DM-06 — no-flag dm list advances the giftWrapSince cursor"
|
||||
local id="dm-06 cursor advance"
|
||||
|
||||
# First no-flag list: drains everything and advances the cursor to now.
|
||||
local first_out first_count
|
||||
first_out=$(amy_json_d dm list --timeout 5) || {
|
||||
record_result "$id" fail "first dm list failed"; return
|
||||
}
|
||||
first_count=$(printf '%s' "$first_out" | jq -r '.messages | length')
|
||||
info "first dm list returned $first_count message(s)"
|
||||
|
||||
# Second no-flag list: nothing new should land.
|
||||
local second_out second_count
|
||||
second_out=$(amy_json_d dm list --timeout 5) || {
|
||||
record_result "$id" fail "second dm list failed"; return
|
||||
}
|
||||
second_count=$(printf '%s' "$second_out" | jq -r '.messages | length')
|
||||
info "second dm list returned $second_count message(s)"
|
||||
|
||||
if [[ "$first_count" -ge 1 && "$second_count" -eq 0 ]]; then
|
||||
record_result "$id" pass
|
||||
else
|
||||
record_result "$id" fail "expected first>=1, second==0; got first=$first_count second=$second_count"
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user