Skip to content

test(e2e-live): per-session isolation + bounded-parallel provisioning - #10

Open
vrogojin wants to merge 3 commits into
fix/e2e-live-real-issuesfrom
feat/parallel-e2e-isolation
Open

vrogojin wants to merge 3 commits into
fix/e2e-live-real-issuesfrom
feat/parallel-e2e-isolation

Conversation

@vrogojin

@vrogojin vrogojin commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

Two concurrent invocations of `npm run test:e2e-live` previously shared a flat namespace for Docker container names and /tmp directories — a handful of name collisions away from cross-run interference. Adds a per-process `SESSION_ID` (8-hex, generated once at module load; overridable via `TRADER_E2E_SESSION_ID`) and stamps it onto:

  • Every Docker container name (trader + escrow): `trader-e2e---`
  • Every /tmp wallet/controller dir: `/tmp/trader-e2e---XXXXXX`
  • The diagnostic-dump filter in `waitForDealInState` (was a dead lookup of `escrow-e2e` that never matched anything; now uses the session prefix)

Replaces `provisionTradersStaggered`'s sequential for-loop with a bounded worker pool (default concurrency 3, env-tunable via `TRADER_E2E_PROVISION_CONCURRENCY`).

Adds opt-in `VITEST_MAX_FORKS` env knob to `vitest.e2e-live.config.ts`. Default remains 1 (`singleFork: true`) for backward compatibility — multi-fork parallelism is opt-in.

Test plan

  • All 113 helper unit tests pass with the new session module wired in
  • Smoke test: single test file (`basic-roundtrip`) runs cleanly with session-prefixed containers (`trader-e2e-6a3177c7-basic-buyer-h0t91t`)
  • Full e2e-live suite passing 11/11 (with all related fixes merged) in 23 min

vrogojin added 2 commits May 3, 2026 10:14
Two concurrent invocations of `npm run test:e2e-live` previously shared a
flat namespace for Docker container names and /tmp directories — a hand-
ful of name collisions away from cross-run interference. Add a per-process
SESSION_ID (8-hex, generated once at module load; overridable via
TRADER_E2E_SESSION_ID) and stamp it onto:

  - Every Docker container name (trader + escrow): trader-e2e-<SID>-<label>-<rand>
  - Every /tmp wallet/controller dir: /tmp/trader-e2e-<SID>-<label>-XXXXXX
  - The diagnostic-dump filter in waitForDealInState (was a dead lookup
    `escrow-e2e` that never matched anything; now uses the session prefix
    which captures both trader and escrow containers cleanly)

Nostr-side identifiers (per-trader secp256k1 keypair, nametag from UUID)
already have ≥10⁹ entropy so they don't need a session prefix; adding one
to instance_id would only reduce the 9-hex randomness in the nametag
slice, increasing collision risk within a session.

Replace `provisionTradersStaggered`'s sequential for-loop with a bounded
worker pool (default concurrency 3, env-tunable via
TRADER_E2E_PROVISION_CONCURRENCY). The pool preserves input ordering of
results and caps simultaneous Sphere.init/nametag-publish load on the
shared testnet relay. Validated by provisioning-load-investigation that
3-way parallel is reliable on a healthy relay; tunable down to 1 when
the relay is degraded.

Add VITEST_MAX_FORKS opt-in knob to vitest.e2e-live.config.ts. Default
remains 1 (singleFork: true) for backward compatibility — multi-fork
parallelism is opt-in by env so users can flip it when ready.

All 113 helper unit tests pass with the new session module wired in.
…er + leak-free worker pool

Two critical findings from the steelman review of #10:

## C1: anchor docker --filter name regex with `^`

`docker ps --filter name=X` is a SUBSTRING match by default — `name=foo`
matches any container whose name CONTAINS "foo", not just those that
START with "foo". The PR documented session-prefix as a security-style
isolation guarantee, but the unanchored filter could in theory match
adjacent-session containers if their session IDs shared leading hex digits.

Anchor with `^` (Docker passes the value through to its regexp matcher,
so `^prefix` is honored). Verified with two containers `test-prefix-foo`
and `xtest-prefix-foo-y`:
- without `^`: matches both (substring)
- with `^`: matches only the prefix-anchored one

## C2: Promise.allSettled + dispose orphaned tenants on partial failure

The previous worker pool used `await Promise.all(workers)`, which rejects
on first error. But other in-flight workers continue spawning containers
AFTER the function rejects, and those containers never reach `results`,
so the caller's `afterAll` never sees them. **Container + /tmp leak,
strictly worse than the sequential predecessor.**

Fix: each worker catches per-task errors into a shared `errors[]` array
and continues draining. After all workers settle, if any errors were
recorded, dispose every tenant that DID succeed (they're unreachable to
the caller through the rejected promise) before re-throwing the first
error. Multiple errors are attached as `.otherErrors` on the primary so
they aren't silently swallowed.

## Drive-by: replace top-level `generatePrivateKey` import

`@unicitylabs/sphere-sdk` no longer exports `generatePrivateKey` at the
package root (moved to the L1 sub-namespace). Replace with an inlined
`randomBytes(32).toString('hex')` — a secp256k1 private key is just 32
random bytes, and the probability of generating an invalid value is
~2^-128 (vanishingly small).

## Test plan

