feat(cli): drop --data-dir; tests isolate via $HOME override

There is no installed base to protect, so the self-contained
`--data-dir P` escape hatch goes away entirely. amy now has exactly
one layout — the production multi-account one — and tests exercise
that same code path. Drops one global flag, one DataDir construction
mode, and the "tests use a different code path than users" footgun.

Layout (unchanged from the previous commits — just the only mode now):

    ~/.amy/
    ├── current                      # `amy use NAME` marker
    ├── shared/
    │   └── events-store/            # FsEventStore, one per machine
    ├── alice/
    │   ├── identity.json
    │   ├── state.json
    │   ├── aliases.json             # {"alice": "<own npub>"} after init
    │   └── marmot/
    └── bob/ ...

`DataDir.DEFAULT_ROOT` reads `$HOME` directly (falling back to
`user.home`) because JDK 21 resolves `user.home` via getpwuid and
ignores `$HOME` — which would have broken the standard CLI test
isolation pattern of `HOME=/tmp/foo amy …` (the same convention
git, gpg, npm follow).

Test sweep:

- `cli/tests/headless/helpers.sh`, `cli/tests/dm/setup.sh`,
  `cli/tests/cache/cache-headless.sh` wrappers all switch to
  `HOME=$STATE_DIR amy --name X …`.
- `ensure_identity_for` drops its `dir` parameter; the function and
  every harness call site go through `--name` only.
