Skip to content

feat(pic): add ttl option to PocketIcServer.start - #278

Merged
adamspofford-dfinity merged 2 commits into
dfinity:mainfrom
Kamirus:feat/pocket-ic-server-ttl
Jul 29, 2026
Merged

feat(pic): add ttl option to PocketIcServer.start#278
adamspofford-dfinity merged 2 commits into
dfinity:mainfrom
Kamirus:feat/pocket-ic-server-ttl

Conversation

@Kamirus

@Kamirus Kamirus commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The pocket-ic server shuts down after its default 60 second idle TTL. Test runs that interleave slow work between replica interactions — compiling canisters mid-run, for example — can exceed that window, and the server dies before the next request. This adds a ttl option to StartServerOptions that passes --ttl <seconds> to the spawned process; when omitted, no flag is passed and the server default applies as before.

Follow-up to #276.

Passes --ttl <seconds> to the spawned pocket-ic process to control how
long the server stays alive after the last completed operation. When
omitted, no flag is passed and the server default applies.
Copilot AI review requested due to automatic review settings July 29, 2026 07:51
@Kamirus
Kamirus requested a review from a team as a code owner July 29, 2026 07:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional server TTL configuration to PocketIcServer.start() so consumers can extend PocketIC’s idle shutdown window for longer-running test flows (e.g., compiling canisters mid-run) without changing the default behavior when the option is omitted.

Changes:

  • Add ttl?: number to StartServerOptions.
  • Forward ttl to the spawned PocketIC process as --ttl <seconds> when provided.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/pic/src/pocket-ic-server.ts Builds the PocketIC spawn argument list and conditionally appends --ttl based on options.
packages/pic/src/pocket-ic-server-types.ts Extends StartServerOptions with a documented ttl field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/pic/src/pocket-ic-server.ts
Comment thread packages/pic/src/pocket-ic-server.ts
Rejects NaN, Infinity, floats and negatives with an InvalidTtlError
instead of passing an invalid --ttl arg to the pocket-ic process.
@adamspofford-dfinity
adamspofford-dfinity added this pull request to the merge queue Jul 29, 2026
Merged via the queue into dfinity:main with commit 6cc935c Jul 29, 2026
15 checks passed
@lwshang lwshang mentioned this pull request Aug 3, 2026
Kamirus added a commit to caffeinelabs/mops that referenced this pull request Aug 4, 2026
…#642)

Closes #561.

`pocket-ic` 9.0.0 and newer now runs through upstream `@dfinity/pic`
0.23.0. Nothing changes for anyone on a supported pin: the same
toolchain-managed binary starts with the same `--ttl`, and installing
the CLI still downloads no extra binary. Pins below 9.0.0 keep working
but warn, and stop working in v3.

## Why replace the fork

`pic-js-mops` has no public source repository. It is published to npm
from a local checkout of `dfinity/pic-js` carrying four patches, so the
only way to audit it is diffing the published tarball against upstream,
and exactly one person can publish a fix.

