import { GlobalConfig } from '../../config/global.ts'; import { applyConstraintsFiltering } from '../../modules/datasource/common.ts'; import * as _datasource from '../../modules/datasource/index.ts'; import * as memCache from '../cache/memory/index.ts'; import { setCustomEnv } from '../env.ts'; import { generateInstallCommands, getToolConfig, isDynamicInstall, resolveConstraint, } from './containerbase.ts'; import type { ToolConstraint, ToolName } from './types.ts'; vi.mock('../../modules/datasource/index.ts'); const datasource = vi.mocked(_datasource); describe('util/exec/containerbase', () => { describe('isDynamicInstall()', () => { beforeEach(() => { GlobalConfig.reset(); vi.stubEnv('CONTAINERBASE', undefined); }); it('returns false if binarySource is not install', () => { expect(isDynamicInstall()).toBeFalse(); }); it('returns false if not containerbase', () => { GlobalConfig.set({ binarySource: 'install' }); expect(isDynamicInstall()).toBeFalse(); }); it('ignores CONTAINERBASE from the custom env', () => { GlobalConfig.set({ binarySource: 'install' }); setCustomEnv({ CONTAINERBASE: 'true' }); expect(isDynamicInstall()).toBeFalse(); setCustomEnv({}); }); it('returns false if any unsupported tools', () => { GlobalConfig.set({ binarySource: 'install' }); vi.stubEnv('CONTAINERBASE', 'true'); const toolConstraints: ToolConstraint[] = [ { toolName: 'node' }, // @ts-expect-error -- intentionally using invalid constraint names { toolName: 'invalid' }, ]; expect(isDynamicInstall(toolConstraints)).toBeFalse(); }); it('returns true if supported tools', () => { GlobalConfig.set({ binarySource: 'install' }); vi.stubEnv('CONTAINERBASE', 'true'); const toolConstraints: ToolConstraint[] = [{ toolName: 'npm' }]; expect(isDynamicInstall(toolConstraints)).toBeTrue(); }); }); describe('getToolConfig()', () => { it('returns config for a known tool', () => { const config = getToolConfig('npm'); expect(config).toBeDefined(); expect(config).toMatchObject({ datasource: expect.toBeString(), versioning: expect.toBeString(), }); }); it('returns undefined for an unknown tool', () => { // @ts-expect-error -- intentionally using invalid tool name expect(getToolConfig('unknown-tool')).toBeUndefined(); }); }); describe('resolveConstraint()', () => { it('returns from config', async () => { await expect( resolveConstraint({ toolName: 'composer', constraint: '1.1.0' }), ).resolves.toBe('1.1.0'); }); it('returns highest stable', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '1.0.0' }, { version: '1.1.0' }, { version: '1.3.0' }, { version: '2.0.14' }, { version: '2.1.0' }, { version: '2.2.0-pre.0' }, ], }); await expect(resolveConstraint({ toolName: 'composer' })).resolves.toBe( '2.1.0', ); }); it('returns highest unstable', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2.0.14-b.1' }, { version: '2.1.0-a.1' }], }); await expect(resolveConstraint({ toolName: 'composer' })).resolves.toBe( '2.1.0-a.1', ); }); it('respects latest', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ tags: { latest: '2.0.14', }, releases: [ { version: '1.0.0' }, { version: '1.1.0' }, { version: '1.3.0' }, { version: '2.0.14' }, { version: '2.1.0' }, { version: '2.2.0-pre.0' }, ], }); await expect(resolveConstraint({ toolName: 'composer' })).resolves.toBe( '2.0.14', ); }); it('supports rust release channels', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '1' }, { version: '1.65' }, { version: '1.65.0' }, ], }); await expect(resolveConstraint({ toolName: 'rust' })).resolves.toBe( '1.65.0', ); }); it('supports nightly rust release channels', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: 'nightly' }, { version: 'nightly-2026-06-19' }], }); await expect(resolveConstraint({ toolName: 'rust' })).resolves.toBe( 'nightly-2026-06-19', ); }); it('throws for unknown tools', async () => { await expect( resolveConstraint({ toolName: 'whoops' as ToolName }), ).rejects.toThrow('Invalid tool to install: whoops'); }); it('throws no releases', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [], }); await expect(resolveConstraint({ toolName: 'composer' })).rejects.toThrow( 'No tool releases found.', ); }); it('falls back to latest version if no compatible release', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '1.2.3' }], }); await expect( resolveConstraint({ toolName: 'composer', constraint: '^3.1.0' }), ).resolves.toBe('1.2.3'); }); it('falls back to latest version if invalid constraint', async () => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '1.2.3' }], }); await expect( resolveConstraint({ toolName: 'composer', constraint: 'whoops' }), ).resolves.toBe('1.2.3'); }); it.each` version | expected ${'^3.9.0'} | ${'3.10.4'} ${'^3.9'} | ${'3.10.4'} ${'3.9.*'} | ${'3.9.1'} ${'>3.8,<3.10'} | ${'3.9.1'} ${'==3.9.*'} | ${'3.9.1'} `( 'supports python ranges "$version" => "$expected"', async ({ version: constraint, expected }) => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '3.9.0' }, { version: '3.9.1' }, { version: '3.10.0' }, { version: '3.10.4' }, ], }); await expect( resolveConstraint({ toolName: 'python', constraint }), ).resolves.toBe(expected); }, ); it('removes pep440 ==', async () => { await expect( resolveConstraint({ toolName: 'pipenv', constraint: '==2020.8.13', }), ).resolves.toBe('2020.8.13'); }); it.each` version | expected ${'>=2.5.0 <2.6.0'} | ${'2.5.1'} ${'>=2.6.0-0.0.pre <2.7.0-0.0.pre'} | ${'2.6.0-0.0.pre'} ${'>=2.8.0'} | ${'2.8.1'} ${'<2.9.0-0.1.pre'} | ${'2.8.1'} ${'2.10.0-0.2.pre'} | ${'2.10.0-0.2.pre'} ${'>=2.11.0-0.1.pre'} | ${'2.12.0-4.1.pre'} ${'<=2.10.0'} | ${'2.8.1'} `( 'supports flutter ranges "$version" => "$expected"', async ({ version: constraint, expected }) => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '2.5.0-1.0.pre', isStable: false }, { version: '2.5.0', isStable: true }, { version: '2.5.1', isStable: true }, { version: '2.6.0-0.0.pre', isStable: false }, { version: '2.8.0', isStable: true }, { version: '2.8.1', isStable: true }, { version: '2.9.0-0.1.pre', isStable: false }, { version: '2.12.0-4.1.pre', isStable: false }, ], }); await expect( resolveConstraint({ toolName: 'flutter', constraint }), ).resolves.toBe(expected); }, ); it.each` version | expected ${'>2.17.0 <2.17.8'} | ${'2.17.7'} ${'>2.19.0'} | ${'2.19.0-81.0.dev'} ${'<=2.17.5'} | ${'2.17.5'} ${'<2.17.5'} | ${'2.19.0-81.0.dev'} ${'<2.17.6'} | ${'2.17.5'} `( 'supports dart ranges "$version" => "$expected"', async ({ version: constraint, expected }) => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '2.17.0-69.2.beta', isStable: false }, { version: '2.17.0-7.0.dev', isStable: false }, { version: '2.17.5', isStable: true }, { version: '2.17.6', isStable: true }, { version: '2.17.7', isStable: true }, { version: '2.18.0', isStable: true }, { version: '2.18.0-44.1.beta', isStable: false }, { version: '2.18.0-99.0.dev', isStable: false }, { version: '2.18.4', isStable: true }, { version: '2.18.5', isStable: true }, { version: '2.19.0-255.2.beta', isStable: false }, { version: '2.19.0-81.0.dev', isStable: false }, ], }); await expect( resolveConstraint({ toolName: 'dart', constraint }), ).resolves.toBe(expected); }, ); }); describe('resolveConstraint() with resolved tools', () => { // run the real constraints filtering against npm-like releases function mockNpmReleases(...nodeConstraints: string[]): void { datasource.getPkgReleases.mockImplementation((config) => Promise.resolve( applyConstraintsFiltering( { releases: nodeConstraints.map((node, i) => ({ version: `${11 + i}.0.0`, constraints: { node: [node] }, })), }, config, ), ), ); } it('picks the newest release compatible with the parent version', async () => { mockNpmReleases('^20.17.0 || >=22.9.0', '^22.22.2 || >=24.15.0'); await expect( resolveConstraint({ toolName: 'npm' }, { node: '20.17.0' }), ).resolves.toBe('11.0.0'); }); it('picks the newest release when the parent version supports it', async () => { mockNpmReleases('^20.17.0 || >=22.9.0', '^22.22.2 || >=24.15.0'); await expect( resolveConstraint({ toolName: 'npm' }, { node: '24.15.0' }), ).resolves.toBe('12.0.0'); }); it('ignores the parent version when no release is compatible', async () => { mockNpmReleases('^22.9.0', '^22.22.2 || >=24.15.0'); await expect( resolveConstraint({ toolName: 'npm' }, { node: '20.17.0' }), ).resolves.toBe('12.0.0'); expect(datasource.getPkgReleases).toHaveBeenLastCalledWith({ datasource: 'npm', packageName: 'npm', versioning: 'npm', }); }); }); describe('generateInstallCommands()', () => { beforeEach(() => { datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '1.0.0' }, { version: '1.1.0' }, { version: '1.3.0' }, { version: '2.0.14' }, { version: '2.1.0' }, ], }); }); it('returns install commands', async () => { const toolConstraints: ToolConstraint[] = [ { toolName: 'composer', }, ]; await expect(generateInstallCommands(toolConstraints)).resolves.toEqual([ 'install-tool composer 2.1.0', ]); }); it('only matches a tool against its own parent', async () => { const toolConstraints: ToolConstraint[] = [ { toolName: 'node', constraint: '20.17.0' }, { toolName: 'composer' }, ]; await expect(generateInstallCommands(toolConstraints)).resolves.toEqual([ 'install-tool node 20.17.0', 'install-tool composer 2.1.0', ]); expect(datasource.getPkgReleases).toHaveBeenCalledExactlyOnceWith({ datasource: 'github-releases', packageName: 'containerbase/composer-prebuild', versioning: 'composer', }); }); it('filters tool releases by the resolved parent version', async () => { const toolConstraints: ToolConstraint[] = [ { toolName: 'node', constraint: '20.17.0' }, { toolName: 'npm', constraint: '>=1' }, ]; await expect(generateInstallCommands(toolConstraints)).resolves.toEqual([ 'install-tool node 20.17.0', 'install-tool npm 2.1.0', ]); expect(datasource.getPkgReleases).toHaveBeenCalledExactlyOnceWith({ datasource: 'npm', packageName: 'npm', versioning: 'npm', constraintsFiltering: 'strict', constraints: { node: '20.17.0' }, }); }); it('repeats install commands when not memoizing', async () => { const toolConstraints: ToolConstraint[] = [{ toolName: 'composer' }]; await generateInstallCommands(toolConstraints); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2.1.0' }], }); await expect(generateInstallCommands(toolConstraints)).resolves.toEqual([ 'install-tool composer 2.1.0', ]); }); describe('memoize', () => { beforeEach(() => { memCache.init(); }); afterEach(() => { memCache.reset(); }); it('skips a tool already installed this run at the same version', async () => { const toolConstraints: ToolConstraint[] = [{ toolName: 'composer' }]; await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.1.0']); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2.1.0' }], }); await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual([]); }); it('re-issues the install command when the resolved version changes', async () => { const toolConstraints: ToolConstraint[] = [{ toolName: 'composer' }]; await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.1.0']); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2.2.0' }], }); await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.2.0']); }); it('re-issues the install command when switching back to an earlier version', async () => { const toolConstraints: ToolConstraint[] = [{ toolName: 'composer' }]; await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.1.0']); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2.2.0' }], }); await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.2.0']); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2.1.0' }], }); await expect( generateInstallCommands(toolConstraints, true), ).resolves.toEqual(['install-tool composer 2.1.0']); }); }); }); });