Skip to content

test(integration): #156 — swap (Docker e2e) + crypto/util + init-nametag + group + market#14

Merged
vrogojin merged 6 commits into
integration/all-fixesfrom
test/cli-swap-coverage
May 15, 2026
Merged

test(integration): #156 — swap (Docker e2e) + crypto/util + init-nametag + group + market#14
vrogojin merged 6 commits into
integration/all-fixesfrom
test/cli-swap-coverage

Conversation

@vrogojin
Copy link
Copy Markdown
Contributor

Summary

Fills the remaining CLI test gaps documented in the PR #13 follow-up comment on issue #156. 5 commits, +1,749 lines, +89 tests across 5 namespaces.

Commit New tests Surface
744afa7 18 swap-* offline: help + arg validation, swap-ping HELP gap, swap-propose multi-flag guards
8dfe8b4 3 + infra swap e2e (Docker escrow + testnet relay): default ping/propose/cancel; full-settlement stretch behind E2E_RUN_SWAP_FULL=1
379a8d8 37 crypto/util expansion: 12 commands, behaviour pins for --unsafe-print TTY guard, deterministic derive-*, hex-to-wif literal, base58 / encrypt-decrypt roundtrips
78d593a 2 init --nametag combined flow (mint-on-init e2e)
e377376 26 group (9 cmds) + market (5 cmds) offline tiers

Infrastructure additions

  • test/integration/local-infra/docker-compose.yml — local Nostr relay (port 7778, image pinned). Reserved for future group/market e2e.
  • test/integration/local-infra/relay.ts — relay lifecycle helper (ported from trader-service).
  • test/integration/local-infra/escrow.ts — escrow container spawn + log-poll for sphere_initialized. Defaults to escrow:local-uxf; override via SPHERE_CLI_ESCROW_IMAGE.
  • test/integration/helpers.tscreateSphereEnv now accepts { extraEnv } for callers needing SPHERE_NOSTR_RELAYS etc.

Critical finding — escrow image staleness

The published `ghcr.io/vrogojin/agentic-hosting/escrow:v0.1` image (2026-04-25) is missing:

  • PR #105 (UXF Inter-Wallet Transfer Protocol)
  • PR #115 (SDK race-condition fixes from live e2e)
  • PR #119 (verifyPayout OVER_COVERAGE rejection)
  • PR #128 (getTokenIdsForInvoice on deps facade)
  • PR #146/#147/#149/#152 (UXF dispatcher work)
  • Many `payments/*` faucet-flow regression fixes

For the swap e2e to work against current sphere-sdk, the escrow image must be rebuilt locally. Build steps documented in `escrow.ts`:
```
mkdir /tmp/escrow-uxf-build && cd /tmp/escrow-uxf-build
rsync -a --exclude=node_modules --exclude=dist --exclude=.git \
/home/vrogojin/escrow-service/ ./escrow-service/
rsync -a --exclude=node_modules --exclude=dist --exclude=.git \
--exclude=tests --exclude=.claude --exclude=docs --exclude=examples \
/home/vrogojin/uxf/ ./sphere-sdk/
docker build -f escrow-service/Dockerfile -t escrow:local-uxf .
```

Downstream TODO (not blocking this PR): publish `ghcr.io/vrogojin/agentic-hosting/escrow:v0.2` from updated agentic_hosting against current uxf so external CI runs don't need the local-build step.

Test plan

  • Offline tier: `SKIP_INTEGRATION=1 npm run test:integration` → 162 passing / 55 skipped / 0 failed (43s)
  • swap e2e default tier: `E2E_RUN_SWAP=1 npm run test:integration -- cli-swap-e2e` → 2 passing / 1 skipped (2.5 min)
  • swap e2e full settlement: gated behind `E2E_RUN_SWAP_FULL=1` (known fragile — bob's `--deposit --no-wait` submission doesn't always complete before alice's 300s status budget; tracked as follow-up)
  • init --nametag combined flow: 2 tests passing against testnet (~5s)
  • All offline tests deterministic, <5s per file

Open follow-ups (not blocking)

  1. swap full-settlement fragility — needs deeper ordering rework (likely change accept→deposit sequencing or lengthen budget). Gated.
  2. group/market e2e tiers — defer until a NIP-29-capable local relay is validated (the unicity-tokens-relay used here is generic nostr-rs-relay; not confirmed to handle NIP-29 moderation events). Offline tiers in this PR cover the load-bearing CLI plumbing.
  3. Publish escrow:v0.2 — agentic-hosting team needs to push a rebuilt image against current uxf.

Refs #156

Vladimir Rogojin added 6 commits May 15, 2026 17:14
Pins the binary-level CLI plumbing between users and SwapModule for all
8 swap-* commands: namespace bridge, arg parsing, help-text shape, and
pre-getSphere() validation paths.

  - Help-shape pins for 7 commands (Usage line + per-flag regex)
  - swap-ping HELP_TEXT gap pin (no help entry; locks current behaviour)
  - Arg-validation pins: 6 swap-* commands that require args[1] before
    wallet load; refactor moving the check below getSphere() flips red
  - swap-propose multi-flag guard: missing flags, partial flags,
    out-of-range --timeout (60-86400 sec)

18 tests, all offline, ~4.7s total. No infrastructure dependencies.
Live swap lifecycle (Docker escrow + funded wallets) ships in a
follow-up commit on this branch.

Refs #156
… + faucet)

