From 5d40e97f38ea9ba9f64c76575aa78b45aebbe127 Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Wed, 22 Apr 2026 22:37:04 +0200 Subject: [PATCH 1/3] fix(integrity): `--lock update` must regenerate on corrupt lockfile `updateLockFile` early-returned whenever `checkLockFileLight` passed, i.e. whenever `mopsTomlDepsHash` matched. That light check does not validate the per-file hashes in `mops.lock`, so a lockfile with a corrupt file-level hash (e.g. one written by a pre-2.12.2 CLI, a partial/crashed write, or a manual edit) could never be repaired via `mops install --lock update`: the update no-op'd, the follow-up `checkLockFile` then failed on the same corrupt hash and exited 1. The only recovery was `rm mops.lock`. Thread the existing `force` flag from `checkIntegrity` into `updateLockFile`. When the user explicitly passes `--lock update`, skip the light-check shortcut and unconditionally regenerate. Default `mops install` (no flag) still hits the fast path. Add a regression test that corrupts one per-file hash and asserts `install --lock update` rewrites the lock to a non-corrupt state. Fixes #514. --- cli/CHANGELOG.md | 1 + cli/integrity.ts | 8 +++++--- cli/tests/cli.test.ts | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 968ef137..f5b490ad 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,6 +1,7 @@ # Mops CLI Changelog ## Next +- Fix `mops install --lock update` silently no-op'ing on a corrupt lockfile (#515) ## 2.12.2 - 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` diff --git a/cli/integrity.ts b/cli/integrity.ts index d564e453..23cf5543 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -41,7 +41,7 @@ export async function checkIntegrity(lock?: "check" | "update" | "ignore") { } if (lock === "update") { - await updateLockFile(); + await updateLockFile({ force }); await checkLockFile(force); } else if (lock === "check") { await checkLockFile(force); @@ -159,9 +159,11 @@ export function checkLockFileLight(): boolean { return false; } -export async function updateLockFile() { +export async function updateLockFile({ force = false }: { force?: boolean } = {}) { // if lock file exists and mops.toml hasn't changed, don't update it - if (checkLockFileLight()) { + // (unless forced: `--lock update` must unconditionally regenerate so users + // can recover from a corrupt lockfile without `rm mops.lock`) + if (!force && checkLockFileLight()) { return; } diff --git a/cli/tests/cli.test.ts b/cli/tests/cli.test.ts index 132746ce..ca956616 100644 --- a/cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -1,5 +1,5 @@ import { describe, expect, jest, test } from "@jest/globals"; -import { existsSync, rmSync } from "node:fs"; +import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import path from "path"; import { cli } from "./helpers"; @@ -92,4 +92,39 @@ describe("install", () => { rmSync(path.join(cwd, ".mops"), { recursive: true, force: true }); } }); + + // Regression: `install --lock update` used to early-return if mops.toml's + // deps hash was unchanged, even when the lockfile's per-file hashes were + // stale/corrupt. The subsequent checkLockFile would then fail and exit 1, + // so `--lock update` could never recover a broken lock — the only escape + // was `rm mops.lock`. See issue #514. + test("--lock update rewrites a lockfile with a corrupt file hash", async () => { + const cwd = path.join(import.meta.dirname, "install/success"); + const lockFile = path.join(cwd, "mops.lock"); + rmSync(lockFile, { force: true }); + try { + const first = await cli(["install"], { cwd, env: { CI: undefined } }); + expect(first.exitCode).toBe(0); + expect(existsSync(lockFile)).toBe(true); + + const bad = "BAD0000000000000000000000000000000000000000000000000000000000BAD"; + const original = readFileSync(lockFile, "utf8"); + const corrupted = original.replace( + /"core@1\.0\.0\/mops\.toml":\s*"[0-9a-f]{64}"/, + `"core@1.0.0/mops.toml": "${bad}"`, + ); + expect(corrupted).not.toBe(original); + writeFileSync(lockFile, corrupted); + + const result = await cli(["install", "--lock", "update"], { + cwd, + env: { CI: undefined }, + }); + expect(result.exitCode).toBe(0); + expect(readFileSync(lockFile, "utf8")).not.toContain(bad); + } finally { + rmSync(lockFile, { force: true }); + rmSync(path.join(cwd, ".mops"), { recursive: true, force: true }); + } + }); }); From 16ce5564605cd1053c1f805cdb03bc412409ef70 Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Wed, 22 Apr 2026 22:55:13 +0200 Subject: [PATCH 2/3] fix(lint): prettier formatting --- cli/integrity.ts | 4 +++- cli/tests/cli.test.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/integrity.ts b/cli/integrity.ts index 23cf5543..a44ddb6e 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -159,7 +159,9 @@ export function checkLockFileLight(): boolean { return false; } -export async function updateLockFile({ force = false }: { force?: boolean } = {}) { +export async function updateLockFile({ + force = false, +}: { force?: boolean } = {}) { // if lock file exists and mops.toml hasn't changed, don't update it // (unless forced: `--lock update` must unconditionally regenerate so users // can recover from a corrupt lockfile without `rm mops.lock`) diff --git a/cli/tests/cli.test.ts b/cli/tests/cli.test.ts index ca956616..bb9c09b8 100644 --- a/cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -107,7 +107,8 @@ describe("install", () => { expect(first.exitCode).toBe(0); expect(existsSync(lockFile)).toBe(true); - const bad = "BAD0000000000000000000000000000000000000000000000000000000000BAD"; + const bad = + "BAD0000000000000000000000000000000000000000000000000000000000BAD"; const original = readFileSync(lockFile, "utf8"); const corrupted = original.replace( /"core@1\.0\.0\/mops\.toml":\s*"[0-9a-f]{64}"/, From c698b73ffb0c375e624d79c6d53e2e3d1d8a40ad Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Thu, 23 Apr 2026 09:53:43 +0200 Subject: [PATCH 3/3] fix(integrity): suggest `--lock update` recovery when integrity check fails When `checkLockFile` exits on a per-file hash mismatch, point users at the now-working `--lock update` recovery flag. Without this hint, users hitting #514 had no in-product signal that the escape hatch existed. Also document the recovery use case in `mops install --lock` docs. Made-with: Cursor --- cli/integrity.ts | 5 +++++ docs/docs/cli/1-deps/02-mops-install.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/integrity.ts b/cli/integrity.ts index a44ddb6e..123a20e9 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -317,6 +317,11 @@ export async function checkLockFile(force = false) { console.error(`Mismatched hash for ${fileId}`); console.error(`Locked hash: ${lockedHash}`); console.error(`Actual hash: ${localHash}`); + console.error(""); + console.error( + "If you have not modified files under .mops/, your lockfile may be stale or corrupt.", + ); + console.error("Run `mops install --lock update` to regenerate it."); process.exit(1); } } diff --git a/docs/docs/cli/1-deps/02-mops-install.md b/docs/docs/cli/1-deps/02-mops-install.md index f23731fb..7892274b 100644 --- a/docs/docs/cli/1-deps/02-mops-install.md +++ b/docs/docs/cli/1-deps/02-mops-install.md @@ -26,7 +26,7 @@ See [mops.lock](/mops.lock) for details on lockfile contents and when to commit What to do with the [lockfile](/mops.lock). Possible values: -- `update` — keep the lockfile in sync with current dependencies and verify file integrity (default) +- `update` — keep the lockfile in sync with current dependencies and verify file integrity (default). Pass explicitly to force regeneration if the lockfile is stale or corrupt. - `check` — verify file integrity against an existing lockfile; fail if the lockfile is missing or out of date - `ignore` — skip the lockfile entirely