- 113/113 helper unit tests pass
- typecheck clean for the test/ tree (the unrelated `mintFungibleToken`
  type error in `src/trader/main.ts` belongs to PR #12 and is fixed there)
- Docker filter anchor verified empirically with two test containers
vrogojin added a commit that referenced this pull request May 3, 2026
Call `sphere.accounting.setAutoReturn('*', true)` immediately after
`Sphere.init` so any terminated invoice this wallet is a target of will
have its surplus (`coveredAmount > requestedAmount`) refunded
automatically to each over-paying party at their `refundAddress` ?? `senderAddress`.

Why this matters: the SDK's AccountingModule already tracks per-payer
contributions and emits `invoice:overpayment`, but the actual refund
only fires when auto-return is enabled. With sphere-sdk PR-119 making
`SwapModule.verifyPayout` explicitly fail with `OVER_COVERAGE` on
net > expected, surplus on a trader's payout invoice is detectable AND
must be refunded — this PR wires the refund.

## Review-feedback hardening

**RATE_LIMITED guard** (PR-12 review W2). The SDK throws `RATE_LIMITED`
if `setAutoReturn('*', true)` is called twice within a 5-second cooldown.
Process restart wouldn't normally hit it (cooldown is in-memory only),
but an in-process supervisor that retries `startTrader` on error would.
`RATE_LIMITED` here is functionally a no-op (flag is already true) so we
treat it as success, log distinct event, and continue startup.

**Startup-cost note** (PR-12 review W1). `setAutoReturn('*', true)` is
NOT just a flag flip — when enabled, the SDK iterates
`closedInvoices ∪ cancelledInvoices` (capped at 100) and runs
`_executeAutoReturnFromFrozen` for each, which issues real outbound
payments. Operators should expect the first call after wallet migration
to be slow. Documented in the comment.

## Drive-by

`@unicitylabs/sphere-sdk` no longer exports `generatePrivateKey` at the
package root (moved to the L1 sub-namespace). Replace with inlined
`randomBytes(32).toString('hex')` in `test/e2e-live/helpers/tenant-fixture.ts`.
**The same fix is also in PR #10**; whichever lands first wins, the
other gets a trivial merge. Required for typecheck/tests to pass against
current sphere-sdk.

`mintFungibleToken` was added in sphere-sdk's
`refactor/extract-cli-to-sphere-cli` branch and never landed in main.
Trader-service's `TRADER_TEST_FUND` test-helper code path called it
unconditionally; replace with a guarded shim that throws explicitly when
the method isn't available. The e2e suite uses the faucet path, not
TRADER_TEST_FUND, so this guard never trips in production CI but the
typecheck error blocks docker image build.

## Test plan

- [x] All 671 unit tests pass (was failing before drive-by fix)
- [x] Typecheck + lint clean
- [x] **End-to-end validation** (with sphere-sdk #119 + #120 active):
      trader-service e2e-live suite 11/11 passing in 23 min;
      multi-agent test went from 26-min OVER_COVERAGE hang → 4.9-min PASS
…tion + dash-aware sanitizer

Three review-warning items from #10:

## W4: 32-bit → 64-bit SESSION_ID entropy

`randomBytes(4)` = 32 bits. Birthday-bound collision probability at 100
concurrent CI shards: ~1.2e-6 (small but non-zero). At 1000 shards: ~1.2e-4.
Bumping to `randomBytes(8)` = 64 bits drops collision probability to
~5e-15 even at thousands of concurrent runs — effectively impossible for
any realistic deployment. Cost: 4 additional random bytes. Trivial.

## W5: validate env-var inputs loudly, not silently

Two env vars used to silently floor invalid values:

- `TRADER_E2E_PROVISION_CONCURRENCY=0` (intent: force sequential, cc=1)
  used to fall back to DEFAULT_PROVISION_CONCURRENCY=3, surprising the
  operator. Now throws with a clear "must be >= 1, use cc=1 to force
  sequential" message.
- `VITEST_MAX_FORKS=abc` used to silently fall back to 1 via `|| 1`.
  Typos slipped through. Now throws with a "must be a positive integer"
  message. Same treatment for negative/NaN.

Both validators preserve the documented defaults when the env var is
unset or empty — no behavior change in the common case.

## W6: hex sanitizer respects dashes

`TRADER_E2E_SESSION_ID` override was sanitized via `[^0-9a-f]` strip,
silently mangling values like `ci-job-1234-abc` (CI driver tag) into
`abc`. Tighten to `[^0-9a-z-]` so dashes (which Docker container names
allow) are preserved. Length cap raised to 32 chars to match the
larger 64-bit auto-generated ID. Non-Docker-safe characters (slashes,
colons, spaces) are still stripped to prevent argv injection.

## Drive-by note

The pre-existing `mintFungibleToken` typecheck error in `src/trader/main.ts`
is fixed by PR #12. Whichever lands first wins; both touch this file.

## Test plan

- All 671 helper unit tests pass
- Typecheck clean for test/ tree (the mintFungibleToken error is upstream)
- VITEST_MAX_FORKS=abc throws "must be a positive integer"
- TRADER_E2E_PROVISION_CONCURRENCY=0 throws "must be >= 1"
- TRADER_E2E_SESSION_ID=ci-job-1234 round-trips with dashes intact
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