feat(pic): support overriding the PocketIC binary path - #276
Conversation
|
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 |
There was a problem hiding this comment.
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
binPathtoStartServerOptionsand resolve the binary path in priority order:binPath→POCKET_IC_BIN→ packaged binary. - Skip downloading the packaged PocketIC binary during install when
POCKET_IC_BINis 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.
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.
73eb667 to
2d755d6
Compare
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.
`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).
…#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.
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__dirnameno longer points at the package. This adds abinPathoption toStartServerOptions, falling back to thePOCKET_IC_BINenvironment variable; when neither is set, the binary downloaded at install time is used as before. The Rust and Python PocketIC clients both already supportPOCKET_IC_BIN.