Files
renovate/lib/modules/manager/mise/artifacts.spec.ts
jdx 94f0219613 feat(manager/mise): run mise lock in safe mode without allowedUnsafeExecutions (#44749)
* feat(manager/mise): run mise lock in safe mode without allowedUnsafeExecutions

When `constraints.mise` pins a version that supports mise safe mode
(`MISE_SAFE=1`), run `mise lock` with safe mode enabled instead of
requiring `mise` in `allowedUnsafeExecutions`. Safe mode is a hard
boundary against project config executing code (templates exec/read_file,
_.source, hooks, tasks, asdf scripts, plugin installs are all refused),
so it — not the allowlist — protects the host.

The existing allowlisted path is unchanged: allowlisted repos run
`mise lock` exactly as before, without forcing safe mode, so configs
that legitimately execute code during locking keep working.

Safe mode also removes the need to pre-install node/npm/golang/ruby as
resolver tools, since npm version listing uses mise's built-in registry
HTTP client and go runs with GOTOOLCHAIN=local.

Refs: jdx/mise#11146 (safe mode), jdx/mise#11145 (lock --bump)

* fix: address oxlint and pin safe-mode min version to 2026.7.12

- logger.warn requires a static message with interpolated values in the
  metadata object (oxlint rule); move the version into metadata
- set MISE_SAFE_MODE_MIN_VERSION to 2026.7.12, the expected first release
  containing safe mode (merged after v2026.7.11)

* feat(manager/mise): use mise lock --bump for lock file maintenance

lockFileMaintenance means "update to the latest versions", but plain
`mise lock` only refreshes metadata for already-locked versions. Use
`mise lock --bump` so fuzzy selectors (e.g. node = "22") advance to the
latest matching version. `--bump` requires the same minimum mise version
as safe mode, so it is gated on the mise constraint and falls back to a
plain refresh otherwise.

Generalizes the version-gate helper (miseConstraintGuarantees) shared by
safe mode and --bump.

* refactor(manager/mise): detect mise version at runtime instead of via constraints

Determine safe-mode / --bump support by running `mise version` (which
does not load or execute project config) rather than requiring a pinned
`constraints.mise`. This works off whatever mise is actually installed,
so no extra configuration is needed.

The probe is skipped on the allowlisted, non-maintenance path, where the
version cannot change the outcome — keeping that hot path unchanged.

* feat(manager/mise): skip mise trust in safe mode

Safe mode now makes an untrusted config fully inert and no longer
requires trust to load it (jdx/mise#11151), so on the safe-mode path
`mise trust` is unnecessary and is skipped — `mise lock` runs with
`MISE_SAFE=1` alone. The allowlisted path is unchanged and still runs
`mise trust`.

* feat(manager/mise): confirm safe mode min version against v2026.7.12 release

MISE_SAFE=1 safe mode and `mise lock --bump` first shipped in mise
v2026.7.12 (released 2026-07-23), confirming the assumed
MISE_SAFE_MODE_MIN_VERSION. Resolve the pending-release TODO and align
the test version fixture with the real release date.

Validated end-to-end against the real v2026.7.12 binary: `mise version`
output matches the parser, `MISE_SAFE=1 mise lock` refreshes an untrusted
config without trust, `mise lock --bump` advances fuzzy selectors, and
project [env] is ignored under safe mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(manager/mise): cover mise version probe failure path

Add a test for the detectMiseVersion catch branch (exec throwing), so the
non-allowlisted path falls back to conservative behavior. Restores 100%
patch coverage for the mise manager.

* Update lib/modules/manager/mise/readme.md

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>

* Update lib/modules/manager/mise/readme.md

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>

* docs(mise): fix safe mode section link

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
2026-07-27 06:26:22 +00:00

783 lines
23 KiB
TypeScript

import upath from 'upath';
import { mockDeep } from 'vitest-mock-extended';
import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util.ts';
import { env, fs, hostRules } from '~test/util.ts';
import { GlobalConfig } from '../../../config/global.ts';
import type {
InternalGlobalConfigOptions,
RepoGlobalConfig,
} from '../../../config/types.ts';
import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts';
import * as docker from '../../../util/exec/docker/index.ts';
import * as _datasource from '../../datasource/index.ts';
import type { UpdateArtifactsConfig } from '../types.ts';
import { updateArtifacts } from './artifacts.ts';
import * as lockfile from './lockfile.ts';
import { updateLockedDependency } from './update-locked.ts';
const datasource = vi.mocked(_datasource);
vi.mock('../../datasource/index.ts', () => mockDeep());
vi.mock('../../../util/exec/env.ts');
vi.mock('../../../util/fs/index.ts');
const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = {
// `join` fixes Windows CI
localDir: upath.join('/tmp/github/some/repo'),
cacheDir: upath.join('/tmp/renovate/cache'),
containerbaseDir: upath.join('/tmp/renovate/cache/containerbase'),
binarySource: 'global',
allowedUnsafeExecutions: ['mise'],
};
process.env.CONTAINERBASE = 'true';
const config: UpdateArtifactsConfig = {};
const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true };
const trustCmd = 'mise trust mise.toml';
const updateToolCmd = 'mise lock node';
const trustLocalCmd = 'mise trust mise.local.toml';
const trustEnvCmd = 'mise trust mise.test.toml';
const trustEnvLocalCmd = 'mise trust mise.test.local.toml';
const trustSubdirCmd = 'mise trust mise.toml';
const updateMultipleToolsCmd = 'mise lock node python';
const lockfileMaintenanceCmd = 'mise lock';
const miseVersionCmd = 'mise version';
// `mise version` output for a release that supports safe mode / `mise lock --bump`
const safeMiseVersionOutput = {
stdout: '2026.7.12 linux-x64 (2026-07-23)',
stderr: '',
};
describe('modules/manager/mise/artifacts', () => {
beforeEach(() => {
env.getChildProcessEnv.mockReturnValue({
...envMock.basic,
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US',
});
GlobalConfig.set(adminConfig);
docker.resetPrefetchedImages();
hostRules.clear();
});
it('returns null if lock file does not exist', async () => {
fs.readLocalFile.mockResolvedValueOnce(null);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [],
newPackageFileContent: '',
config,
});
expect(res).toBeNull();
expect(execSnapshots).toEqual([]);
});
it('returns null when mise is not allowlisted and version cannot be determined', async () => {
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
fs.readLocalFile.mockResolvedValueOnce('existing content');
// default mock returns empty stdout, so the version probe cannot parse a version
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toBeNull();
expect(execSnapshots).toMatchObject([{ cmd: miseVersionCmd }]);
});
it('runs mise lock with MISE_SAFE when a safe-mode mise version is detected and not allowlisted', async () => {
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll(safeMiseVersionOutput);
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toEqual([
{
file: {
contents: expect.stringContaining('version = "24.16.0"'),
path: 'mise.lock',
type: 'addition',
},
},
]);
// safe mode does not require trust, so `mise trust` is not run
expect(execSnapshots).toMatchObject([
{ cmd: miseVersionCmd },
{
cmd: updateToolCmd,
options: {
env: expect.objectContaining({ MISE_SAFE: '1' }),
},
},
]);
});
it('returns null when mise is not allowlisted and the detected version predates safe mode', async () => {
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
fs.readLocalFile.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll({
stdout: '2026.7.11 linux-x64 (2026-07-20)',
stderr: '',
});
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toBeNull();
expect(execSnapshots).toMatchObject([{ cmd: miseVersionCmd }]);
});
it('falls back to conservative behavior when the version probe throws', async () => {
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
fs.readLocalFile.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecSequence([new Error('mise not found')]);
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
// an unparseable/failed probe means safe mode cannot be guaranteed, so the
// allowlist is still required and locking is skipped
expect(res).toBeNull();
expect(execSnapshots).toMatchObject([{ cmd: miseVersionCmd }]);
});
it('does not set MISE_SAFE or probe the version on the allowlisted path', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{
cmd: updateToolCmd,
options: {
env: expect.not.objectContaining({ MISE_SAFE: '1' }),
},
},
]);
});
it('returns null if lock file unchanged after exec', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toBeNull();
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{ cmd: updateToolCmd },
]);
});
it('returns updated lock file on success', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toEqual([
{
file: {
contents: expect.stringContaining('version = "24.16.0"'),
path: 'mise.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{ cmd: updateToolCmd },
]);
});
it('returns artifactError on exec failure with combined output', async () => {
fs.readLocalFile.mockResolvedValueOnce('existing content');
const error = new Error('exec error');
(error as any).stdout = 'stdout output';
(error as any).stderr = 'stderr output';
const execSnapshots = mockExecSequence([error]);
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toEqual([
{
artifactError: {
fileName: 'mise.lock',
stderr: `stdout output\nstderr output\nexec error`,
},
},
]);
expect(execSnapshots).toMatchObject([{ cmd: trustCmd }]);
});
it('rethrows TEMPORARY_ERROR', async () => {
fs.readLocalFile.mockResolvedValueOnce('existing content');
mockExecSequence([new Error(TEMPORARY_ERROR)]);
await expect(
updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
}),
).rejects.toThrow(TEMPORARY_ERROR);
});
it('runs mise lock for lockFileMaintenance', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config: lockMaintenanceConfig,
});
// version probe returns nothing here, so no --bump
expect(execSnapshots).toMatchObject([
{ cmd: miseVersionCmd },
{ cmd: trustCmd },
{ cmd: lockfileMaintenanceCmd },
]);
});
it('runs mise lock --bump for lockFileMaintenance when mise supports it', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll(safeMiseVersionOutput);
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config: lockMaintenanceConfig,
});
expect(execSnapshots).toMatchObject([
{ cmd: miseVersionCmd },
{ cmd: trustCmd },
{ cmd: 'mise lock --bump' },
]);
});
it('runs mise lock <tools> for targeted updates', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }, { depName: 'python' }],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{ cmd: updateMultipleToolsCmd },
]);
});
it('looks up latest versions for tool versions before `mise lock`, if not set', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
for (const version of ['1.2.3', '2.3.4', '3.4.5', '4.5.6', '5.6.7']) {
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [{ version: version }],
});
}
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config: {
// no constraints
},
});
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool mise 1.2.3' },
{ cmd: 'install-tool node 2.3.4' },
{ cmd: 'install-tool npm 3.4.5' },
{ cmd: 'install-tool golang 4.5.6' },
{ cmd: 'install-tool ruby 5.6.7' },
{ cmd: trustCmd },
{ cmd: updateToolCmd },
]);
});
it('uses constraints to specify tool versions before `mise lock`, if set', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config: {
constraints: {
mise: '2026.6.12',
node: '24.16.0',
npm: '11.4.2',
go: '1.24.4',
ruby: '3.4.3',
},
},
});
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool mise 2026.6.12' },
{ cmd: 'install-tool node 24.16.0' },
{ cmd: 'install-tool npm 11.4.2' },
{ cmd: 'install-tool golang 1.24.4' },
{ cmd: 'install-tool ruby 3.4.3' },
{ cmd: trustCmd },
{ cmd: updateToolCmd },
]);
});
it.each`
depName | oldVersion | newVersion | newPackageFileContent
${'npm:renovate'} | ${'43.220.0'} | ${'43.233.3'} | ${`[tools]\n"npm:renovate" = "43.233.3"\n`}
${'go:github.com/DarthSim/hivemind'} | ${'1.0.0'} | ${'1.1.0'} | ${`[tools]\n"go:github.com/DarthSim/hivemind" = { version = "1.1.0", install_env = { GOPROXY = "direct" } }\n`}
${'gem:rubocop'} | ${'1.75.0'} | ${'1.76.0'} | ${`[tools]\n"gem:rubocop" = "1.76.0"\n`}
`(
'updates mise.lock for $depName with dynamic installs and no constraints',
async ({ depName, oldVersion, newVersion, newPackageFileContent }) => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
datasource.getPkgReleases.mockResolvedValue({
releases: [{ version: '1.0.0' }],
});
const oldLockFileContent = `[[tools."${depName}"]]\nversion = "${oldVersion}"\nbackend = "${depName}"\n`;
const newLockFileContent = `[[tools."${depName}"]]\nversion = "${newVersion}"\nbackend = "${depName}"\n`;
fs.readLocalFile
.mockResolvedValueOnce(oldLockFileContent)
.mockResolvedValueOnce(newLockFileContent);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName }],
newPackageFileContent,
config,
});
expect(res).toEqual([
{
file: {
type: 'addition',
path: 'mise.lock',
contents: newLockFileContent,
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool mise 1.0.0' },
{ cmd: 'install-tool node 1.0.0' },
{ cmd: 'install-tool npm 1.0.0' },
{ cmd: 'install-tool golang 1.0.0' },
{ cmd: 'install-tool ruby 1.0.0' },
{ cmd: trustCmd },
{ cmd: `mise lock ${depName}` },
]);
},
);
it('injects GITHUB_TOKEN when host rule found', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll();
hostRules.add({
hostType: 'github',
matchHost: 'https://api.github.com/',
token: 'github-token',
});
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{
cmd: updateToolCmd,
options: {
cwd: '/tmp/github/some/repo',
env: expect.objectContaining({
GITHUB_TOKEN: 'github-token',
}),
},
},
]);
});
it('handles empty updatedDeps with fallback to full lock', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{ cmd: lockfileMaintenanceCmd },
]);
});
it('handles environment-specific lock files', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.test.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toEqual([
{
file: {
contents: expect.stringContaining('version = "24.16.0"'),
path: 'mise.test.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: trustEnvCmd },
{
cmd: updateToolCmd,
options: {
env: expect.objectContaining({
MISE_ENV: 'test',
}),
},
},
]);
});
it('uses --local flag for local config files', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.local.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toEqual([
{
file: {
contents: expect.stringContaining('version = "24.16.0"'),
path: 'mise.local.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: trustLocalCmd },
{ cmd: 'mise lock --local node' },
]);
});
it('uses --local flag and MISE_ENV for env-specific local config files', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.test.local.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchObject([
{ cmd: trustEnvLocalCmd },
{
cmd: 'mise lock --local node',
options: {
env: expect.objectContaining({
MISE_ENV: 'test',
}),
},
},
]);
});
it('uses --local flag for lock file maintenance on local config', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.local.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config: lockMaintenanceConfig,
});
expect(execSnapshots).toMatchObject([
{ cmd: miseVersionCmd },
{ cmd: trustLocalCmd },
{ cmd: 'mise lock --local' },
]);
});
it('prevents command injection', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce('existing content');
const execSnapshots = mockExecAll();
await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: '|| date; echo ' }, { depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{ cmd: "mise lock '|| date; echo ' node" },
]);
});
it('handles subdirectory package files', async () => {
fs.readLocalFile
.mockResolvedValueOnce('existing content')
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'subdir/mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '',
config,
});
expect(res).toEqual([
{
file: {
contents: expect.stringContaining('version = "24.16.0"'),
path: 'subdir/mise.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: trustSubdirCmd },
{
cmd: updateToolCmd,
options: {
cwd: '/tmp/github/some/repo/subdir',
},
},
]);
});
describe('updateLockedDependency', () => {
const lockFileContent = `
[[tools.node]]
version = "20.11.0"
backend = "core:node"
[[tools.python]]
version = "3.10.17"
`;
it('returns already-updated when version matches', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.11.0',
});
expect(res).toEqual({ status: 'already-updated' });
});
it('returns already-updated for tool with backend prefix', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'core:node',
currentVersion: '20.10.0',
newVersion: '20.11.0',
});
expect(res).toEqual({ status: 'already-updated' });
});
it('returns unsupported when version does not match', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '22.0.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns unsupported when tool not in lock file', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'ruby',
currentVersion: '3.2.0',
newVersion: '3.3.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns unsupported when no lock file content', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent: undefined,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.11.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns unsupported for invalid lock file content', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent: 'invalid toml {{{',
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.11.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns unsupported when depName is undefined', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: undefined as never,
currentVersion: '20.10.0',
newVersion: '20.11.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns update-failed in case of errors', () => {
vi.spyOn(lockfile, 'getLockedVersion').mockImplementationOnce(() => {
throw new Error('unexpected error');
});
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.11.0',
});
expect(res).toEqual({ status: 'update-failed' });
});
});
});