- `A_DIR`/`B_DIR`/`D_DIR` get repointed at `$STATE_DIR/.amy/X`. The
  one consumer (`cache-headless.sh`'s T6 `relays.json` check) still
  works since it's just a path probe.
- `cli/tests/dm/tests-dm.sh` ghost identities get their own
  short-lived `$HOME` so they don't pollute the main test root.

Cache-test T4 inverts: pre-shared-store, "B has not seen A → first
profile show is a relay miss, second is a cache hit" tested
per-account caching. With one shared events-store, B's first lookup
of A is already a cache hit because A wrote kind:0 there during
bootstrap. T4 now asserts that — drops the stale "second lookup hits
cache" half. T7 (no-identity maintenance verbs) gets a fresh fake
`$HOME` plus a throwaway `--name` so the empty store has no inherited
events.

Docs (README + DEVELOPMENT) rewritten to match — quick-start now uses
`amy --name alice create`, the on-disk-layout section shows the new
tree, and the global-flags table replaces `--data-dir` with `--name`
plus the new `amy use` verb.
This commit is contained in:
Claude
2026-04-25 15:07:31 +00:00
parent 46bdd59ead
commit 99586fbe7e
12 changed files with 240 additions and 200 deletions
+48 -45
View File
@@ -28,8 +28,10 @@ REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../.." && pwd)"
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
STATE_DIR="$SCRIPT_DIR/state-cache-headless"
LOG_DIR="$STATE_DIR/logs"
A_DIR="$STATE_DIR/A"
B_DIR="$STATE_DIR/B"
# Per-account dirs under the same fake $HOME=$STATE_DIR — accounts A
# and B share $STATE_DIR/.amy/shared/events-store/ (production layout).
A_DIR="$STATE_DIR/.amy/A"
B_DIR="$STATE_DIR/.amy/B"
RUN_TS="$(date +%Y%m%d-%H%M%S)"
LOG_FILE="$LOG_DIR/run-$RUN_TS.log"
@@ -64,7 +66,7 @@ while [[ $# -gt 0 ]]; do
shift
done
mkdir -p "$STATE_DIR" "$LOG_DIR" "$A_DIR" "$B_DIR"
mkdir -p "$STATE_DIR" "$LOG_DIR"
: >"$LOG_FILE"
: >"$RESULTS_FILE"
@@ -81,7 +83,7 @@ source "$TESTS_DIR/headless/helpers.sh"
# shellcheck source=../dm/setup.sh
source "$TESTS_DIR/dm/setup.sh"
amy_b() { "$AMY_BIN" --data-dir "$B_DIR" --json "$@"; }
amy_b() { HOME="$STATE_DIR" "$AMY_BIN" --name B --secret-backend plaintext --json "$@"; }
# 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.
@@ -111,23 +113,24 @@ banner "Amethyst event-store cache headless ($RUN_TS)"
preflight_dm
start_local_relay
# Identity A: full bootstrap so the store has kind:0 / 3 / 10002 / …
ensure_identity_for A "$A_DIR"
# Identity A: full bootstrap so the shared store has kind:0 / 3 / 10002 / …
ensure_identity_for A
banner "Bootstrapping A's account (publishes kind:0 + bootstrap events)"
"$AMY_BIN" --data-dir "$A_DIR" relay add "$RELAY_URL" --type all >>"$LOG_FILE" 2>&1
# `amy create` here would mint a *second* identity; A_DIR already has one
# from `init`. Build the bootstrap events ourselves by publishing a
# minimal kind:0 + the relay lists, all of which land in A's local store
# via verifyAndStore.
"$AMY_BIN" --data-dir "$A_DIR" relay publish-lists >>"$LOG_FILE" 2>&1
amy_a relay add "$RELAY_URL" --type all >>"$LOG_FILE" 2>&1
# `amy create` here would mint a *second* identity; A already exists from
# `init`. Build the bootstrap events ourselves by publishing a minimal
# kind:0 + the relay lists, all of which land in the shared store via
# verifyAndStore.
amy_a relay publish-lists >>"$LOG_FILE" 2>&1
amy_a profile edit --name "AAA" --about "cache test subject" \
>>"$LOG_FILE" 2>&1 \
|| fail_msg "amy_a profile edit failed"
# Identity B: separate data-dir, separate cache, also pointed at the relay.
ensure_identity_for B "$B_DIR"
"$AMY_BIN" --data-dir "$B_DIR" relay add "$RELAY_URL" --type all >>"$LOG_FILE" 2>&1
"$AMY_BIN" --data-dir "$B_DIR" relay publish-lists >>"$LOG_FILE" 2>&1
# Identity B: same fake $HOME, separate per-account dir, but A and B
# both read/write to $STATE_DIR/.amy/shared/events-store/.
ensure_identity_for B
amy_b relay add "$RELAY_URL" --type all >>"$LOG_FILE" 2>&1
amy_b relay publish-lists >>"$LOG_FILE" 2>&1
# ----------------------------------------------------------------------
# 1. amy store stat reports a non-empty store after bootstrap.
@@ -191,24 +194,20 @@ else
fi
# ----------------------------------------------------------------------
# 4. B has not seen A → first profile show is a relay miss; second is a hit.
# 4. Shared events-store: B looks up A's profile and gets a cache hit
# on the first try, because A's kind:0 already landed in the shared
# store during A's bootstrap. (Pre-shared-store behaviour was "first
# lookup is a relay miss, second is a cache hit"; that test only
# made sense when each account had its own private cache.)
# ----------------------------------------------------------------------
banner "T4 — B sees A first via relay, then via cache"
T4A=$(amy_b_json profile show "$A_NPUB")
T4A_SRC=$(printf '%s' "$T4A" | jq -r '.source')
T4A_NAME=$(printf '%s' "$T4A" | jq -r '.metadata.name // ""')
assert_eq "$T4A_SRC" "relays" T4a.source "first lookup of a stranger comes from relays" \
&& record_result T4a.source pass "first profile lookup of stranger hit relays"
assert_eq "$T4A_NAME" "AAA" T4a.name "B should resolve A's name on first fetch" \
&& record_result T4a.name pass "B resolved A's metadata"
T4B=$(amy_b_json profile show "$A_NPUB")
T4B_SRC=$(printf '%s' "$T4B" | jq -r '.source')
T4B_NAME=$(printf '%s' "$T4B" | jq -r '.metadata.name // ""')
assert_eq "$T4B_SRC" "cache" T4b.source "second lookup of the same stranger must serve from cache" \
&& record_result T4b.source pass "second lookup served from B's cache"
assert_eq "$T4B_NAME" "AAA" T4b.name "cached metadata must match" \
&& record_result T4b.name pass "cached metadata identical to fresh"
banner "T4 — B's profile lookup of A is a shared-store cache hit"
T4=$(amy_b_json profile show "$A_NPUB")
T4_SRC=$(printf '%s' "$T4" | jq -r '.source')
T4_NAME=$(printf '%s' "$T4" | jq -r '.metadata.name // ""')
assert_eq "$T4_SRC" "cache" T4.source "shared store should hand B a cached A profile immediately" \
&& record_result T4.source pass "B saw A from the shared cache"
assert_eq "$T4_NAME" "AAA" T4.name "A's profile metadata must be readable from B's invocation" \
&& record_result T4.name pass "shared-cache metadata round-trips across accounts"
# ----------------------------------------------------------------------
# 5. relay list reads URLs back from the local kind:10002 / 10050 / 10051.
@@ -245,20 +244,24 @@ else
fi
# ----------------------------------------------------------------------
# 7. Maintenance verbs work without an identity (they only need the store).
# 7. Maintenance verbs work even when the selected account has no
# identity yet — they only construct the FsEventStore, never
# `Context.open`. We use a fresh fake $HOME so the empty store has
# no inherited events.
# ----------------------------------------------------------------------
banner "T7 — store maintenance verbs (sweep/scrub/compact) without identity"
TMP_NO_ID=$(mktemp -d)
T7_STAT=$(${AMY_BIN} --data-dir "$TMP_NO_ID" --json store stat)
T7_SWEEP=$(${AMY_BIN} --data-dir "$TMP_NO_ID" --json store sweep-expired)
T7_SCRUB=$(${AMY_BIN} --data-dir "$TMP_NO_ID" --json store scrub)
T7_COMPACT=$(${AMY_BIN} --data-dir "$TMP_NO_ID" --json store compact)
banner "T7 — store maintenance verbs (sweep/scrub/compact) on an empty store"
TMP_HOME=$(mktemp -d)
T7_AMY=(${AMY_BIN} --secret-backend plaintext --name throwaway --json store)
T7_STAT=$(HOME="$TMP_HOME" "${T7_AMY[@]}" stat)
T7_SWEEP=$(HOME="$TMP_HOME" "${T7_AMY[@]}" sweep-expired)
T7_SCRUB=$(HOME="$TMP_HOME" "${T7_AMY[@]}" scrub)
T7_COMPACT=$(HOME="$TMP_HOME" "${T7_AMY[@]}" compact)
assert_eq "$(printf '%s' "$T7_STAT" | jq -r '.events')" "0" T7.stat.events "" \
&& record_result T7.stat pass "stat works without identity"
&& record_result T7.stat pass "stat works on empty store"
assert_eq "$(printf '%s' "$T7_SWEEP" | jq -r '.swept')" "0" T7.sweep.swept "" \
&& record_result T7.sweep pass "sweep-expired works without identity"
&& record_result T7.sweep pass "sweep-expired works on empty store"
assert_eq "$(printf '%s' "$T7_SCRUB" | jq -r '.ok')" "true" T7.scrub.ok "" \
&& record_result T7.scrub pass "scrub works without identity"
&& record_result T7.scrub pass "scrub works on empty store"
assert_eq "$(printf '%s' "$T7_COMPACT" | jq -r '.ok')" "true" T7.compact.ok "" \
&& record_result T7.compact pass "compact works without identity"
rm -rf "$TMP_NO_ID"
&& record_result T7.compact pass "compact works on empty store"
rm -rf "$TMP_HOME"