Two of those patches are now upstream and shipped together in 0.23.0:
[dfinity/pic-js#276](dfinity/pic-js#276) for an
explicit binary path and
[dfinity/pic-js#278](dfinity/pic-js#278) for a
server `--ttl`. The third needs no upstream change — `serverProcess` is
TS-private but present at runtime, so the client seam casts through it
for canister-log streaming. The fourth was a stripped postinstall,
handled below.

## Why it is a devDependency, not a dependency

`@dfinity/pic` downloads a ~94 MB pocket-ic binary in a postinstall that
throws when it cannot. The published `ic-mops` package is the unbundled
`dist/` tree carrying the full dependency list — the bun bundle is a
separate artifact that only `cli.mops.one` installs — so a
`dependencies` entry reaches every user. Installing into a throwaway
prefix with egress blocked:

```
# @dfinity/pic in dependencies
npm error TypeError: fetch failed … connect EPERM 140.82.121.3:443
npm error     at downloadPicBinary (postinstall.mjs:69)
exit 1

# pre-bundled devDependency
added 368 packages in 3s
exit 0
```

mops manages that binary itself via `[toolchain] pocket-ic`, so the
download was never wanted. Three pieces replace it:
`vendor/pic-entry.mjs` re-exports `PocketIc` and `PocketIcServer`
explicitly, because pic ships CJS and a plain `export *` through esbuild
yields no named exports at all; `vendor:pic` bundles that to
`dist/vendor/pic.mjs` (1.1 MB); and `fix-dist` repoints the dynamic
import, throwing if it is ever renamed. `import("pic-ic")` is
deliberately left alone.

Removable later — if pic makes the download lazy, as [suggested during
review of
#276](dfinity/pic-js#276 (comment)),
all three pieces and their smoke test go away.

## Deprecating pins below 9.0.0

Before, a `pocket-ic = "4.0.0"` pin ran with no notice. After:

```
$ mops test --mode replica
`pocket-ic` is pinned to 4.0.0 in `[toolchain]`. Support for `pocket-ic` below 9.0.0 is deprecated and will be removed in mops v3.
Run `mops toolchain use pocket-ic 12.0.0` to move to a supported version and silence this warning.
 PASS
Tests passed
Done in 3.06s, passed 1
```

The warning fires where the legacy client is selected rather than per
command, so it cannot drift from the selection logic and prints once per
run.

One subtlety worth calling out: `addCycles` takes a `number` on `pic-ic`
and a `bigint` upstream. The intersection cast that reconciled the two
old clients would still compile here and then hand a `number` to the
modern client at runtime, so the seam exports a wrapper that branches on
the same pin that picked the client.

## Migration

Bump the pin: `mops toolchain use pocket-ic 12.0.0`.

The affected set is narrower than the warning implies. `pic-ic@0.5.4`
only speaks the 4.0.0 server protocol, so 5.x–8.x pins already fail with
`BinTimeoutError` on `main` today — only 4.0.0 and 9.x+ ever worked,
matching the range documented in
[#412](#412). In practice this
affects 4.0.0 pins.

## `@icp-sdk/core` stays on 4.0.2

pic depends on `@icp-sdk/core` `^5`, but as a plain dependency, not a
peer — so it does not dictate mops's version. mops stays on **4.0.2**
and the vendored bundle carries pic's own `5.x` inside it.

That pin is load-bearing, not incidental. `@icp-sdk/core` 5.x drops the
IC HTTP API `v2` endpoints, and `v2` is the only version the `dfx` and
`dfx-pocket-ic` replicas serve — dfx 0.29.1 answers `/api/v4/…/call`
with a 400 and an HTML error page, and 404s `v3` query/`read_state`. The
SDK's fallback keys on 404 for `v4` only, so it never fires, and
`query`/`read_state` have no fallback at all. Bumping mops to 5.x
therefore breaks every replica test and benchmark in a project with no
`pocket-ic` pin, which is how the unpinned path is still wired. That is
also what made CI red on an earlier revision of this branch.

Two copies are safe here because nothing crosses the seam: `idlFactory`
is the caller-injected `({ IDL }) => …` form, so pic supplies its own
`IDL`, and the `canisterId` pic returns only ever goes back into pic or
through `.toText()`. Worth keeping in mind for future edits — handing a
mops-constructed `Principal` to pic would be the way to break it.
Keeping 4.0.2 also realigns the CLI with `frontend/`, and leaves the SDK
bump with the dfx-replica removal in v3, where it belongs.

## What is unchanged

Supported pins behave identically — same binary path, same `--ttl`, same
output. Unpinned projects still fall back to the deprecated dfx replica
with its existing warning. `pic-ic` itself is untouched. The bundled
`cli.mops.one` distribution needed no changes, since it already inlines
pic from `node_modules`.

Verified against a packed CLI in a throwaway prefix rather than a dev
checkout, across all three replica paths: the root unpinned `mops test`
sweep passes all 18 files on the dfx replica; `pocket-ic = "12.0.0"`
passes `mops test --mode replica` and `mops bench` spawning
`<cache>/pocket-ic/12.0.0/pocket-ic --port-file … --ttl 60` with no
warning and no binary downloaded at install; `pocket-ic = "4.0.0"`
passes the replica test with the warning above, spawning through
`pic-ic`'s older `--pid` style. Legacy `mops bench` is confirmed only as
far as a successful client-to-server round trip — a 2024-era replica
rejects modern moc output, and older moc cannot parse `persistent
actor`.

## Follow-ups worth flagging

- A clean `npm install` in `cli/` still pulls the 94 MB binary, because
devDependency install scripts run for local dev. The lazy-download
change upstream fixes that too.
- Bundling pic's `@icp-sdk/core` costs ~500 KB in `dist/vendor/pic.mjs`.
It collapses back to one copy when v3 drops the dfx replica and mops can
move to 5.x itself.
- v3 removes `pic-ic`, the `AnyPocketIc*` union types it forces, and
turns this warning into a hard error alongside an enforced supported
server range.
- Out of scope: `files: ["*"]` ships both distributions inside the npm
tarball, roughly doubling it. Tracked in `TODO.md`.

## Also here: v3 plan rescope

`NEXT-MAJOR.md` is now split into a committed v3 scope and a deferred
section, with the reasoning recorded inline:

- **The version model stays as it is.** Bare `1.2.3` remains exact,
conflicts stay max-wins, freshness keeps coming from caret-bounded `mops
update`. Cargo's model needs `=` pins as an escape hatch, and `=` in
*published* packages requires backend validator changes plus a window
where older CLIs cannot parse what newer ones publish — caret without
the escape hatch is the worst of both. That track moves to a future
major, gated on moc `--override` so multi-version coexistence lands with
it. v3 keeps the pure wins: a real semver comparator instead of
`parseInt`, and a cross-major conflict warning that always fires.
- **`--locked`, not a `mops ci` command.** npm's separate command is the
ecosystem outlier; cargo, pnpm and yarn all use a flag, and a flag
composes with the commands that resolve implicitly. `--lock` then
disappears entirely: `check` becomes `--locked`, `ignore` has no
successor, and `update` becomes self-healing on a corrupt lock.
- **Lockfile v4 leaves the major.** Pinning git deps by commit SHA needs
a format bump, but cargo shipped v1 through v4 while staying on 1.x by
making formats readable long before writing them.
- **dfx keeps one explicit escape hatch.** Every implicit fallback goes;
`--replica dfx` stays until our own dev loop is on icp-cli.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants