Files
renovate/lib/modules/manager/mise/artifacts.spec.ts
Gregor Zeitlinger 2e951d2254 feat(mise): support selector lockfile updates (#44615)
* mise: support selector lockfile updates

* fix(mise): handle empty lockfile AST

* test(mise): use valid empty TOML AST fixture

* fix(mise): support current regex utility

* fix: clarify lockfile-only lookup handling

* fix(mise): handle selector lockfile updates correctly

* test(mise): cover selector lockfile review fixes

* fix(mise): scope selector artifact refresh

* chore: retrigger CI

* fix(mise): remove unreachable lockfile guard

* refactor(lookup): clarify lockfile-only value handling

* refactor(mise): use shared TOML utilities

* fix(mise): handle selector lockfile edge cases

* fix(mise): satisfy selector CI coverage

* fix(mise): require dependency names

* fix(mise): preserve configured dependency names

* fix(mise): address selector lockfile review feedback

* test(mise): cover major selector lockfile updates

* chore(static-data): auto-update Java LTS versions

So we don't have a hardcoded set of data in the mise manager.

Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>

* fix(mise): address selector lockfile feedback

* test(manager/mise): cover nonnumeric current version prefix

formatLockedVersion()'s currentPrefix fallback to '' only runs when
the current version has no leading numeric match, which no existing
test reached.

Co-Authored-By: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>

---------

Co-authored-by: Jamie Tanna <jamie.tanna@mend.io>
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
2026-09-14 15:57:22 +00:00

1037 lines
30 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('refreshes metadata from the updated in-memory lockfile', async () => {
const originalLockFile =
'[[tools.node]]\nversion = "20.0.0"\nplatforms = { linux = { checksum = "old" } }\n';
const updatedLockFile =
'[[tools.node]]\nversion = "22.0.0"\nplatforms = { linux = { checksum = "new" } }\n';
const refreshedLockFile =
'[[tools.node]]\nversion = "22.0.0"\nplatforms = { linux = { checksum = "refreshed" } }\n';
fs.readLocalFile
.mockResolvedValueOnce(originalLockFile)
.mockResolvedValueOnce(refreshedLockFile);
const execSnapshots = mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '[tools]\nnode = "22"\n',
newLockFileContent: updatedLockFile,
config,
});
expect(fs.writeLocalFile).toHaveBeenCalledWith(
'mise.lock',
updatedLockFile,
);
expect(res).toEqual([
{
file: {
type: 'addition',
path: 'mise.lock',
contents: expect.stringContaining('checksum = "refreshed"'),
},
},
]);
expect(execSnapshots).toMatchObject([
{ cmd: trustCmd },
{ cmd: updateToolCmd },
]);
});
it('returns regenerated content that matches the in-memory update', async () => {
const originalLockFile = '[[tools.node]]\nversion = "20.0.0"\n';
const updatedLockFile = '[[tools.node]]\nversion = "22.0.0"\n';
fs.readLocalFile
.mockResolvedValueOnce(originalLockFile)
.mockResolvedValueOnce(updatedLockFile);
mockExecAll();
const res = await updateArtifacts({
packageFileName: 'mise.toml',
updatedDeps: [{ depName: 'node' }],
newPackageFileContent: '[tools]\nnode = "22"\n',
newLockFileContent: updatedLockFile,
config,
});
expect(res).toEqual([
{
file: {
type: 'addition',
path: 'mise.lock',
contents: updatedLockFile,
},
},
]);
});
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('falls back to the extracted constraints', 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: {
extractedConstraints: {
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('updates the lockfile when version changes', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '22.0.0',
});
expect(res).toMatchObject({
status: 'updated',
files: {
'mise.lock': expect.stringContaining('version = "22.0.0"'),
},
});
});
it('keeps the package file in the update set for artifact refresh', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
packageFileContent: '[[tools.node]]\nversion = "22"\n',
lockFile: 'mise.lock',
lockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '22.0.0',
});
expect(res).toMatchObject({
status: 'updated',
files: {
'mise.toml': '[[tools.node]]\nversion = "22"\n',
'mise.lock': expect.stringContaining('version = "22.0.0"'),
},
});
});
it('does not add a null package file to the update set', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
packageFileContent: null as never,
lockFile: 'mise.lock',
lockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '22.0.0',
});
expect(res).toMatchObject({
status: 'updated',
files: {
'mise.lock': expect.stringContaining('version = "22.0.0"'),
},
});
expect(res.files).not.toHaveProperty('mise.toml');
});
it('preserves a vendor prefix in the lockfile version', () => {
const javaLockFileContent = `
[[tools.java]]
version = 'temurin-25.0.3+9.0.LTS'
backend = 'core:java'
`;
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent: javaLockFileContent,
depName: 'java',
currentVersion: '25.0.3+9.0.LTS',
newVersion: '25.0.4+8.0.LTS',
});
expect(res).toMatchObject({
status: 'updated',
files: {
'mise.lock': expect.stringContaining(
"version = 'temurin-25.0.4+8.0.LTS'",
),
},
});
});
it('supports quoted version keys and ignores other nested keys', () => {
const quotedKeyLockFileContent = `
[[tools.node]]
foo.bar = "ignored"
"version" = "20.11.0"
`;
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent: quotedKeyLockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.12.0',
});
expect(res).toMatchObject({
status: 'updated',
files: {
'mise.lock': expect.stringContaining('"version" = "20.12.0"'),
},
});
});
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 the lockfile schema is invalid', () => {
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent: 'foo = "bar"',
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.12.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns unsupported when the lock entry has no array-table AST node', () => {
const inlineLockFileContent = `
tools = { node = [{ version = "20.11.0" }] }
`;
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent: inlineLockFileContent,
depName: 'node',
currentVersion: '20.10.0',
newVersion: '20.12.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 unsupported when the lock entry lookup cannot be resolved', () => {
vi.spyOn(lockfile, 'getLockedVersion').mockReturnValueOnce('20.11.0');
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 a prefixed lock entry has no short-name match', () => {
vi.spyOn(lockfile, 'getLockedVersion').mockReturnValueOnce('20.11.0');
const res = updateLockedDependency({
packageFile: 'mise.toml',
lockFile: 'mise.lock',
lockFileContent,
depName: 'core:ruby',
currentVersion: '3.2.0',
newVersion: '3.3.0',
});
expect(res).toEqual({ status: 'unsupported' });
});
it('returns update-failed in case of errors', () => {
vi.spyOn(lockfile, 'getLockedTool').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' });
});
});
});