Files
pnpm/lockfile/fs/test/lockfileName.test.ts
Abdullah Alaqeel 61969fbddf fix(deps-status): detect lockfile-only changes (#12106)
## Summary

Fixes `pnpm install` with `optimisticRepeatInstall` incorrectly returning `Already up to date` when `pnpm-lock.yaml` changed but project manifests did not.

Fixes #12100.

## Root Cause

`checkDepsStatus` used modified manifest mtimes as the only signal for whether it needed to validate dependency status. If no manifest was newer than `workspaceState.lastValidatedTimestamp`, it returned `upToDate: true` before checking whether the wanted lockfile had changed.

That skipped lockfile validation for workflows like:

- `git checkout HEAD~1 -- pnpm-lock.yaml`
- restoring only `pnpm-lock.yaml` from a stash
- external tools rewriting the lockfile without touching manifests

## Changes

- Check wanted lockfile mtimes before taking the optimistic fast path.
- If any wanted lockfile is missing or newer than the workspace state timestamp, validate all projects instead of only modified manifests.
- Add a regression test proving a lockfile-only change does not skip wanted-lockfile validation.
- Add a patch changeset for `@pnpm/deps.status` and `pnpm`.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 22:04:07 +00:00

39 lines
1.7 KiB
TypeScript

import { afterEach, describe, expect, jest, test } from '@jest/globals'
import { WANTED_LOCKFILE } from '@pnpm/constants'
jest.unstable_mockModule('@pnpm/network.git-utils', () => ({ getCurrentBranch: jest.fn() }))
const { getCurrentBranch } = await import('@pnpm/network.git-utils')
const { getWantedLockfileName } = await import('../lib/lockfileName.js')
describe('lockfileName', () => {
afterEach(() => {
jest.mocked(getCurrentBranch).mockReset()
})
test('returns default lockfile name if useGitBranchLockfile is off', async () => {
await expect(getWantedLockfileName()).resolves.toBe(WANTED_LOCKFILE)
})
test('returns git branch lockfile name', async () => {
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('main'))
await expect(getWantedLockfileName({ useGitBranchLockfile: true })).resolves.toBe('pnpm-lock.main.yaml')
})
test('returns git branch lockfile name when git branch contains clashes', async () => {
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('a/b/c'))
await expect(getWantedLockfileName({ useGitBranchLockfile: true })).resolves.toBe('pnpm-lock.a!b!c.yaml')
})
test('returns git branch lockfile name when git branch contains uppercase', async () => {
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('aBc'))
await expect(getWantedLockfileName({ useGitBranchLockfile: true })).resolves.toBe('pnpm-lock.abc.yaml')
})
test('passes cwd to getCurrentBranch', async () => {
jest.mocked(getCurrentBranch).mockReturnValue(Promise.resolve('main'))
await getWantedLockfileName({ useGitBranchLockfile: true, cwd: '/some/workspace' })
expect(jest.mocked(getCurrentBranch)).toHaveBeenCalledWith({ cwd: '/some/workspace' })
})
})