feat(cli): amy store stat/sweep-expired/scrub/compact + e2e cache test

Three changes that go together:

1. Reconcile cli/plans/2026-04-24-file-event-store-{overview,nips}.md
   with shipped reality: code lives in quartz/jvmMain/, not commons/;
   data dir is <data-dir>/events-store/, not <root>/events/.

2. New StoreCommands wired as `amy store …`:
     - stat            → events count, kind histogram, disk bytes,
                         oldest/newest createdAt. Pure read, no Context
                         (skips identity check).
     - sweep-expired   → wraps store.deleteExpiredEvents(); reports
                         {swept, remaining}.
     - scrub           → wraps store.scrub() to rebuild idx/ from
                         canonicals.
     - compact         → wraps store.compact() to drop dangling idx/.
   All four open the FsEventStore directly (no Context, no identity
   needed) — they're store-only operations.

3. New e2e harness at cli/tests/cache/cache-headless.sh that boots a
   local nostr-rs-relay, two amy identities (A + B), and asserts:
     T1 — store stat reports non-empty store after publish-lists +
          profile edit, with kind:0 and kind:10002 present.
     T2 — A's `profile show` is `source: "cache"` by default.
     T3 — `--refresh` forces `source: "relays"`.
     T4 — B's first `profile show <A_NPUB>` is a relay miss; second is
          a cache hit (proves drain populates the local store and
          subsequent reads serve from disk).
     T5 — `relay list` reads URLs back from the local kind:10002 /
          10050 / 10051 events.
     T6 — relays.json no longer exists in either data-dir.
     T7 — store stat / sweep-expired / scrub / compact all run
          without an identity present.

Same pattern as cli/tests/dm/dm-interop-headless.sh — reuses the
nostr-rs-relay infrastructure from cli/tests/marmot/setup.sh.
This commit is contained in:
Claude
2026-04-25 04:10:10 +00:00
parent 99be0b2d16
commit 7bbfb52d87
6 changed files with 495 additions and 13 deletions
@@ -399,10 +399,10 @@ reads. O(size of idx trees).
### Location
`commons/src/jvmTest/kotlin/com/vitorpamplona/commons/store/fs/`
Uses JUnit4 (matches rest of the project). Temp-dir fixture creates
a fresh store per test.
`quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/`
(sibling to the SQLite reference tests). Uses kotlin.test (matches
the rest of the quartz `jvmTest` source set). Temp-dir fixture
creates a fresh store per test.
### Test categories
@@ -51,29 +51,36 @@ files may be created or deleted by the user between runs.
`events/`. `amy store scrub` does this.
4. **`events/` is the source of truth.** If a file is there, it is
part of the store. If it is gone, it is gone — tombstones aside.
5. **No business logic.** This is a `commons/` module that the CLI
imports. No Nostr-protocol decisions live in `cli/`.
5. **No business logic.** Lives next to the SQLite reference store
in `quartz/`, sibling pattern. No Nostr-protocol decisions live
in `cli/`.
---
## Module placement
Source: `commons/src/jvmMain/kotlin/com/vitorpamplona/commons/store/fs/`
Source: `quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/`
- Sibling to the existing
`quartz/src/commonMain/kotlin/.../nip01Core/store/sqlite/` reference
implementation. The plan originally proposed `commons/` but the
shipped placement is `quartz/jvmMain/`, where every other event-
store concern already lives — and quartz already has a `jvmTest`
source set so we get JVM-specific tests for free.
- JVM-only (uses `java.nio.file`, `FileChannel.lock`, `Files.createLink`).
- Consumed by `cli/`. Android keeps `SQLiteEventStore`. Desktop can
opt in later if useful.
- Tests under `commons/src/jvmTest/.../store/fs/`.
Not in `quartz/` because `quartz/` is protocol-only (no storage).
Not in `cli/` because it contains reusable logic.
- Tests under `quartz/src/jvmTest/.../store/fs/`.
---
## Directory layout
Root: configurable, defaults to `$AMY_HOME/events/` (where `$AMY_HOME`
is `~/.amy` by default).
Root: configurable. The CLI uses `<data-dir>/events-store/` (where
`<data-dir>` is `--data-dir PATH`, `$AMETHYST_CLI_DATA`, or `./amy`).
The plan originally said `events/`; the actual `DataDir.eventsDir`
field is `events-store/` to leave the bare name `events/` available
for the canonical-events subdirectory inside the store.
```
<root>/