e2e (shellnet) #72
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: e2e (shellnet) | |
| # Real end-to-end tests against the live shellnet: deploy a market, stake, | |
| # split, place/cancel orders, buyFullSet. Slow (~8-10 min), hit the network, | |
| # and spend test NACKL. Separate from pr-tests.yml (its `tests` job skips | |
| # these — nextest does not run `#[ignore]` tests by default). | |
| on: | |
| # Called by pr-tests.yml after its unit/integration jobs pass, so e2e gates | |
| # a PR only once everything else is green. Also runs standalone nightly and | |
| # on demand. | |
| workflow_call: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 3 * * *" # nightly ~03:00 UTC | |
| # Never interrupt a run mid-deploy — a half-applied shellnet deploy leaves | |
| # orphan contracts. One e2e run at a time. | |
| concurrency: | |
| group: e2e-shellnet | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_PROFILE_DEV_DEBUG: "0" | |
| CARGO_PROFILE_TEST_DEBUG: "0" | |
| CARGO_INCREMENTAL: "0" | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| e2e: | |
| name: e2e against shellnet | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| env: | |
| # Same shape as the PR `tests` job — local read-model the e2e helpers | |
| # upsert the deployed market into. | |
| TEST_DATABASE_URL: postgres://dodex:dodex@localhost:5432/dodex_test | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: dodex | |
| POSTGRES_PASSWORD: dodex | |
| POSTGRES_DB: dodex_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U dodex -d dodex_test" | |
| --health-interval 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust nightly | |
| uses: dtolnay/rust-toolchain@nightly | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: pr-tests | |
| cache-on-failure: true | |
| - name: Install nextest | |
| uses: taiki-e/install-action@nextest | |
| # Self-provision fresh throwaway deployer notes per run, minted from the | |
| # public shellnet giver (`mint_pn_pool` → `<out>.seed_notes.json`, the exact | |
| # format TestPnPool::load() reads). Replaces the old S3 fetch: once a run | |
| # drains + withdraws a note (the hasWithdrawn latch) that note is dead for | |
| # deployPMP (ERR_INVALID_STATE), so any long-lived hosted note goes stale. | |
| # Fresh notes per run sidestep cross-run poisoning and need no secret. | |
| # They are drained in the teardown step below. | |
| # TWO NOTES, AND THE CURRENCY IS NOT INTERCHANGEABLE. The trader path | |
| # deploys NACKL markets and stakes out of `_balance[NACKL]`; an inference | |
| # buy is paid out of `_balance[CURRENCIES_ID_SHELL]`. Only the constructor | |
| # writes that ledger (`_balance[tokenType] = value`), so the deposit | |
| # currency at deploy time decides which of the two a note can do — for | |
| # good. Topping a NACKL note up with SHELL does not help: the gas voucher | |
| # credits PHYSICAL ECC on the account, and the escrow stopped coming from | |
| # there. Each pool is single-currency by construction (the minter refuses | |
| # to append a different token type), so they are minted separately and the | |
| # two seed_notes arrays concatenated; `--profile` is what tells the loader | |
| # which rows belong to which suite. | |
| - name: Mint fresh e2e seed notes | |
| working-directory: sdk | |
| run: | | |
| cargo run --release --bin mint_pn_pool -- \ | |
| --count 1 --nominal N10000 --token-type nackl --profile PN-API \ | |
| --endpoint https://shellnet.ackinacki.org \ | |
| --output "$RUNNER_TEMP/pn_pool_api.json" | |
| cargo run --release --bin mint_pn_pool -- \ | |
| --count 1 --nominal N10000 --token-type shell --profile PN-INF \ | |
| --endpoint https://shellnet.ackinacki.org \ | |
| --output "$RUNNER_TEMP/pn_pool_inf.json" | |
| jq -s 'add' \ | |
| "$RUNNER_TEMP/pn_pool_api.seed_notes.json" \ | |
| "$RUNNER_TEMP/pn_pool_inf.seed_notes.json" \ | |
| > "$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json" | |
| test "$(jq 'length' "$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json")" = 2 | |
| jq -r '.[] | "minted \(.profile) tokenType=\(.tokenType) \(.pn_address)"' \ | |
| "$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json" | |
| # Single-threaded on purpose: the PN-per-slot pool removes the per-PN | |
| # `_busy` contention, but every deploy still hits the shellnet-global | |
| # RootOracle (deployOracle) and OracleEventList (addEvent), which | |
| # serialise on-chain — parallel deploys race to exit_code 52/101. | |
| # `--run-ignored only` runs exactly the `#[ignore]`-marked e2e tests. | |
| # No `-E` filter: there is nothing left to exclude. A | |
| # `not binary(e2e_inference_dispute)` used to sit here to skip a suite that | |
| # waited out a ~1200s acceptance window, and it outlived the suite — a | |
| # `binary()` predicate matching nothing is an error in nextest, not a | |
| # no-op, so the step died on the filter before running a single test. | |
| # Anything excluded here must name a binary that exists. | |
| - name: cargo nextest run (e2e, single-threaded) | |
| run: cargo nextest run -p dodex-api --run-ignored only --test-threads 1 --no-fail-fast | |
| # Teardown: drain the throwaway note to a random (uninit) address so the | |
| # plaintext key surfaced in the mint step / fixture controls no funds after | |
| # the run — and the note ends in the hasWithdrawn state, unusable. Runs even | |
| # if the suite failed; best-effort (the next run mints a fresh note anyway), | |
| # so a drain failure never fails the job. | |
| - name: Drain e2e seed notes (teardown) | |
| if: always() | |
| working-directory: sdk | |
| run: | | |
| seed="$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json" | |
| [ -s "$seed" ] || { echo "no seed note to drain — skip"; exit 0; } | |
| # Every row, not just the first: the file holds one note per profile | |
| # and each one's key is in the clear. Draining only row 0 would leave | |
| # the other spendable by anyone who read the log. | |
| for i in $(jq -r 'to_entries[].key' "$seed"); do | |
| jq --argjson i "$i" \ | |
| '{pn_address: .[$i].pn_address, owner_public_key_hex: .[$i].pn_pubkey_hex, owner_secret_key_hex: .[$i].pn_seckey_hex}' \ | |
| "$seed" > "$RUNNER_TEMP/pn_state.json" | |
| dest="0:$(openssl rand -hex 32)" | |
| echo "draining seed note $i ($(jq -r ".[$i].profile // \"<unprofiled>\"" "$seed")) to random dest $dest" | |
| cargo run --release --bin dexdo -- withdraw \ | |
| --pn-state-file "$RUNNER_TEMP/pn_state.json" \ | |
| --dest "$dest" \ | |
| --endpoint shellnet.ackinacki.org || echo "teardown withdraw failed for note $i (non-fatal)" | |
| done |