From 3d3e6240f423f06bcddbfcb7aba8a6fc4b13efd5 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 22 Apr 2026 10:16:30 +0200 Subject: [PATCH 1/2] fix(integrity): dedupe resolved packageIds at the source When multiple mops.toml dependency aliases (e.g. `base`, `base@0`, `base@0.16`) pin to the same `name@version`, `getResolvedMopsPackageIds` returned a list with duplicates, while `mops.lock`'s `hashes` map is naturally keyed by packageId. The integrity count check then failed with "Mismatched number of resolved packages" on otherwise-valid projects. Dedupe inside `getResolvedMopsPackageIds` so callers (registry queries and lockfile checks) see one entry per packageId. This also avoids sending duplicate ids to `getFileHashesByPackageIds` on the canister. Add a regression test fixture pinning `core` and `core@1` to the same version; without the fix `mops install` exits 1 with the mismatch error. Fixes #506. Made-with: Cursor --- cli/CHANGELOG.md | 1 + cli/integrity.ts | 7 ++++++- cli/tests/cli.test.ts | 21 +++++++++++++++++++++ cli/tests/install/aliases/mops.toml | 3 +++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 cli/tests/install/aliases/mops.toml diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 7d0b5e8c..25b7e7e1 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,6 +1,7 @@ # Mops CLI Changelog ## Next +- Fix `mops install` (and any `--lock check` flow) failing with "Mismatched number of resolved packages" when a project's resolved dependencies include multiple aliases (e.g. `base`, `base@0`, `base@0.16`) that pin to the same `name@version` ## 2.12.1 - `mops check`/`build`/`check-stable` skip migration staging when only the pending `next` migration is needed, so `moc` diagnostics reference the real `next-migration/` path. diff --git a/cli/integrity.ts b/cli/integrity.ts index 181f223d..49faea39 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -63,7 +63,12 @@ async function getResolvedMopsPackageIds(): Promise { let packageIds = Object.entries(resolvedPackages) .filter(([_, version]) => getDependencyType(version) === "mops") .map(([name, version]) => getPackageId(name, version)); - return packageIds; + // resolvedPackages is keyed by raw mops.toml dependency keys, so aliases + // like `base`, `base@0`, `base@0.16` survive as separate entries. Once + // collapsed into packageId via getDepName, they can produce duplicates + // when multiple aliases pin to the same name@version. Dedupe so callers + // (registry queries, lockfile checks) see one entry per packageId. + return [...new Set(packageIds)]; } // get hash of local file from '.mops' dir by fileId diff --git a/cli/tests/cli.test.ts b/cli/tests/cli.test.ts index 5dfc6998..132746ce 100644 --- a/cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -71,4 +71,25 @@ describe("install", () => { // mops add/remove/update/sync are not separately tested here because they // all route through the same checkIntegrity code path tested above. + + // Regression: aliases pinning the same package@version (e.g. `core` and + // `core@1` both at "1.0.0") inflated the resolved-packageIds count and + // tripped the lockfile integrity check with a spurious + // "Mismatched number of resolved packages" error. See issue #506. + test("integrity check passes when aliases resolve to the same package@version", async () => { + const cwd = path.join(import.meta.dirname, "install/aliases"); + const lockFile = path.join(cwd, "mops.lock"); + rmSync(lockFile, { force: true }); + try { + const result = await cli(["install"], { cwd, env: { CI: undefined } }); + expect(result.stderr).not.toMatch( + /Mismatched number of resolved packages/, + ); + expect(result.exitCode).toBe(0); + expect(existsSync(lockFile)).toBe(true); + } finally { + rmSync(lockFile, { force: true }); + rmSync(path.join(cwd, ".mops"), { recursive: true, force: true }); + } + }); }); diff --git a/cli/tests/install/aliases/mops.toml b/cli/tests/install/aliases/mops.toml new file mode 100644 index 00000000..ae55b5dc --- /dev/null +++ b/cli/tests/install/aliases/mops.toml @@ -0,0 +1,3 @@ +[dependencies] +core = "1.0.0" +"core@1" = "1.0.0" From 4c688af6e67df7c7d294505ab815a0d8bc16f41f Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Wed, 22 Apr 2026 10:17:50 +0200 Subject: [PATCH 2/2] simplify comment Made-with: Cursor --- cli/integrity.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cli/integrity.ts b/cli/integrity.ts index 49faea39..d564e453 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -63,11 +63,7 @@ async function getResolvedMopsPackageIds(): Promise { let packageIds = Object.entries(resolvedPackages) .filter(([_, version]) => getDependencyType(version) === "mops") .map(([name, version]) => getPackageId(name, version)); - // resolvedPackages is keyed by raw mops.toml dependency keys, so aliases - // like `base`, `base@0`, `base@0.16` survive as separate entries. Once - // collapsed into packageId via getDepName, they can produce duplicates - // when multiple aliases pin to the same name@version. Dedupe so callers - // (registry queries, lockfile checks) see one entry per packageId. + // dedupe: aliases like `base@0`, `base@0.16` collapse to the same packageId return [...new Set(packageIds)]; }