Skip to content

test(cketh): give the PocketIC server a longer lifetime than the suites - #11123

Draft
gregorydemay wants to merge 44 commits into
masterfrom
ic_DEFI-2262_7_pocket-ic-server-ttl
Draft

test(cketh): give the PocketIC server a longer lifetime than the suites#11123
gregorydemay wants to merge 44 commits into
masterfrom
ic_DEFI-2262_7_pocket-ic-server-ttl

Conversation

@gregorydemay

Copy link
Copy Markdown
Contributor

Follow-up to #10955, addressing this review comment.

Summary

The PocketIC server exits hard 10 minutes after it starts, whatever the traffic, and PocketIcBuilder does not expose that lifetime. Since the builder starts the server with reuse: true, every fixture in a test binary shares one server, so the clock runs against the whole suite rather than a single test. A single-threaded run of the ckERC20 suite takes about 8 of those 10 minutes, and losing the server mid-suite fails every test that follows instead of the one that is actually slow.

Start the server explicitly with a longer lifetime and hand the builder its URL.

Notes

The 600s default is hard_ttl, not ttl: ttl is an idle timeout that activity keeps resetting, whereas hard_ttl is measured from launch and ignores traffic.

This follows the approach of #11086, which adds an equivalent long_lived_server_url() helper to this same crate for the sweeper tests. The shape and name here are deliberately identical so the two can be collapsed into one helper once that lands.

🤖 Generated with Claude Code

gregorydemay and others added 30 commits July 29, 2026 11:42
Add a PocketIC-based test fixture (LedgerSuiteOrchestrator, AddErc20TokenFlow,
ManagedCanistersAssert, UniversalCanister, new_pocket_ic()) alongside the
existing StateMachine one in test_utils, and migrate tests/tests.rs to it.

The StateMachine fixture stays untouched at its current paths since
rs/ethereum/cketh still consumes it; it will be deleted once cketh migrates
to PocketIC in a later PR of this stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LedgerSuiteOrchestrator and ManagedCanistersAssert each carried an
AsRef<PocketIc> impl left over from porting the StateMachine fixture, where
it satisfied `T: AsRef<StateMachine>` bounds on stop_canister/
out_of_band_upgrade and a manual CanisterHttpQuery impl. Those call sites now
take `&PocketIc` directly and go through PocketIcHttpQuery, so neither impl
has a caller left (G9/G12).

Addresses review comments:
#10949 (comment)
#10949 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- mod.rs: replace 13 inline `crate::` paths (default_init_arg, the wasm
  loaders, GIT_COMMIT_HASH, MAX_TICKS) with a single `use crate::{...}`
  block, matching the plain `use crate::MINTER_PRINCIPAL;` style already used
  in the sibling flow.rs. This also lets wait_for's panic message go back to
  inline-capture ("...{MAX_TICKS} ticks...") instead of a positional arg.
- mod.rs: alias `ic_cdk::management_canister::CanisterStatusResult` as
  `CdkCanisterStatusResult` to disambiguate it from the
  `ic_management_canister_types::CanisterStatusResult` imported in the same
  file, instead of spelling out the full path twice.
- flow.rs: import `ic_management_canister_types::CanisterId` instead of
  fully qualifying it once, for the same reason mod.rs already imports it.

Addresses review comments:
#10949 (comment)
#10949 (comment)
#10949 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ledger_canister_status() returns an owned CanisterStatusResult temporary, so
module_hash can be moved straight out of it instead of being cloned first
(R3).

Addresses review comment:
#10949 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment was carried over verbatim from the StateMachine fixture and
still referenced "10ms per transfer with state machine tests" inside the
module that specifically does not use StateMachine (C2). Dropping the
parenthetical rather than guessing a PocketIC-specific number.