Adds end-to-end coverage of the swap CLI surface against a real escrow.
Pairs with the offline tier in 744afa7 to give complete pin coverage of
the sphere-cli ↔ SwapModule ↔ escrow protocol path.

What lands:
  - test/integration/local-infra/docker-compose.yml — local NIP-29 relay
    (port 7778; reserved for group/market follow-up — swap uses public
    testnet relay)
  - test/integration/local-infra/relay.ts — relay lifecycle helper,
    ported from /home/vrogojin/trader-service/test/e2e-live/local-infra
  - test/integration/local-infra/escrow.ts — escrow container spawn +
    log-poll for `sphere_initialized` direct address. Image defaults to
    `escrow:local-uxf` (must be locally built against
    integration/all-fixes — see docstring). Override with
    SPHERE_CLI_ESCROW_IMAGE.
  - test/integration/helpers.ts — `createSphereEnv` now accepts
    `{ extraEnv }` for callers that need to inject env vars (e.g.
    SPHERE_NOSTR_RELAYS).
  - test/integration/cli-swap-e2e.integration.test.ts — gated by
    E2E_RUN_SWAP=1. Two scenarios on the default tier:
      1. swap-ping → "Escrow is online"
      2. swap-propose → swap-list on counterparty → swap-cancel →
         confirm absent from default open list
    Plus a stretch full-settlement scenario behind E2E_RUN_SWAP_FULL=1
    (known fragility: deposit-conclude often stalls because bob's
    --deposit --no-wait submission may not complete before alice's
    300s status budget; tracking as follow-up).

Why public testnet relay instead of local: adding a local relay to the
wallet's SPHERE_NOSTR_RELAYS list (alongside testnet) caused the faucet
gift-wrap to never land in the wallet's inbox. Root cause unclear —
under investigation. The simpler architecture (everyone on testnet
relay) gives a working 2.5-min default e2e tier.

Why the local image: ghcr.io/vrogojin/agentic-hosting/escrow:v0.1
(2026-04-25) predates UXF protocol (PR #105), swap race fixes (#115),
verifyPayout OVER_COVERAGE (#119), getTokenIdsForInvoice exposure
(#128). Building against integration/all-fixes was required.

Tests:
  - Default tier: 2 scenarios pass (~2.5 min)
  - Skipped without E2E_RUN_SWAP=1 (default CI fast tier)

Refs #156
Expands the 49-line cli-crypto.integration.test.ts to a 37-test
table-driven suite covering all 12 crypto/util commands with
HELP_TEXT entries:
  generate-key, validate-key, hex-to-wif, derive-pubkey,
  derive-address, base58-encode, base58-decode,
  to-smallest, to-human, format, encrypt, decrypt

Three layers, all offline (~10s total):

  1. Help-shape pins for all 12 (Usage line + per-flag regexes).
  2. Arg-validation pins: 11 commands pre-validate args[1] before
     getSphere(). Bare invocation → usage hint + non-zero exit.
  3. Behaviour pins:
       - generate-key emits pubkey + alpha1 address; secrets hidden
       - --unsafe-print SECURITY guard: refuses non-TTY (prevents
         leaking the freshly-minted privkey into vitest log buffers)
       - validate-key true/false JSON output shape + exit code
       - hex-to-wif deterministic WIF for stable test privkey
       - derive-pubkey is deterministic (literal pin)
       - derive-address is deterministic AND index-sensitive
       - base58-encode/decode roundtrip ("Hello" ↔ 9Ajdvzr)
       - to-smallest/to-human roundtrip via 8-decimal default
       - encrypt → OpenSSL "U2FsdGVkX1" magic header
       - decrypt JSON-quoted ciphertext → plaintext
       - decrypt wrong-pw does NOT yield original plaintext
         (CLI exits 0 — CryptoJS AES-CBC has no HMAC; pin documents
         current behaviour and catches pathological keystream collisions)

Refs #156
Adds 2 e2e tests pinning the init-time nametag registration in
cli-wallet-lifecycle:
  - `sphere init --nametag it_<hex>` returns identity JSON with the
    nametag field populated (proves Sphere.init's nametag option
    minted on-chain)
  - `sphere nametag my` confirms the binding persisted locally
    (proves the registerNametag → storage write path)

The combined flow differs from `init` then `nametag register` in
its failure mode: a mid-mint failure can leave wallet stored but
nametag unregistered. Sphere.init handles this defensively; we pin
the happy path so a regression that drops nametag persistence
during init becomes visible.

Note: `payments validate` was in the follow-up gap list but does
NOT actually exist as a command — only `verify-balance` does (which
cli-wallet-state already pins). No action needed there.

Refs #156
Adds 26 tests pinning the namespace-bridge → dispatcher glue for the
two remaining unaddressed CLI namespaces:

  group (9 commands, 17 tests):
    create / list / my / join / leave / send / messages / members / info
    — help-shape + arg-validation pins for the NIP-29 group chat surface.

  market (5 commands, 9 tests):
    post / search / my / close / feed
    — help-shape + arg-validation pins for the P2P bulletin-board surface.
    Including market-post's two-step pre-getSphere() guard (description
    positional, then --type required flag).

Live e2e tiers deliberately deferred:
  - group: would need a NIP-29-capable local relay (the unicity-tokens-
    relay used by the swap suite is generic nostr-rs-relay; not
    confirmed to handle NIP-29 moderation events). SDK-level coverage
    exists in sphere-sdk tests/relay/groupchat-relay.test.ts.
  - market: would need long-form NIP-23 relay + broadcast network.
    SDK-level coverage covers the module mechanics.

The offline tier here is enough to catch the failure modes that hurt
users most: a refactor renames a flag (silent break), or moves the
arg check below getSphere() (turning "did I type the command right?"
into a 10-second wallet load). Live roundtrips are SDK-team territory.

Refs #156
Addresses code-reviewer feedback before merging into integration/all-fixes.

BLOCKER fix:
  - escrow.ts:175 — UNICITY_RELAYS → UNICITY_NOSTR_RELAYS. The escrow
    service's acp-adapter reads UNICITY_NOSTR_RELAYS (with
    SPHERE_NOSTR_RELAYS fallback). The old var name was silently
    ignored, so the container fell back to network defaults regardless
    of opts.relayUrl. Worked today only because the e2e suite targets
    the public testnet relay (which is also the network default);
    pointing at a local Nostr relay would have failed silently.

IMPORTANT fixes:
  - escrow.ts:168 — UNICITY_MANAGER_DIRECT_ADDRESS now uses
    `DIRECT://<pubkey>` form, not raw pubkey hex. The current escrow
    code only checks for non-empty, but a future routing change would
    dereference it as a transport address; the placeholder needs to
    be syntactically correct.
  - helpers.ts — MaxListenersExceededWarning from accumulating exit
    handlers (3 per re-evaluation × 14 test files = 42 vs default
    limit of 10). Guard against re-registration via a global symbol
    so the three process.once handlers fire at most once per worker
    regardless of how many test files import this module.

NIT fixes:
  - escrow.ts materializeWalletDir — 0700 chmod on the wallet dir
    + mkdir mode hardening. Matches the helpers.ts pattern. Paranoia
    for non-POSIX platforms where mkdtemp may inherit DACLs.
  - cli-swap-e2e.integration.test.ts — added inline comment on the
    full-settlement describe.skipIf block documenting why the tier is
    gated, the known failure mode, and likely fix paths.
  - cli-crypto.integration.test.ts — replaced "known shortcoming"
    wording with explicit TODO(security) marker that flags the
    decrypt-wrong-pw-exits-0 path as a CLI defect to be addressed in
    a separate PR (move to AES-GCM with backward-compat shim).

Test plan:
  - Typecheck: clean
  - Offline tier: 162 passing / 55 skipped / 0 failed (44s)
  - E2E_RUN_SWAP=1: ping + propose/list/cancel pass (~2.2 min)
  - Sigint/sigterm cleanup for the escrow Docker container is NOT
    addressed in this commit (reviewer's lowest-severity nit; the
    container is visible via `docker ps` and easy to clean up
    manually).

Refs #156
@vrogojin vrogojin merged commit d5e1504 into integration/all-fixes May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant