import { codeBlock } from 'common-tags'; import { mockExecAll } from '~test/exec-util.ts'; import { fs, git, partial } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { InternalGlobalConfigOptions, RepoGlobalConfig, } from '../../../config/types.ts'; import type { StatusResult } from '../../../util/git/types.ts'; import type { UpdateArtifact } from '../types.ts'; import { updateArtifacts } from './artifacts.ts'; vi.mock('../../../util/exec/env.ts'); vi.mock('../../../util/fs/index.ts'); const globalConfig: RepoGlobalConfig & InternalGlobalConfigOptions = { localDir: '', binarySource: 'global', }; const devboxJson = codeBlock` { "$schema": "https://raw.githubusercontent.com/jetpack-io/devbox/0.10.1/.schema/devbox.schema.json", "packages": ["nodejs@20", "metabase@0.49.1", "postgresql@latest", "gh@latest"], } `; describe('modules/manager/devbox/artifacts', () => { describe('updateArtifacts()', () => { let updateArtifact: UpdateArtifact; beforeEach(() => { GlobalConfig.set(globalConfig); updateArtifact = { config: {}, newPackageFileContent: '', packageFileName: '', updatedDeps: [], }; }); it('skips if no updatedDeps and no lockFileMaintenance', async () => { await expect(updateArtifacts(updateArtifact)).resolves.toBeNull(); }); it('skips if no lock file in config', async () => { updateArtifact.updatedDeps = [{}]; await expect(updateArtifacts(updateArtifact)).resolves.toBeNull(); }); it('skips if cannot read lock file', async () => { updateArtifact.updatedDeps = [ { manager: 'devbox', lockFiles: ['devbox.lock'] }, ]; await expect(updateArtifacts(updateArtifact)).resolves.toBeNull(); }); it('returns installed devbox.lock', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const execSnapshots = mockExecAll(); const oldLockFileContent = Buffer.from('Old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: {}, }), ).resolves.toEqual([ { file: { type: 'addition', path: 'devbox.lock', contents: newLockFileContent, }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'devbox update nodejs --no-install', options: { cwd: '.', env: {}, maxBuffer: 10485760, timeout: 900000, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', }, }, ]); }); it('calls install instead of update --no-install if an older version of devbox is constrained', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const execSnapshots = mockExecAll(); const oldLockFileContent = Buffer.from('Old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: { constraints: { devbox: '0.13.0', }, }, }), ).resolves.toEqual([ { file: { type: 'addition', path: 'devbox.lock', contents: newLockFileContent, }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'devbox install', options: { cwd: '.', env: {}, maxBuffer: 10485760, timeout: 900000, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', }, }, ]); }); it('falls back to the extracted devbox constraint', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const execSnapshots = mockExecAll(); fs.readLocalFile.mockResolvedValueOnce( Buffer.from('Old devbox.lock') as never, ); fs.readLocalFile.mockResolvedValueOnce( Buffer.from('New devbox.lock') as never, ); await updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: { extractedConstraints: { devbox: '0.13.0', }, }, }); // the extracted constraint decides that `--no-install` is unsupported expect(execSnapshots).toMatchObject([{ cmd: 'devbox install' }]); }); it('returns installed devbox.lock with multiple updated deps', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const execSnapshots = mockExecAll(); const oldLockFileContent = Buffer.from('Old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'ruby', }, ], config: {}, }), ).resolves.toEqual([ { file: { type: 'addition', path: 'devbox.lock', contents: newLockFileContent, }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'devbox update nodejs --no-install', options: { cwd: '.', env: {}, maxBuffer: 10485760, timeout: 900000, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', }, }, { cmd: 'devbox update ruby --no-install', options: { cwd: '.', env: {}, maxBuffer: 10485760, timeout: 900000, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', }, }, ]); }); it('returns null if no updatedDeps are passed', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const oldLockFileContent = Buffer.from('Old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [{}], config: {}, }), ).resolves.toBeNull(); }); it('returns null if no updatedDeps have depNames', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce('{}'); const oldLockFileContent = Buffer.from('Old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], }, ], config: {}, }), ).resolves.toBeNull(); }); it('returns updated devbox.lock', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: ['devbox.lock'], }), ); const oldLockFileContent = Buffer.from('old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [{}], config: { isLockFileMaintenance: true, }, }), ).resolves.toEqual([ { file: { type: 'addition', path: 'devbox.lock', contents: newLockFileContent, }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'devbox update --no-install', options: { cwd: '.', env: {}, maxBuffer: 10485760, timeout: 900000, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', }, }, ]); }); it('calls update without --no-install flag if an older version of devbox is being used', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: ['devbox.lock'], }), ); const oldLockFileContent = Buffer.from('old devbox.lock'); const newLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [{}], config: { isLockFileMaintenance: true, constraints: { devbox: '< 0.14.0', }, }, }), ).resolves.toEqual([ { file: { type: 'addition', path: 'devbox.lock', contents: newLockFileContent, }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'devbox update', options: { cwd: '.', env: {}, maxBuffer: 10485760, timeout: 900000, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', }, }, ]); }); it('returns null if no changes are found', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: [], }), ); mockExecAll(); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [], config: {}, }), ).resolves.toBeNull(); }); it('returns null if devbox.lock not found after update', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: [], }), ); mockExecAll(); const oldLockFileContent = Buffer.from('Old devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: {}, }), ).resolves.toBeNull(); }); it('returns null if devbox.lock not found', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: [], }), ); mockExecAll(); fs.readLocalFile.mockResolvedValueOnce(null); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: {}, }), ).resolves.toBeNull(); }); it('returns null if no lock file changes are found', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(`{}`); git.getRepoStatus.mockResolvedValueOnce( partial({ modified: [], }), ); mockExecAll(); const oldLockFileContent = Buffer.from('Old devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: {}, }), ).resolves.toBeNull(); }); it('returns an artifact error on failure', async () => { fs.getSiblingFileName.mockReturnValueOnce('devbox.lock'); const newLockFileContent = `{}`; const oldLockFileContent = Buffer.from('New devbox.lock'); fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never); fs.readLocalFile.mockResolvedValueOnce(newLockFileContent); await expect( updateArtifacts({ packageFileName: 'devbox.json', newPackageFileContent: devboxJson, updatedDeps: [ { manager: 'devbox', lockFiles: ['devbox.lock'], depName: 'nodejs', }, ], config: {}, }), ).resolves.toEqual([ { artifactError: { fileName: 'devbox.lock', stderr: "Cannot read properties of undefined (reading 'stdout')", }, }, ]); }); }); });