Addresses review comment:
#10949 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
//:pocket-ic-server was listed in both `data` (needed for the
$(rootpath //:pocket-ic-server) used by POCKET_IC_BIN) and `deps`. It's a
runtime binary reached only through that env var, not linked, so `data`
alone is the accurate declaration -- matching rs/ledger_suite/icp/BUILD.bazel
(data only), as opposed to rs/bitcoin/checker and rs/dogecoin/ckdoge/minter
(deps only, where the binary is invoked differently).

Verified bazel test //rs/ethereum/ledger-suite-orchestrator:integration_tests
still passes (POCKET_IC_BIN resolves fine from data alone).

Addresses review comment:
#10949 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline raw-transaction and transaction-hash string literals in
minter/tests/cketh.rs were byte-identical to the DEFAULT_WITHDRAWAL_TRANSACTION
and DEFAULT_WITHDRAWAL_TRANSACTION_HASH constants already exported by
cketh/test_utils. Replace them with the constants so the upcoming
PocketIC migration only needs to regenerate the signature in one place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocketIC

Swap ic-state-machine-tests (and its transitive management-canister-types
/ic-types/ic-cdk deps) for the sync pocket-ic client in
rs/ethereum/cketh/test_utils and the minter's integration_tests target, and
wire up the pocket-ic-server binary as test data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flip CkEthSetup and its helpers (mock.rs, flow.rs, ckerc20.rs) onto the
sync PocketIC client: builder uses with_fiduciary_subnet() with canister
execution rate limiting disabled, canisters are funded with
add_cycles(id, u128::MAX), and the minter is initialized with the
fiduciary subnet's "key_1" ECDSA key instead of the StateMachine-only
"master_ecdsa_public_key".

The ECDSA key change means the minter's Ethereum address changes, so
every signature-derived constant is regenerated: MINTER_ADDRESS,
DEFAULT_WITHDRAWAL_TRANSACTION(_HASH), DEFAULT_CKERC20_WITHDRAWAL_
TRANSACTION(_HASH), and the r/s of the default ERC20 signed transaction
in response.rs.

Canister-http mocking is rebuilt around PocketIC's get_canister_http() +
mock_canister_http_response() (replacing the StateMachine-specific manual
cleanup_response/transform dance), while preserving the legacy
"Http body exceeds size limit of {n} bytes." reject the minter's
is_response_too_large matches on. Polling for a specific outcall now also
tolerates ic-cdk-timers' per-timer concurrent-call cap: the block-height-
refresh timer and the log-scraping timer compete for it, and once hit, the
loser defers by a full REFRESH_LATEST_BLOCK_HEIGHT_INTERVAL that
nanosecond-granularity ticking alone can never cross.

Awaiting an update call now goes through a manual ingress_status polling
loop instead of PocketIC's own await_call, since the latter ticks up to
100 rounds with no opportunity to inject mock canister-http responses in
between.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapt cketh.rs and ckerc20.rs to the PocketIC-backed test_utils API
(error_code/reject_message instead of code()/description(), get_canister_
http() instead of canister_http_request_contexts(), stop_canister(id,
None), Principal-based canister ids), and regenerate the few remaining
signature/key-derivation-dependent literals inline in these test files:
the per-account deposit-address in should_record_address_to_deposit and
the resubmitted-transaction literal in
should_resubmit_new_transaction_with_same_max_fee_per_gas_when_price_increased.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…odes

Addresses review comments on PR #10955:
- mock.rs:187 (medium, G5) - the fallback in `matches()` reimplemented
  `is_latest_block_refresh`, with a different malformed-body behaviour
  (`.expect(...)` vs. `Err(_) => false`). Collapse to
  `!is_latest_block_refresh(request)` and make the predicate `pub(crate)`
  so `lib.rs` can reuse the same implementation.
- mock.rs:152 (medium) - `find_rpc_call_retrying`'s retry budget was
  10 * REFRESH_LATEST_BLOCK_HEIGHT_INTERVAL (300s), more than
  SCRAPING_ETH_LOGS_INTERVAL (180s), so it could itself trigger an
  unrelated log scrape. Capped at 180s (6 attempts): 150s (5 attempts)
  was tried and left several tests unable to find their target call
  within budget, so the cap sits at the interval's exact boundary
  rather than strictly under it.
- mock.rs:125 (nit, G25) - named the SysFatal/SysTransient reject-code
  literals instead of repeating a comment at each site.
- poll_for_request's inline SysTransient reject block now calls the
  shared `reject_stray_http_outcall` (see next commit) instead of
  duplicating it (nit, G5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review comments on PR #10955:
- lib.rs:885 (medium) - the single `await_call` bundled two policies:
  "await the message" and "answer every pending outcall with HTTP 500".
  `stop_minter` needs the draining; `WithdrawalFlow::minter_response`,
  `Erc20WithdrawalFlow::expect_trap` and `DepositErc20Flow::expect_trap`
  do not, since the drain could silently swallow an outcall one of
  those tests intended to stub itself. Split into `await_call` (which
  only clears stray latest-block-refresh outcalls - never a test-owned
  stub, see `mock::is_latest_block_refresh` - so it cannot swallow
  anything a test cares about) and `await_call_draining_outcalls` (the
  old blanket-500 behaviour), used only by `stop_minter`.
- lib.rs:570 (medium) - `stop_ongoing_https_outcalls` returned as if it
  had succeeded even when it never managed to drain the pending
  outcalls. Panic instead, with the still-pending requests attached.
- lib.rs:576 (medium, C2) - the comment on the same function pointed at
  `mock::tick_until_next_http_request`, which no longer exists (renamed
  to `JsonRpcRequestMatcher::poll_for_request` /
  `find_rpc_call_retrying`). Retargeted.
- lib.rs:851 (medium, G5) - `drain_stray_latest_block_refresh_calls` had
  its own copy of the latest-block-refresh predicate, parsed as a raw
  `serde_json::Value` and `.unwrap()`-ed. Reuse
  `mock::is_latest_block_refresh` instead.
- lib.rs:837 (nit) - `drain_startup_http_outcalls` and
  `drain_stray_latest_block_refresh_calls` burned their full tick budget
  even once the outcall queue was already empty. Added an early exit.
- lib.rs:584 (nit, G5) - the HTTP-500 reply block in
  `drain_pending_https_outcalls` was byte-for-byte the one inside
  `await_call`. Extracted a shared `reply_500` helper.
- lib.rs:881 (nit, C4) - the doc comment said "keeps rejecting" pending
  outcalls, but the code replies with an HTTP 500 (a different path
  through the minter than a canister-http reject). Reworded.
- lib.rs:889 (nit, G25/G16) - `100 * MAX_TICKS` expressed 1000 as a
  multiple of an unrelated constant. Wrote the literal with a one-line
  rationale (this is a 100x relaxation of the `await_ingress(id,
  MAX_TICKS)` bound it replaces, needed because DTS can slice a large
  log-response over many more rounds than that).
- lib.rs:538 (nit, G12) - `try_stop_minter_without_stopping_ongoing_
  https_outcalls`'s 10-iteration loop was dead on the passing path (the
  first check returns immediately) and re-read a terminal status 9 more
  times on the failing path. Collapsed to a single tick + assert.
- lib.rs:562 (Copilot) - `tick_until_minter_canister_status` re-read
  `minter_status()` without ever advancing a round itself, relying on
  `canister_status`'s own update-call processing to make progress.
  Added an explicit `tick()` so the loop doesn't depend on that
  incidental side effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review comment on PR #10955:
- ckerc20.rs:114 (nit, G11) - this is the only await in the suite that
  uses pocket-ic's built-in `await_call` instead of
  `ic_cketh_test_utils::await_call`. It is correct (the preceding
  `stop_ongoing_https_outcalls()` already guarantees, by panicking
  otherwise, that no outcall is left pending), but the asymmetry could
  otherwise lead the next reader to copy the wrong one. Added a comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…an enum

Addresses re-review comments on PR #10955:
- lib.rs:900 (nit) - the doc comment claimed no test-owned stub ever
  targets a latest-block-refresh outcall, but two stubs do (with an
  explicit `["latest", false]` param: tests/cketh.rs:842,
  tests/ckerc20.rs:258). They're safe because they're built and
  consumed synchronously before any await runs, not because of any
  exclusion. Reworded to say so, so a future test that interleaves such
  a stub with an awaited call doesn't get its request silently rejected
  out from under it.
- lib.rs:925 (nit, F3/G15) - `poll_for_call`'s `drain_outcalls: bool`
  was a flag argument. Replaced with a two-variant `OutcallPolicy` enum
  (`RejectRefreshOnly` / `DrainAll`), matching the two named public
  wrappers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing in rs/ethereum consumes the old StateMachine-based fixture
(lib.rs's LedgerSuiteOrchestrator, flow.rs, universal_canister.rs) now
that both LSO and cketh integration tests run on PocketIC. Delete it
and the now-unused ic-state-machine-tests, ic-types, and
ic-management-canister-types-private dependencies, keeping only the
environment-agnostic helpers (wasm loaders, init-arg builders,
supported_erc20_tokens/usdc/usdt, constants) that the pocket_ic module
and cketh still import at their existing paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the helpers PR #10955 added on top of the mechanical StateMachine
-> PocketIC port that have no equivalent in the pre-migration fixture:
drain_startup_http_outcalls, drain_stray_latest_block_refresh_calls,
is_latest_block_refresh, the OutcallPolicy/await_call_draining_outcalls
split, and find_rpc_call_retrying. Call sites fall back to the plain
StateMachine-era equivalents (a single-pass stop_ongoing_https_outcalls,
PocketIc::await_call, a 10-tick find_rpc_call poll). Integration test
failures are expected and intentionally left unfixed: this branch is an
analysis instrument to catalog which tests implicitly depended on the
removed machinery, ahead of deciding whether to reintroduce explicit
mocks instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CkEthSetup::new ended with an update call backing the MINTER_ADDRESS
assertion. That call executes the rounds which let the minter's genesis
timers fire, so every fixture-built test inherited eight canister-http
outcalls pending at genesis time. Those requests cross PocketIC's 60s
outcall timeout as soon as a test advances time, which is what the
stripped fixture machinery was compensating for.

Moving the assertion into its own test takes this branch from 3/26 to
20/27 passing without reintroducing any fixture machinery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stop_minter answered the pending HTTPS outcalls in the same round it
submitted the stop request, so the drain ran before the minter had left
the Running state. The outcalls its timers issue in that last running
round only become visible afterwards, and await_call offers no point at
which a test can answer them, so the stop waited on eight call contexts
that nobody could close.

Ticking once between the two lets the stop take effect first. The minter
issues no further outcalls while stopping, so a single drain pass then
suffices.

cketh_test goes from 20/27 to 24/27 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…calls

Advancing time past CANISTER_HTTP_TIMEOUT_INTERVAL kills the outcalls
still in flight, but get_canister_http keeps listing them until a round
processes the timeouts. A stub could therefore bind to a request that is
already dead and see its response discarded, leaving the request issued
by the next scraping cycle unanswered. Ticking once before polling
settles those requests first.

The two block number stubs in should_panic_when_last_finalized_block_in
_the_past also went unconstrained, so they could answer the block height
refresh timer's query for the latest block instead of the finalized
block the scraping cycle waits for.

cketh_test goes from 24/27 to 25/27 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The minter derives its Ethereum address from a timer scheduled at
install, so the address is only known once the canister has executed a
few rounds. A freshly built fixture has executed none, which is what the
deposit flow relies on: it expects to trigger the first scraping cycle
itself. Ticking to derive the address beforehand costs that cycle to
TimerGuard AlreadyProcessing and breaks the flow.

Assert instead what holds at each point: no address before the minter
has run, and the derived address after the deposit. The value itself
stays covered by should_derive_minter_address.

cketh_test goes from 25/27 to 26/27 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two block number stubs went unconstrained, so they could answer the
block height refresh timer's query for the latest block and leave the
finalized block the scraping cycle waits for unanswered. The scrape then
never reached eth_getLogs.

cketh_test is now fully green at 27/27.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deposit flow's block number stub went unconstrained, so it could
answer the block height refresh timer's query for the latest block and
leave the finalized block the scraping cycle waits for unanswered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registering the ERC-20 tokens drives the minter through several rounds,
so unlike the ckETH fixture the ckERC20 one hands over a minter that has
a scraping cycle in flight. Advancing time then leaves that cycle
holding the scraping TimerGuard, so the firing due at the new time is
dropped as AlreadyProcessing and the expected block number query is
never issued.

Answering the query the pending cycle waits for lets it finish and
release the guard. Responding with the block number already scraped
keeps it from scraping logs nobody mocked. The block number stub that
follows also went unconstrained and could answer the block height
refresh timer's query instead of the finalized one.

ckerc20_test goes from 22/40 to 37/40 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…line

should_scrape_from_last_scraped_after_upgrade drives the scraping cycles
itself instead of going through the deposit flow, so it needs the same
handling: answer the query the cycle left in flight by the ckERC20
fixture before advancing time, otherwise that cycle still holds the
scraping TimerGuard and the firing due at the new time is dropped.

Its block number stubs also went unconstrained and could answer the
block height refresh timer's query for the latest block.

ckerc20_test goes from 37/40 to 38/40 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ckETH fixture hands over a minter that has executed no round, so the
deposit flow triggers the first scraping cycle itself and nothing is in
flight when it advances time. The ckERC20 fixture registers its tokens
through the orchestrator and therefore hands over a minter with a cycle
already running, which still holds the scraping TimerGuard when the flow
advances time, so the firing due then is dropped as AlreadyProcessing.

Answer the query such a cycle waits for, and only then, so that flows
starting from a fixture with nothing in flight keep expecting to trigger
the first cycle themselves.

ckerc20_test goes from 38/40 to 40/40 passing, cketh_test stays at
27/27.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PocketIC harness times out canister http requests and keeps listing
them until a round processes the timeouts, and a scraping cycle left in
flight holds the TimerGuard the next firing needs. Neither is visible
from the code that works around them, so state the reasoning next to
each tick, each settled cycle and each constrained block number stub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…info

should_derive_minter_address was inserted between the waiver and the
test it belongs to, leaving the deprecated MinterInfo field that
should_retrieve_minter_info asserts on uncovered. Cargo Lint Linux
builds the test targets with -D warnings and rejected it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…meout

Advancing time past the canister http timeout fails every outcall in
flight, but the requests stay listed until a round processes the
failures, and the cycle awaiting them keeps holding the scraping
TimerGuard until it learns of them. A stub could therefore bind to a
request that was already dead and see its response discarded, and the
firing due at the new time was dropped as AlreadyProcessing.

Route the advances through the fixture and do to those outcalls up
front, and deterministically, what the advance would have done to them
anyway. This replaces the three places that each answered the pending
query on their own before advancing.

Two details the flows depend on: nothing happens when no outcall is in
flight, since a fixture that has executed no round expects to trigger
the first scraping cycle itself; and a single round delivers the
failures, where a second would let the timers fire before the advance.

The block number stubs keep matching on the finalized block, which is
what the scraping cycle actually queries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ching

Advancing time now fails the outcalls in flight before it happens, so no
dead request survives to be listed when a stub looks for its match and
the poll no longer has anything to settle first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gregorydemay and others added 14 commits August 3, 2026 14:45
The StateMachine fixture had to address the management canister itself
to upgrade a canister as its controller. PocketIc exposes that directly
and builds the very same install_code argument, down to the install mode,
so call it instead of assembling the argument by hand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cket-ic

# Conflicts:
#	Cargo.lock
#	rs/ethereum/cketh/test_utils/Cargo.toml
#	rs/ethereum/cketh/test_utils/src/ckerc20.rs
The server exits hard 10 minutes after it starts whatever the traffic,
and every fixture in a test binary shares one server, so the clock runs
against the whole suite rather than a single test. A single-threaded run
of the ckERC20 suite takes about 8 of those 10 minutes, and losing the
server mid-suite fails every test that follows instead of the one that
is actually slow.

PocketIcBuilder does not expose the lifetime, so start the server here
and hand the builder its URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The server exits hard 10 minutes after it starts whatever the traffic,
and every fixture in a test binary shares one server, so the clock runs
against the whole suite rather than a single test. A single-threaded run
of the ckERC20 suite takes about 8 of those 10 minutes, and losing the
server mid-suite fails every test that follows instead of the one that
is actually slow.

PocketIcBuilder does not expose the lifetime, so start the server here
and hand the builder its URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the test label Aug 12, 2026
Base automatically changed from ic_DEFI-2262_3_cketh-pocket-ic to master August 13, 2026 06:38
pull Bot pushed a commit to bit-cook/ic that referenced this pull request Aug 13, 2026
## 📚 PR stack

1. ~~dfinity#10949~~ — test(ledger-suite-orchestrator): migrate integration
tests to PocketIC (merged)
2. ~~dfinity#10950~~ — test(cketh): deduplicate signed-transaction and
transaction-hash literals (merged)
3. **dfinity#10955 — test(cketh): migrate integration tests to PocketIC** 👈 you
are here
4. ~~dfinity#10956~~ — test(ledger-suite-orchestrator): drop the StateMachine
test fixture — **its content is part of this PR**, see below

Follow-up, based on this PR: dfinity#11123 (PocketIC server lifetime).

## Summary

Flips `rs/ethereum/cketh/test_utils` and the minter's
`cketh.rs`/`ckerc20.rs` integration tests from `ic-state-machine-tests`
onto the sync `pocket_ic` client, following the pattern established for
the ledger-suite-orchestrator in dfinity#10949.

The minter is now initialized against the fiduciary subnet's `"key_1"`
ECDSA key (StateMachine's `"master_ecdsa_public_key"` isn't available on
PocketIC's fiduciary subnet), which changes the minter's derived
Ethereum address. Every signature-derived constant is regenerated as a
consequence: `MINTER_ADDRESS`, `DEFAULT_WITHDRAWAL_TRANSACTION(_HASH)`,
`DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION(_HASH)`, the r/s of the default
ERC20 signed transaction, the per-account deposit address in
`should_record_address_to_deposit`, and the resubmitted-transaction
literal in
`should_resubmit_new_transaction_with_same_max_fee_per_gas_when_price_increased`.

Canister-http mocking is rebuilt around PocketIC's `get_canister_http()`
/ `mock_canister_http_response()`, preserving the legacy `"Http body
exceeds size limit of {n} bytes."` reject that the minter's
response-size check matches on.

`tests/dump_stable_memory.rs` and `tests/deposit_from_cex_demo.rs` are
untouched.

## Also included: dropping the StateMachine fixture from the
orchestrator

This was meant to be a separate PR (dfinity#10956) reviewed on top of this one.
It was marked merged by mistake — see [the note
there](dfinity#10956 (comment))
— and since GitHub cannot reopen a merged PR, its content stays here
rather than being shuffled again.

With the ckETH minter's tests on PocketIC, nothing in `rs/ethereum` uses
the orchestrator's StateMachine-based fixture, so it is deleted along
with the now-unused `ic-state-machine-tests`, `ic-types`,
`ic-management-canister-types-private` and `assert_matches`
dependencies. The environment-agnostic helpers the `pocket_ic` module
and cketh still import (wasm loaders, init-arg builders,
`usdc()`/`usdt()`, constants) stay at their existing paths. After this
PR, no Rust target under `rs/ethereum` compiles against
`ic-state-machine-tests`.

## Behavioural differences PocketIC forces, and how they are handled

PocketIC differs from StateMachine in two ways the tests are sensitive
to. Both are handled in the fixture, in one place each, rather than
worked around per test.

**Time advances kill the outcalls in flight.** PocketIC fails
canister-http requests once they cross `CANISTER_HTTP_TIMEOUT_INTERVAL`;
StateMachine left them pending indefinitely. The requests also stay
listed until a round processes the failures, so a stub could bind to a
request that was already dead and see its response discarded. Worse, a
scraping cycle awaiting such a request keeps holding its `TimerGuard`,
so the firing due at the new time is dropped as `AlreadyProcessing` and
the query the test expects is never issued. `CkEthSetup::advance_time`
therefore fails those outcalls up front and deterministically, doing
what the advance would have done anyway. It is a no-op when nothing is
in flight, because a fixture that has executed no round is expected to
trigger the first scraping cycle itself.

**A stop request takes effect only in the following round.** The
outcalls the minter's timers issue in that last running round become
visible after it, so `stop_minter` ticks once before draining them.

The block number stubs also now match on `["finalized", false]`, which
is what the scraping cycle actually queries — unconstrained, they could
be consumed by the block height refresh timer's query for the latest
block.

## Runtime

Measured against master with identical in-test instrumentation on both
sides: **+5% (ckETH) and +11% (ckERC20)** of wall-clock at default
parallelism (+15%/+20% single-threaded). Fixture-light tests are
indistinguishable from master; the difference tracks the number of IC
rounds a test drives, since PocketIC runs out of process. The dominant
cost in both harnesses is tests that rebuild the whole fixture several
times within one `#[test]`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant