Skip to content

feat(pic): support overriding the PocketIC binary path - #276

Merged
adamspofford-dfinity merged 3 commits into
dfinity:mainfrom
Kamirus:feat/pocket-ic-bin-env
Jul 28, 2026
Merged

feat(pic): support overriding the PocketIC binary path#276
adamspofford-dfinity merged 3 commits into
dfinity:mainfrom
Kamirus:feat/pocket-ic-bin-env

Conversation

@Kamirus

@Kamirus Kamirus commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Mops currently uses a fork of this repo as the main dependency for the pocket ic.
I wanted to upstream the changes that we need in mops so that we could switch to this dependency directly without a fork.

PocketIcServer.start() resolves the server binary to a fixed path inside the package directory, so the binary cannot be supplied externally — for example when a tool manages pocket-ic versions itself, or when the library is bundled and __dirname no longer points at the package. This adds a binPath option to StartServerOptions, falling back to the POCKET_IC_BIN environment variable; when neither is set, the binary downloaded at install time is used as before. The Rust and Python PocketIC clients both already support POCKET_IC_BIN.

Copilot AI review requested due to automatic review settings July 28, 2026 09:54
@Kamirus
Kamirus requested a review from a team as a code owner July 28, 2026 09:54
@cla-idx-bot

cla-idx-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

Dear @Kamirus,

In order to potentially merge your code in this open-source repository and therefore proceed with your contribution, we need to have your approval on DFINITY's CLA.

If you decide to agree with it, please visit this issue and read the instructions there. Once you have signed it, re-trigger the workflow on this PR to see if your code can be merged.

— The DFINITY Foundation

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 support for resolving the PocketIC server binary from a caller-supplied path or environment variable, improving compatibility with external version managers and bundled environments where the packaged binary path may not be usable.

Changes:

  • Add binPath to StartServerOptions and resolve the binary path in priority order: binPathPOCKET_IC_BIN → packaged binary.
  • Skip downloading the packaged PocketIC binary during install when POCKET_IC_BIN is set.
  • Add tests and documentation describing the new configuration options.

Reviewed changes

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

Show a summary per file
File Description
packages/pic/src/pocket-ic-server.ts Adds runtime binary path resolution logic to support overrides.
packages/pic/src/pocket-ic-server-types.ts Exposes the new binPath option in the public options type with docs.
packages/pic/postinstall.mjs Skips binary download when an external binary is configured via env var.
packages/pic/tests/src/pocket-ic-server.spec.ts Adds unit tests covering override precedence and env var behavior.
docs/src/content/docs/guides/running-tests.mdx Documents how to configure the PocketIC binary for test runs.

💡 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
Comment thread packages/pic/postinstall.mjs Outdated
Comment thread docs/src/content/docs/guides/running-tests.mdx
Comment thread packages/pic/src/pocket-ic-server-types.ts
The server binary can now be provided externally via the binPath option
or the POCKET_IC_BIN environment variable, matching the Rust and Python
PocketIC clients. When POCKET_IC_BIN is set at install time, the install
script skips downloading the binary. The downloaded binary remains the
default.
@Kamirus
Kamirus force-pushed the feat/pocket-ic-bin-env branch from 73eb667 to 2d755d6 Compare July 28, 2026 10:06
Comment thread docs/src/content/docs/guides/running-tests.mdx Outdated
Drop the install-time download skip: a POCKET_IC_BIN value set at
install time (potentially by an unrelated context) would leave the
package without a binary to fall back to at runtime.
@Kamirus
Kamirus requested a review from mraszyk July 28, 2026 13:22
@adamspofford-dfinity
adamspofford-dfinity added this pull request to the merge queue Jul 28, 2026
Merged via the queue into dfinity:main with commit 09dd36a Jul 28, 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 3, 2026
`pocket-ic` 9.0.0 and newer now runs through upstream `@dfinity/pic`
0.23.0 instead of the `pic-js-mops` fork. That fork has no public source
repository — it is published from a local checkout of `dfinity/pic-js`
with four patches, so the only way to audit it is diffing the published
tarball, and one person can publish a fix. Two of the patches are now
upstream (dfinity/pic-js#276 for an explicit binary path, #278 for a
server `--ttl`), the third needs no upstream change (`serverProcess` is
TS-private but present at runtime, so the client seam casts), and the
fourth is handled here.

`@dfinity/pic` stays a devDependency, pre-bundled into `dist/vendor/`:
its postinstall downloads a ~94 MB pocket-ic binary and throws when it
cannot, and the published CLI is the unbundled `dist/` tree with the full
dependency list, so a `dependencies` entry would make `npm i -g ic-mops`
pull that binary and fail outright with no network. mops manages the
binary itself via `[toolchain] pocket-ic`. Removable later: if pic makes
the download lazy, this becomes an ordinary dependency again.

Pins below 9.0.0 keep working through the unchanged `pic-ic` client but
now warn, and go away in v3 — the same deprecate-then-drop path used for
the dfx replica and vessel. `addCycles` gets a wrapper rather than a
cast, because it takes a `number` on `pic-ic` and a `bigint` upstream and
an intersection type would compile while feeding the wrong one at
runtime.

Verified with a packed CLI installed into a throwaway prefix: with
`pocket-ic = "12.0.0"` both `mops test --mode replica` and `mops bench`
pass, spawning the toolchain-managed binary with `--ttl`, and the install
downloads no binary; with `pocket-ic = "4.0.0"` the replica test still
passes and warns. `@icp-sdk/core` moves to 5.4.0 as a single deduped copy
(pic wants ^5, and 6.x would nest a second copy and give two
`Principal`/`IDL` identities).
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.

4 participants