fix(cli): rename global --name to --account to free --name for subcommands

The marmot harness surfaced the bug on first run: amy stripped
`marmot group create --name "Interop-02"` thinking the global
account selector ran into the group's display-name flag. Result:
amy resolved the "Interop-02" account, found no identity.json, and
errored — no group ever got created.

Renames the global account-selector flag to `--account`. The per-
subcommand `--name` flags (`marmot group create --name "Demo"`,
`profile edit --name "Alice"`, `marmot await group --name X`) are
untouched — they're free of the global parser now that it doesn't
claim the same name.

Sweep:
- `Main.kt`: GlobalFlag.NAME → ACCOUNT, long "--account".
- `Config.kt`: DataDir.resolve param renamed nameFlag → accountFlag;
  every error message points the user at --account.
- `UseCommand.kt`: error hint says `amy --account NAME init`.
- All test wrappers + direct $AMY_BIN calls under cli/tests/ swap
  the global `--name X` for `--account X` (subcommand --name kept
  exactly where it appeared).
- README + DEVELOPMENT updated.
This commit is contained in:
Claude
2026-04-25 15:26:17 +00:00
parent 99586fbe7e
commit e17ef42e54
10 changed files with 54 additions and 54 deletions
+6 -6
View File
@@ -201,11 +201,11 @@ Amy-specific layer still needs its own coverage:
| Layer | Test approach | | Layer | Test approach |
|---|---| |---|---|
| Argument parsing (`Args`, flag forms, `--name=…` vs `--name …`) | Plain JVM unit tests in `cli/src/test/kotlin/`. | | Argument parsing (`Args`, flag forms, `--account=…` vs `--account …`) | Plain JVM unit tests in `cli/src/test/kotlin/`. |
| Error / exit-code contract (bad args → 2, await timeout → 124, runtime → 1) | Table-driven tests invoking `main(argv)` with captured stdout/stderr. | | Error / exit-code contract (bad args → 2, await timeout → 124, runtime → 1) | Table-driven tests invoking `main(argv)` with captured stdout/stderr. |
| JSON output shape (each command's keys and types under `--json`) | Snapshot tests: run a command with `--json` against a throwaway `$HOME` (`HOME=$(mktemp -d) amy --name X …`), assert the JSON matches a golden file. The default text render has no shape contract and shouldn't be snapshotted. | | JSON output shape (each command's keys and types under `--json`) | Snapshot tests: run a command with `--json` against a throwaway `$HOME` (`HOME=$(mktemp -d) amy --account X …`), assert the JSON matches a golden file. The default text render has no shape contract and shouldn't be snapshotted. |
| File layout on disk (`identity.json`, `events-store/…`, `marmot/groups/*.mls`, `marmot/keypackages.bundle`) | Structural assertions after a command sequence. | | File layout on disk (`identity.json`, `events-store/…`, `marmot/groups/*.mls`, `marmot/keypackages.bundle`) | Structural assertions after a command sequence. |
| Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`. Each harness spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple identities (`--name A`, `--name D`, etc.) sharing the same `~/.amy/shared/events-store/` and drives a scenario through them (+ `wn` for Marmot interop against whitenoise-rs). Today there are two suites: `cli/tests/marmot/` (MLS scenarios vs whitenoise-rs) and `cli/tests/dm/` (NIP-17 DM round-trips between two `amy` accounts). | | Round-trip between two accounts on a local relay | End-to-end shell harnesses under `cli/tests/`. Each harness spins up a local `nostr-rs-relay` and a fresh `$HOME=$STATE_DIR` so amy sees a virgin `~/.amy/`, then bootstraps multiple identities (`--account A`, `--account D`, etc.) sharing the same `~/.amy/shared/events-store/` and drives a scenario through them (+ `wn` for Marmot interop against whitenoise-rs). Today there are two suites: `cli/tests/marmot/` (MLS scenarios vs whitenoise-rs) and `cli/tests/dm/` (NIP-17 DM round-trips between two `amy` accounts). |
| Interop with other clients | Covered by `cli/tests/marmot/marmot-interop-headless.sh` (drives Amy against whitenoise-rs `wn`/`wnd`). Add new scenarios there or start a new sibling under `cli/tests/`. | | Interop with other clients | Covered by `cli/tests/marmot/marmot-interop-headless.sh` (drives Amy against whitenoise-rs `wn`/`wnd`). Add new scenarios there or start a new sibling under `cli/tests/`. |
**What not to test here:** event signing, filter assembly, MLS **What not to test here:** event signing, filter assembly, MLS
@@ -224,12 +224,12 @@ At the byte-banging level, a minimal round-trip looks like:
set -euo pipefail set -euo pipefail
export HOME=$(mktemp -d) # virgin ~/.amy/ for the duration of this script export HOME=$(mktemp -d) # virgin ~/.amy/ for the duration of this script
amy --name alice create amy --account alice create
amy --name bob create amy --account bob create
# ... the scenario under test ... # ... the scenario under test ...
amy --name bob marmot await message "$GID" --match "hello" --timeout 60 amy --account bob marmot await message "$GID" --match "hello" --timeout 60
``` ```
If an Amethyst scenario cannot be scripted through Amy yet, that's If an Amethyst scenario cannot be scripted through Amy yet, that's
+15 -15
View File
@@ -32,7 +32,7 @@ What every caller — user, script, agent, CI — can rely on:
- **`--json` is the machine contract. One line. One object.** Stable - **`--json` is the machine contract. One line. One object.** Stable
snake_case keys. Pipe it into `jq`, parse it from Python, hand it snake_case keys. Pipe it into `jq`, parse it from Python, hand it
to an agent. Pass `--json` anywhere before the subcommand: to an agent. Pass `--json` anywhere before the subcommand:
`amy --json whoami`, `amy --name alice --json marmot group list`. `amy --json whoami`, `amy --account alice --json marmot group list`.
- **stderr is for humans.** Progress, warnings, per-relay ACK traces. - **stderr is for humans.** Progress, warnings, per-relay ACK traces.
Safe to discard. Errors land here too: `error: <code>: <detail>` by Safe to discard. Errors land here too: `error: <code>: <detail>` by
default, or JSON `{"error":"…","detail":"…"}` under `--json`. default, or JSON `{"error":"…","detail":"…"}` under `--json`.
@@ -47,7 +47,7 @@ What every caller — user, script, agent, CI — can rely on:
observed Nostr event lands in `~/.amy/shared/events-store/` (one observed Nostr event lands in `~/.amy/shared/events-store/` (one
cache for every account on the machine). Delete to reset; copy to cache for every account on the machine). Delete to reset; copy to
move. Tests isolate by overriding `$HOME` for the amy subprocess move. Tests isolate by overriding `$HOME` for the amy subprocess
(`HOME=/tmp/run.123 amy --name alice …`) — same convention `git`, (`HOME=/tmp/run.123 amy --account alice …`) — same convention `git`,
`gpg`, and `npm` use. `gpg`, and `npm` use.
Only the `--json` shape and the exit codes are public API. The default Only the `--json` shape and the exit codes are public API. The default
@@ -153,19 +153,19 @@ The `installDist` tree under `cli/build/install/amy/` is self-contained
# 1. Create the alice account (~/.amy/alice/) with a full Amethyst-style # 1. Create the alice account (~/.amy/alice/) with a full Amethyst-style
# bootstrap: keypair, default NIP-65 / inbox / key-package relays, # bootstrap: keypair, default NIP-65 / inbox / key-package relays,
# nine bootstrap events. # nine bootstrap events.
amy --name alice create amy --account alice create
# 2. Publish a fresh MLS KeyPackage so others can invite you. # 2. Publish a fresh MLS KeyPackage so others can invite you.
amy --name alice marmot key-package publish amy --account alice marmot key-package publish
# 3. Create a group, invite someone, send a message. # 3. Create a group, invite someone, send a message.
amy --name alice marmot group create --name "Test Group" amy --account alice marmot group create --name "Test Group"
amy --name alice marmot group add <GID> npub1...bob amy --account alice marmot group add <GID> npub1...bob
amy --name alice marmot message send <GID> "hello" amy --account alice marmot message send <GID> "hello"
# 4. On the receiving side, in ~/.amy/bob/. # 4. On the receiving side, in ~/.amy/bob/.
amy --name bob marmot await group --name "Test Group" --timeout 60 amy --account bob marmot await group --name "Test Group" --timeout 60
amy --name bob marmot message list <GID> amy --account bob marmot message list <GID>
``` ```
With one account you can drop the flag (auto-pick); with several, pin With one account you can drop the flag (auto-pick); with several, pin
@@ -174,7 +174,7 @@ with `jq` by adding `--json` to switch stdout from human text to a
parseable single-line JSON object: parseable single-line JSON object:
```bash ```bash
GID=$(amy --name alice --json marmot group create --name "Test" | jq -r .group_id) GID=$(amy --account alice --json marmot group create --name "Test" | jq -r .group_id)
``` ```
For an interop-test script template, see For an interop-test script template, see
@@ -235,7 +235,7 @@ itself crashed".
### Global flags ### Global flags
- `--name ACCOUNT` — pick which account under `~/.amy/<account>/` to - `--account ACCOUNT` — pick which account under `~/.amy/<account>/` to
operate on. Required only when more than one account exists; with a operate on. Required only when more than one account exists; with a
single account amy auto-picks. Override with `amy use NAME` to single account amy auto-picks. Override with `amy use NAME` to
pin one as the default. ACCOUNT must match `[a-zA-Z0-9_-]{1,64}`. pin one as the default. ACCOUNT must match `[a-zA-Z0-9_-]{1,64}`.
@@ -292,7 +292,7 @@ events.
│ ├── addressable/… # one slot per (kind, pubkey, d-tag) for kind:30000-39999 │ ├── addressable/… # one slot per (kind, pubkey, d-tag) for kind:30000-39999
│ ├── idx/ # hardlink indexes (kind / author / owner / tag / fts / expires_at) │ ├── idx/ # hardlink indexes (kind / author / owner / tag / fts / expires_at)
│ └── tombstones/ # NIP-09 / NIP-62 enforcement │ └── tombstones/ # NIP-09 / NIP-62 enforcement
├── alice/ # one dir per account (e.g. created by `amy --name alice init`) ├── alice/ # one dir per account (e.g. created by `amy --account alice init`)
│ ├── identity.json # nsec/npub/hex — the account │ ├── identity.json # nsec/npub/hex — the account
│ ├── state.json # sync cursors (giftWrapSince, groupSince) │ ├── state.json # sync cursors (giftWrapSince, groupSince)
│ ├── aliases.json # local name → npub map (init writes a self-entry) │ ├── aliases.json # local name → npub map (init writes a self-entry)
@@ -321,10 +321,10 @@ those events to upstream relays. There is no `relays.json`.
## Troubleshooting ## Troubleshooting
- **`no identity`** — run `init`, `create`, or `login` first, or pass a - **`no identity`** — run `init`, `create`, or `login` first, or pass a
different `--name`. different `--account`.
- **`no account at ~/.amy`** — there's no account dir yet; create one - **`no account at ~/.amy`** — there's no account dir yet; create one
with `amy --name <name> init`. with `amy --account <name> init`.
- **`multiple accounts in ~/.amy (...)`** — disambiguate with `--name` - **`multiple accounts in ~/.amy (...)`** — disambiguate with `--account`
on the next call, or pin a default via `amy use NAME`. on the next call, or pin a default via `amy use NAME`.
- **`not_member`** — the group GID is unknown to this account. Run - **`not_member`** — the group GID is unknown to this account. Run
`marmot group list` to confirm, or `marmot await group --name …` to `marmot group list` to confirm, or `marmot await group --name …` to
@@ -239,7 +239,7 @@ class DataDir(
fun validateName(name: String): String { fun validateName(name: String): String {
require(NAME_REGEX.matches(name)) { require(NAME_REGEX.matches(name)) {
"--name must match [a-zA-Z0-9_-]{1,64} (got '$name')" "--account must match [a-zA-Z0-9_-]{1,64} (got '$name')"
} }
require(name !in RESERVED_NAMES) { require(name !in RESERVED_NAMES) {
"'$name' is reserved (cannot be used as an account name)" "'$name' is reserved (cannot be used as an account name)"
@@ -251,17 +251,17 @@ class DataDir(
* Build a [DataDir] from the parsed CLI flags. * Build a [DataDir] from the parsed CLI flags.
* *
* Resolution order: * Resolution order:
* 1. `--name X` if provided. * 1. `--account X` if provided.
* 2. `<root>/current` marker (set by `amy use X`). * 2. `<root>/current` marker (set by `amy use X`).
* 3. Sole subdirectory of `<root>` other than `shared/`. * 3. Sole subdirectory of `<root>` other than `shared/`.
* 4. Error — caller must disambiguate via `--name` or `amy use`. * 4. Error — caller must disambiguate via `--account` or `amy use`.
*/ */
fun resolve( fun resolve(
nameFlag: String?, accountFlag: String?,
secrets: SecretStore, secrets: SecretStore,
): DataDir { ): DataDir {
val rootBase = DEFAULT_ROOT val rootBase = DEFAULT_ROOT
val name = if (nameFlag != null) validateName(nameFlag) else pickAccount(rootBase) val name = if (accountFlag != null) validateName(accountFlag) else pickAccount(rootBase)
val accountRoot = File(rootBase, name).absoluteFile val accountRoot = File(rootBase, name).absoluteFile
val sharedEvents = File(rootBase, "$SHARED_DIR_NAME/events-store").absoluteFile val sharedEvents = File(rootBase, "$SHARED_DIR_NAME/events-store").absoluteFile
return DataDir( return DataDir(
@@ -284,11 +284,11 @@ class DataDir(
if (current.isFile) { if (current.isFile) {
val pinned = current.readText().trim() val pinned = current.readText().trim()
require(pinned.isNotEmpty()) { require(pinned.isNotEmpty()) {
"${current.absolutePath} is empty; rewrite with `amy use <name>` or pass --name" "${current.absolutePath} is empty; rewrite with `amy use <name>` or pass --account"
} }
require(File(rootBase, pinned).isDirectory) { require(File(rootBase, pinned).isDirectory) {
"${current.absolutePath} pins '$pinned' but ${File(rootBase, pinned).absolutePath} doesn't exist; " + "${current.absolutePath} pins '$pinned' but ${File(rootBase, pinned).absolutePath} doesn't exist; " +
"rewrite with `amy use <name>` or pass --name" "rewrite with `amy use <name>` or pass --account"
} }
return pinned return pinned
} }
@@ -296,7 +296,7 @@ class DataDir(
return when (accounts.size) { return when (accounts.size) {
0 -> { 0 -> {
throw IllegalArgumentException( throw IllegalArgumentException(
"no account at ${rootBase.absolutePath}; create one with `amy --name <name> init`", "no account at ${rootBase.absolutePath}; create one with `amy --account <name> init`",
) )
} }
@@ -307,7 +307,7 @@ class DataDir(
else -> { else -> {
throw IllegalArgumentException( throw IllegalArgumentException(
"multiple accounts in ${rootBase.absolutePath} (${accounts.joinToString(", ")}); " + "multiple accounts in ${rootBase.absolutePath} (${accounts.joinToString(", ")}); " +
"pick one with --name <name> or `amy use <name>`", "pick one with --account <name> or `amy use <name>`",
) )
} }
} }
@@ -82,7 +82,7 @@ private suspend fun dispatch(argv: Array<String>): Int {
// Pull global flags out of argv before subcommand parsing so subcommands see // Pull global flags out of argv before subcommand parsing so subcommands see
// only their own args. // only their own args.
val filteredArgs = mutableListOf<String>() val filteredArgs = mutableListOf<String>()
var nameFlag: String? = null var accountFlag: String? = null
var secretBackendFlag: String? = null var secretBackendFlag: String? = null
var passphraseFileFlag: String? = null var passphraseFileFlag: String? = null
var i = 0 var i = 0
@@ -90,7 +90,7 @@ private suspend fun dispatch(argv: Array<String>): Int {
val a = argv[i] val a = argv[i]
val (matched, consumed) = extractGlobalFlag(a, argv, i) val (matched, consumed) = extractGlobalFlag(a, argv, i)
when (matched) { when (matched) {
GlobalFlag.NAME -> nameFlag = consumed.value GlobalFlag.ACCOUNT -> accountFlag = consumed.value
GlobalFlag.SECRET_BACKEND -> secretBackendFlag = consumed.value GlobalFlag.SECRET_BACKEND -> secretBackendFlag = consumed.value
GlobalFlag.PASSPHRASE_FILE -> passphraseFileFlag = consumed.value GlobalFlag.PASSPHRASE_FILE -> passphraseFileFlag = consumed.value
GlobalFlag.JSON -> Output.mode = Output.Mode.JSON GlobalFlag.JSON -> Output.mode = Output.Mode.JSON
@@ -116,7 +116,7 @@ private suspend fun dispatch(argv: Array<String>): Int {
} }
val secrets = SecretStore.from(backendFlag = secretBackendFlag, passphraseFile = passphraseFileFlag) val secrets = SecretStore.from(backendFlag = secretBackendFlag, passphraseFile = passphraseFileFlag)
val dataDir = DataDir.resolve(nameFlag = nameFlag, secrets = secrets) val dataDir = DataDir.resolve(accountFlag = accountFlag, secrets = secrets)
return when (head) { return when (head) {
"init" -> { "init" -> {
@@ -210,7 +210,7 @@ private enum class GlobalFlag(
val long: String, val long: String,
val takesValue: Boolean = true, val takesValue: Boolean = true,
) { ) {
NAME("--name"), ACCOUNT("--account"),
SECRET_BACKEND("--secret-backend"), SECRET_BACKEND("--secret-backend"),
PASSPHRASE_FILE("--passphrase-file"), PASSPHRASE_FILE("--passphrase-file"),
JSON("--json", takesValue = false), JSON("--json", takesValue = false),
@@ -253,7 +253,7 @@ private fun printUsage() {
|amy — Amethyst command-line interface |amy — Amethyst command-line interface
| |
|Usage: |Usage:
| amy [--name ACCOUNT] | amy [--account ACCOUNT]
| [--secret-backend auto|keychain|ncryptsec|plaintext] | [--secret-backend auto|keychain|ncryptsec|plaintext]
| [--passphrase-file PATH] | [--passphrase-file PATH]
| [--json] | [--json]
@@ -267,13 +267,13 @@ private fun printUsage() {
| [a-zA-Z0-9_-]{1,64} (no spaces, no slashes). | [a-zA-Z0-9_-]{1,64} (no spaces, no slashes).
| |
| Resolution order: | Resolution order:
| 1. --name X if given. | 1. --account X if given.
| 2. ~/.amy/current marker (set by `amy use X`). | 2. ~/.amy/current marker (set by `amy use X`).
| 3. Sole subdirectory of ~/.amy/ other than shared/. | 3. Sole subdirectory of ~/.amy/ other than shared/.
| 4. Error — disambiguate with --name or `amy use`. | 4. Error — disambiguate with --account or `amy use`.
| |
| Test harnesses isolate by overriding ${'$'}HOME for the amy | Test harnesses isolate by overriding ${'$'}HOME for the amy
| subprocess (`HOME=/tmp/run.123 amy --name alice ...`). | subprocess (`HOME=/tmp/run.123 amy --account alice ...`).
| |
| use NAME pin NAME as the active account | use NAME pin NAME as the active account
| use --clear remove the pin | use --clear remove the pin
@@ -296,7 +296,7 @@ private fun printUsage() {
| |
|Identity: |Identity:
| init [--nsec NSEC] create or import a bare identity (no defaults published) | init [--nsec NSEC] create or import a bare identity (no defaults published)
| create [--name NAME] provision a full Amethyst-style account + publish bootstrap events | create [--name NAME] provision a full Amethyst-style account + publish bootstrap events
| login KEY [--password X] import (nsec|ncryptsec|mnemonic|npub|nprofile|hex|nip05) | login KEY [--password X] import (nsec|ncryptsec|mnemonic|npub|nprofile|hex|nip05)
| whoami print current identity | whoami print current identity
| |
@@ -74,7 +74,7 @@ object UseCommand {
if (!accountDir.isDirectory) { if (!accountDir.isDirectory) {
return Output.error( return Output.error(
"no_account", "no_account",
"${accountDir.absolutePath} doesn't exist; create it with `amy --name $name init`", "${accountDir.absolutePath} doesn't exist; create it with `amy --account $name init`",
) )
} }
SecureFileIO.secureMkdirs(rootBase) SecureFileIO.secureMkdirs(rootBase)
+2 -2
View File
@@ -83,7 +83,7 @@ source "$TESTS_DIR/headless/helpers.sh"
# shellcheck source=../dm/setup.sh # shellcheck source=../dm/setup.sh
source "$TESTS_DIR/dm/setup.sh" source "$TESTS_DIR/dm/setup.sh"
amy_b() { HOME="$STATE_DIR" "$AMY_BIN" --name B --secret-backend plaintext --json "$@"; } amy_b() { HOME="$STATE_DIR" "$AMY_BIN" --account B --secret-backend plaintext --json "$@"; }
# Helper: B-side amy_json. The dm headless's helpers.sh hardcodes A_DIR # Helper: B-side amy_json. The dm headless's helpers.sh hardcodes A_DIR
# in amy_a; we need a parallel for B without re-sourcing. # in amy_a; we need a parallel for B without re-sourcing.
@@ -251,7 +251,7 @@ fi
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
banner "T7 — store maintenance verbs (sweep/scrub/compact) on an empty store" banner "T7 — store maintenance verbs (sweep/scrub/compact) on an empty store"
TMP_HOME=$(mktemp -d) TMP_HOME=$(mktemp -d)
T7_AMY=(${AMY_BIN} --secret-backend plaintext --name throwaway --json store) T7_AMY=(${AMY_BIN} --secret-backend plaintext --account throwaway --json store)
T7_STAT=$(HOME="$TMP_HOME" "${T7_AMY[@]}" stat) T7_STAT=$(HOME="$TMP_HOME" "${T7_AMY[@]}" stat)
T7_SWEEP=$(HOME="$TMP_HOME" "${T7_AMY[@]}" sweep-expired) T7_SWEEP=$(HOME="$TMP_HOME" "${T7_AMY[@]}" sweep-expired)
T7_SCRUB=$(HOME="$TMP_HOME" "${T7_AMY[@]}" scrub) T7_SCRUB=$(HOME="$TMP_HOME" "${T7_AMY[@]}" scrub)
+4 -4
View File
@@ -78,15 +78,15 @@ preflight_dm() {
# `--secret-backend=plaintext` keeps these throwaway interop runs headless — # `--secret-backend=plaintext` keeps these throwaway interop runs headless —
# the default `auto` would try the OS keychain (not available in CI) and then # the default `auto` would try the OS keychain (not available in CI) and then
# ask for a NIP-49 passphrase. Plaintext still writes 0600-owner-only. # ask for a NIP-49 passphrase. Plaintext still writes 0600-owner-only.
amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --name A --secret-backend plaintext --json "$@"; } amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json "$@"; }
amy_d() { HOME="$STATE_DIR" "$AMY_BIN" --name D --secret-backend plaintext --json "$@"; } amy_d() { HOME="$STATE_DIR" "$AMY_BIN" --account D --secret-backend plaintext --json "$@"; }
# --- identity bootstrap ------------------------------------------------------ # --- identity bootstrap ------------------------------------------------------
ensure_identity_for() { ensure_identity_for() {
local who="$1" local who="$1"
step "initialising Identity $who (amy at \$HOME=$STATE_DIR --name $who)" step "initialising Identity $who (amy at \$HOME=$STATE_DIR --account $who)"
local out local out
out=$(HOME="$STATE_DIR" "$AMY_BIN" --name "$who" --secret-backend plaintext --json init) || { out=$(HOME="$STATE_DIR" "$AMY_BIN" --account "$who" --secret-backend plaintext --json init) || {
fail_msg "amy init failed for $who: $out"; exit 1 fail_msg "amy init failed for $who: $out"; exit 1
} }
local npub hex local npub hex
+6 -6
View File
@@ -13,14 +13,14 @@
# dm-06 cursor advance (subsequent no-flag `dm list` is empty) # dm-06 cursor advance (subsequent no-flag `dm list` is empty)
# Wrap amy_json around either account so the per-test code stays tight. # Wrap amy_json around either account so the per-test code stays tight.
# Both share $HOME=$STATE_DIR (set by the harness); --name picks the # Both share $HOME=$STATE_DIR (set by the harness); --account picks the
# account inside it. `--json` opts into amy's machine-readable contract; # account inside it. `--json` opts into amy's machine-readable contract;
# assertions below parse with jq. # assertions below parse with jq.
amy_json_for() { amy_json_for() {
local account="$1"; shift local account="$1"; shift
local out local out
if ! out=$(HOME="$STATE_DIR" "$AMY_BIN" --name "$account" --secret-backend plaintext --json "$@" 2>>"$LOG_FILE"); then if ! out=$(HOME="$STATE_DIR" "$AMY_BIN" --account "$account" --secret-backend plaintext --json "$@" 2>>"$LOG_FILE"); then
fail_msg "amy --name $account $*: exit $? (see $LOG_FILE)" fail_msg "amy --account $account $*: exit $? (see $LOG_FILE)"
printf '%s\n' "$out" >>"$LOG_FILE" printf '%s\n' "$out" >>"$LOG_FILE"
return 1 return 1
fi fi
@@ -96,7 +96,7 @@ test_03_dm_send_rejects_no_inbox() {
# test's main STATE_DIR with a third account. # test's main STATE_DIR with a third account.
local ghost_home; ghost_home=$(mktemp -d "${STATE_DIR}/ghost-home.XXXXXX") local ghost_home; ghost_home=$(mktemp -d "${STATE_DIR}/ghost-home.XXXXXX")
local ghost_out ghost_npub local ghost_out ghost_npub
ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --name ghost --secret-backend plaintext --json init) || { ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --account ghost --secret-backend plaintext --json init) || {
record_result "$id" fail "ghost init failed"; rm -rf "$ghost_home"; return record_result "$id" fail "ghost init failed"; rm -rf "$ghost_home"; return
} }
ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub') ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub')
@@ -104,7 +104,7 @@ test_03_dm_send_rejects_no_inbox() {
# A sends without --allow-fallback; amy should refuse. # A sends without --allow-fallback; amy should refuse.
local raw rc local raw rc
raw=$(HOME="$STATE_DIR" "$AMY_BIN" --name A --secret-backend plaintext --json dm send "$ghost_npub" "should be rejected" 2>&1) raw=$(HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json dm send "$ghost_npub" "should be rejected" 2>&1)
rc=$? rc=$?
rm -rf "$ghost_home" rm -rf "$ghost_home"
if [[ "$rc" -ne 0 ]] && printf '%s' "$raw" | grep -q '"error":"no_dm_relays"'; then if [[ "$rc" -ne 0 ]] && printf '%s' "$raw" | grep -q '"error":"no_dm_relays"'; then
@@ -126,7 +126,7 @@ test_04_dm_send_allow_fallback() {
# publish should succeed even though the ghost has no 10050. # publish should succeed even though the ghost has no 10050.
local ghost_home; ghost_home=$(mktemp -d "${STATE_DIR}/ghost-home.XXXXXX") local ghost_home; ghost_home=$(mktemp -d "${STATE_DIR}/ghost-home.XXXXXX")
local ghost_out ghost_npub local ghost_out ghost_npub
ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --name ghost --secret-backend plaintext --json init) || { ghost_out=$(HOME="$ghost_home" "$AMY_BIN" --account ghost --secret-backend plaintext --json init) || {
record_result "$id" fail "ghost init failed"; rm -rf "$ghost_home"; return record_result "$id" fail "ghost init failed"; rm -rf "$ghost_home"; return
} }
ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub') ghost_npub=$(printf '%s' "$ghost_out" | jq -r '.npub')
+1 -1
View File
@@ -14,7 +14,7 @@
# #
# `--json` opts into amy's machine-readable output (the harness parses it # `--json` opts into amy's machine-readable output (the harness parses it
# with jq); the default human-text output is for terminal use. # with jq); the default human-text output is for terminal use.
amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --name alice --secret-backend plaintext --json "$@"; } amy_a() { HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json "$@"; }
# Run amy, log stderr, surface JSON on stdout, remember last result. # Run amy, log stderr, surface JSON on stdout, remember last result.
amy_json() { amy_json() {
+1 -1
View File
@@ -271,7 +271,7 @@ test_14_wn_removes_a() {
local deadline=$(( $(date +%s) + 120 )) removed=0 local deadline=$(( $(date +%s) + 120 )) removed=0
while [[ $(date +%s) -lt $deadline ]]; do while [[ $(date +%s) -lt $deadline ]]; do
local show rc local show rc
show=$(HOME="$STATE_DIR" "$AMY_BIN" --name A --secret-backend plaintext --json \ show=$(HOME="$STATE_DIR" "$AMY_BIN" --account A --secret-backend plaintext --json \
marmot group show "$a_gid" 2>&1) marmot group show "$a_gid" 2>&1)
rc=$? rc=$?
printf '%s\n' "$show" >>"$LOG_FILE" printf '%s\n' "$show" >>"$LOG_FILE"