From aaf9bbb537aabe3c827c5933eb5e1917e191ecda Mon Sep 17 00:00:00 2001 From: Kim T Date: Thu, 30 Jul 2026 21:57:52 -0700 Subject: [PATCH] [refactor] Extract install()'s per-file-type handling into installTargets.ts install() was a ~200-line method mixing download/hash/elevation orchestration with the logic for where each installed file's final destination is (installer marker dir, Sampler move, embedded .pkg/.dmg, format-sorted plugin files, flat app/preset/project move) - impossible to unit-test any one destination without exercising the whole pipeline. Move each destination into its own function in helpers/installTargets.ts, each taking an explicit params object and returning the directories it populated (which install() now just adds to its rollback-tracking Set from #105, rather than inlining `installedDirs.add(dirTarget)` at each of five call sites). Behavior-preserving except for one verified dead branch: the pre-refactor code selected `formatDir` from presetFormatDir/projectFormatDir when `this.type` was Presets/Projects, but that value was only ever read inside the `this.type === Plugins` branch - mutually exclusive with type being Presets/Projects for a single ManagerLocal instance - so those two branches could never actually be reached. Simplified to just pluginFormatDir directly. Added direct unit tests for each extracted destination in installTargets.test.ts (mocking only archiveExtract, not the full download/hash/elevation path) - install()'s own integration tests are unchanged and still pass, confirming the extraction didn't alter behavior. --- src/classes/ManagerLocal.ts | 129 +++------------- src/helpers/installTargets.ts | 210 ++++++++++++++++++++++++++ tests/helpers/installTargets.test.ts | 212 +++++++++++++++++++++++++++ 3 files changed, 443 insertions(+), 108 deletions(-) create mode 100644 src/helpers/installTargets.ts create mode 100644 tests/helpers/installTargets.test.ts diff --git a/src/classes/ManagerLocal.ts b/src/classes/ManagerLocal.ts index 30cfb64..f0730b2 100644 --- a/src/classes/ManagerLocal.ts +++ b/src/classes/ManagerLocal.ts @@ -15,17 +15,15 @@ import { fileCreate, fileCreateJson, fileCreateYaml, - fileExec, fileExists, fileHash, - fileInstall, fileOpen, fileReadJson, fileReadYaml, - filesMove, isAdmin, runCliAsAdmin, } from '../helpers/file.js'; +import { installArchiveFile, installInstallerFile } from '../helpers/installTargets.js'; import { isValidGithubRepo, isValidSlug, isValidVersion, pathGetSlug, pathGetVersion } from '../helpers/utils.js'; import { commandExists, getArchitecture, getSystem, isTests } from '../helpers/utilsLocal.js'; import { apiBuffer } from '../helpers/api.js'; @@ -38,11 +36,8 @@ import { ConfigInterface } from '../types/Config.js'; import { ConfigLocal } from './ConfigLocal.js'; import { packageCompatibleFiles, packageErrors, packageRecommendations } from '../helpers/package.js'; import { PresetInterface } from '../types/Preset.js'; -import { presetFormatDir } from '../types/PresetFormat.js'; import { ProjectInterface } from '../types/Project.js'; -import { projectFormatDir } from '../types/ProjectFormat.js'; import { FileFormat } from '../types/FileFormat.js'; -import { PluginType } from '../types/PluginType.js'; import { SystemType } from '../types/SystemType.js'; import { packageLoadFile, packageSaveFile } from '../helpers/packageLocal.js'; @@ -354,16 +349,14 @@ export class ManagerLocal extends Manager { // If installer, run the installer headless (without the user interface). if (file.type === FileType.Installer) { - // Test time out if installing during tests. - if (isTests()) fileOpen(filePath); - else fileInstall(filePath); - // Currently we don't get a list of paths from the installer. - // Create empty directory and save package version information. - // Installers have to be manually uninstalled for now. - const dirTarget: string = path.join(this.typeDir, 'Installers', slug, versionNum); - dirCreate(dirTarget); - fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); - installedDirs.add(dirTarget); + installInstallerFile({ + typeDir: this.typeDir, + slug, + versionNum, + pkgVersion, + filePath, + isTestsFn: isTests, + }).forEach(dir => installedDirs.add(dir)); } // If archive, extract the archive to temporary directory, then move individual files. @@ -375,98 +368,18 @@ export class ManagerLocal extends Manager { slug, versionNum, ); - const dirSub: string = path.join(slug, versionNum); - let formatDir: Record = pluginFormatDir; - if (this.type === RegistryType.Apps) formatDir = pluginFormatDir; - else if (this.type === RegistryType.Presets) formatDir = presetFormatDir; - else if (this.type === RegistryType.Projects) formatDir = projectFormatDir; - await archiveExtract(filePath, dirSource); - - // Move entire directory, maintaining the same folder structure. - if (pkgVersion.type === PluginType.Sampler) { - const dirTarget: string = path.join(this.typeDir, 'Samplers', dirSub); - dirCreate(dirTarget); - dirMove(dirSource, dirTarget); - fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); - installedDirs.add(dirTarget); - } else { - // Check if archive contains installer files (pkg, dmg) that should be run - const allFiles = dirRead(`${dirSource}/**/*`).filter(f => !dirIs(f)); - const installerFiles = allFiles.filter(f => { - const ext = path.extname(f).toLowerCase(); - return ext === '.pkg' || ext === '.dmg'; - }); - - if (installerFiles.length > 0) { - // Run installer files found in archive - for (const installerFile of installerFiles) { - if (isTests()) fileOpen(installerFile); - else fileInstall(installerFile); - } - // Create directory and save package info for installer - const dirTarget: string = path.join(this.typeDir, 'Installers', dirSub); - dirCreate(dirTarget); - fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); - installedDirs.add(dirTarget); - } else if (this.type === RegistryType.Plugins) { - // For plugins, move files into type-specific subdirectories - const filesMoved: string[] = filesMove(dirSource, this.typeDir, dirSub, formatDir); - if (filesMoved.length === 0) { - throw new Error(`No compatible files found to install for ${slug}`); - } - filesMoved.forEach((fileMoved: string) => { - const fileJson: string = path.join(path.dirname(fileMoved), 'index.json'); - fileCreateJson(fileJson, pkgVersion); - // A single archive can contain multiple formats (e.g. VST3 and CLAP), moved into - // different formatDir subdirectories - track each one, not just the first. - installedDirs.add(path.dirname(fileMoved)); - }); - } else { - // For apps/projects/presets, move entire directory without type subdirectories - const dirTarget: string = path.join(this.typeDir, dirSub); - dirCreate(dirTarget); - dirMove(dirSource, dirTarget); - fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); - installedDirs.add(dirTarget); - // Ensure executable permissions for likely executables inside moved app/project/preset - try { - const movedFiles = dirRead(path.join(dirTarget, '**', '*')).filter(f => !dirIs(f)); - movedFiles.forEach((movedFile: string) => { - const ext = path.extname(movedFile).slice(1).toLowerCase(); - if (['', 'elf', 'exe'].includes(ext)) { - try { - fileExec(movedFile); - } catch (err) { - this.log(`Failed to set exec on ${movedFile}:`, err); - } - } - }); - } catch (err) { - this.log('Error while setting executable permissions:', err); - } - // Also handle macOS .app bundles: set exec on binaries in Contents/MacOS - try { - const appDirs = dirRead(path.join(dirTarget, '**', '*.app')).filter(d => dirIs(d)); - appDirs.forEach((appDir: string) => { - try { - const macosBinPattern = path.join(appDir, 'Contents', 'MacOS', '**', '*'); - const macosFiles = dirRead(macosBinPattern).filter(f => !dirIs(f)); - macosFiles.forEach((binFile: string) => { - try { - fileExec(binFile); - } catch (err) { - this.log(`Failed to set exec on app binary ${binFile}:`, err); - } - }); - } catch (err) { - this.log(`Error scanning .app contents for ${appDir}:`, err); - } - }); - } catch (err) { - this.log(err); - } - } - } + const dirs = await installArchiveFile({ + typeDir: this.typeDir, + type: this.type, + slug, + versionNum, + pkgVersion, + filePath, + dirSource, + isTestsFn: isTests, + log: (...args: any) => this.log(...args), + }); + dirs.forEach(dir => installedDirs.add(dir)); } } } catch (err) { diff --git a/src/helpers/installTargets.ts b/src/helpers/installTargets.ts new file mode 100644 index 0000000..1851b04 --- /dev/null +++ b/src/helpers/installTargets.ts @@ -0,0 +1,210 @@ +import path from 'path'; +import { + archiveExtract, + dirCreate, + dirIs, + dirMove, + dirRead, + fileCreateJson, + fileExec, + fileInstall, + fileOpen, +} from './file.js'; +import { filesMove } from './file.js'; +import { PackageVersion } from '../types/Package.js'; +import { pluginFormatDir } from '../types/PluginFormat.js'; +import { PluginType } from '../types/PluginType.js'; +import { RegistryType } from '../types/Registry.js'; + +// Each install target function below handles exactly one of install()'s per-file-type +// destinations, and returns every directory it populated under `typeDir` (the live, user-facing +// install location) so the caller can track them for rollback on a later failure - see +// ManagerLocal.install(). Each is a small, self-contained unit: given a real (or temp-directory) +// filesystem and a package version, it can be called and its result asserted directly, without +// going through install()'s download/hash-check/elevation orchestration at all. + +export interface InstallerFileParams { + typeDir: string; + slug: string; + versionNum: string; + pkgVersion: PackageVersion; + filePath: string; + isTestsFn: () => boolean; +} + +// FileType.Installer: run the installer headless (or open it, under tests) and record a marker +// directory - installers install themselves outside typeDir, so there's nothing to move here; +// the directory only lets scan()/isPackageInstalled() know this version is present. +export function installInstallerFile(params: InstallerFileParams): string[] { + const { typeDir, slug, versionNum, pkgVersion, filePath, isTestsFn } = params; + // Test time out if installing during tests. + if (isTestsFn()) fileOpen(filePath); + else fileInstall(filePath); + // Currently we don't get a list of paths from the installer. + // Create empty directory and save package version information. + // Installers have to be manually uninstalled for now. + const dirTarget: string = path.join(typeDir, 'Installers', slug, versionNum); + dirCreate(dirTarget); + fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); + return [dirTarget]; +} + +export interface ArchiveFileParams { + typeDir: string; + type: RegistryType; + slug: string; + versionNum: string; + pkgVersion: PackageVersion; + filePath: string; + // Scratch directory to extract into - the caller computes this since it needs appDir/file.type, + // neither of which this module otherwise needs to know about. + dirSource: string; + isTestsFn: () => boolean; + log: (...args: any) => void; +} + +// FileType.Archive: extract to the given scratch directory, then dispatch to whichever final +// destination applies to this package/archive - a Sampler moves as one opaque unit, an archive +// containing its own installer (.pkg/.dmg) runs it and just marks presence (like +// installInstallerFile above), a Plugins-type package's files get sorted into format-specific +// subdirectories, and everything else (apps/presets/projects) moves as a flat directory. +export async function installArchiveFile(params: ArchiveFileParams): Promise { + const { typeDir, type, slug, versionNum, pkgVersion, filePath, dirSource, isTestsFn, log } = params; + const dirSub: string = path.join(slug, versionNum); + await archiveExtract(filePath, dirSource); + + if (pkgVersion.type === PluginType.Sampler) { + return installSamplerArchive(typeDir, dirSource, dirSub, pkgVersion); + } + + const embeddedInstallers = findEmbeddedInstallers(dirSource); + if (embeddedInstallers.length > 0) { + return installEmbeddedInstallers(typeDir, dirSub, embeddedInstallers, pkgVersion, isTestsFn); + } + + if (type === RegistryType.Plugins) { + return installPluginFormats(typeDir, dirSource, dirSub, slug, pkgVersion); + } + + return installFlatDirectory(typeDir, dirSource, dirSub, pkgVersion, log); +} + +// Move entire directory, maintaining the same folder structure. +function installSamplerArchive( + typeDir: string, + dirSource: string, + dirSub: string, + pkgVersion: PackageVersion, +): string[] { + const dirTarget: string = path.join(typeDir, 'Samplers', dirSub); + dirCreate(dirTarget); + dirMove(dirSource, dirTarget); + fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); + return [dirTarget]; +} + +// A .pkg/.dmg found inside an otherwise-archive-typed download (e.g. a zip wrapping a macOS +// installer) - these still need to run as an installer rather than moving into typeDir directly. +function findEmbeddedInstallers(dirSource: string): string[] { + return dirRead(`${dirSource}/**/*`) + .filter(f => !dirIs(f)) + .filter(f => ['.pkg', '.dmg'].includes(path.extname(f).toLowerCase())); +} + +function installEmbeddedInstallers( + typeDir: string, + dirSub: string, + installerFiles: string[], + pkgVersion: PackageVersion, + isTestsFn: () => boolean, +): string[] { + // Run installer files found in archive + for (const installerFile of installerFiles) { + if (isTestsFn()) fileOpen(installerFile); + else fileInstall(installerFile); + } + // Create directory and save package info for installer + const dirTarget: string = path.join(typeDir, 'Installers', dirSub); + dirCreate(dirTarget); + fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); + return [dirTarget]; +} + +// For plugins, move files into type-specific subdirectories. Only ever called for +// RegistryType.Plugins (see installArchiveFile above) - pluginFormatDir is the only formatDir +// that can apply here, so unlike the pre-refactor code this doesn't branch on `type` to pick a +// formatDir: that branch could never actually select presetFormatDir/projectFormatDir in +// practice, since this function itself is only reached when type === Plugins. +function installPluginFormats( + typeDir: string, + dirSource: string, + dirSub: string, + slug: string, + pkgVersion: PackageVersion, +): string[] { + const filesMoved: string[] = filesMove(dirSource, typeDir, dirSub, pluginFormatDir); + if (filesMoved.length === 0) { + throw new Error(`No compatible files found to install for ${slug}`); + } + const dirsPopulated = new Set(); + filesMoved.forEach((fileMoved: string) => { + const fileJson: string = path.join(path.dirname(fileMoved), 'index.json'); + fileCreateJson(fileJson, pkgVersion); + // A single archive can contain multiple formats (e.g. VST3 and CLAP), moved into different + // formatDir subdirectories - track each one, not just the first. + dirsPopulated.add(path.dirname(fileMoved)); + }); + return Array.from(dirsPopulated); +} + +// For apps/projects/presets, move entire directory without type subdirectories. +function installFlatDirectory( + typeDir: string, + dirSource: string, + dirSub: string, + pkgVersion: PackageVersion, + log: (...args: any) => void, +): string[] { + const dirTarget: string = path.join(typeDir, dirSub); + dirCreate(dirTarget); + dirMove(dirSource, dirTarget); + fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion); + // Ensure executable permissions for likely executables inside moved app/project/preset + try { + const movedFiles = dirRead(path.join(dirTarget, '**', '*')).filter(f => !dirIs(f)); + movedFiles.forEach((movedFile: string) => { + const ext = path.extname(movedFile).slice(1).toLowerCase(); + if (['', 'elf', 'exe'].includes(ext)) { + try { + fileExec(movedFile); + } catch (err) { + log(`Failed to set exec on ${movedFile}:`, err); + } + } + }); + } catch (err) { + log('Error while setting executable permissions:', err); + } + // Also handle macOS .app bundles: set exec on binaries in Contents/MacOS + try { + const appDirs = dirRead(path.join(dirTarget, '**', '*.app')).filter(d => dirIs(d)); + appDirs.forEach((appDir: string) => { + try { + const macosBinPattern = path.join(appDir, 'Contents', 'MacOS', '**', '*'); + const macosFiles = dirRead(macosBinPattern).filter(f => !dirIs(f)); + macosFiles.forEach((binFile: string) => { + try { + fileExec(binFile); + } catch (err) { + log(`Failed to set exec on app binary ${binFile}:`, err); + } + }); + } catch (err) { + log(`Error scanning .app contents for ${appDir}:`, err); + } + }); + } catch (err) { + log(err); + } + return [dirTarget]; +} diff --git a/tests/helpers/installTargets.test.ts b/tests/helpers/installTargets.test.ts new file mode 100644 index 0000000..b1b6d3b --- /dev/null +++ b/tests/helpers/installTargets.test.ts @@ -0,0 +1,212 @@ +import path from 'path'; +import { afterEach, beforeAll, expect, test, vi } from 'vitest'; +import * as fileHelpers from '../../src/helpers/file'; +import { dirCreate, dirDelete, fileCreate, fileExists, fileReadJson } from '../../src/helpers/file'; +import { installArchiveFile, installInstallerFile } from '../../src/helpers/installTargets'; +import { PLUGIN } from '../data/Plugin'; +import { PRESET } from '../data/Preset'; +import { PROJECT } from '../data/Project'; +import { PluginType } from '../../src/types/PluginType'; +import { RegistryType } from '../../src/types/Registry'; + +// These target install*() from src/helpers/installTargets.ts directly - unlike +// ManagerLocal.test.ts's install()/uninstall() round trips, none of these go through +// download/hash-check/elevation at all, so each destination (installer marker, sampler, embedded +// installer, plugin format sorting, flat move) can be asserted in isolation. + +const APP_DIR: string = path.join('test', 'installTargets'); + +beforeAll(() => { + dirDelete(APP_DIR); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test('installInstallerFile records a marker directory and package metadata without moving the installer itself', () => { + const typeDir = path.join(APP_DIR, 'installer', 'plugins'); + const filePath = path.join(APP_DIR, 'installer', 'downloads', 'surge.deb'); + dirCreate(path.dirname(filePath)); + fileCreate(filePath, 'not a real installer'); + const fileOpenSpy = vi.spyOn(fileHelpers, 'fileOpen').mockReturnValue(undefined as any); + + const dirsCreated = installInstallerFile({ + typeDir, + slug: 'surge-synthesizer/surge', + versionNum: '1.3.1', + pkgVersion: PLUGIN, + filePath, + isTestsFn: () => true, + }); + + expect(fileOpenSpy).toHaveBeenCalledWith(filePath); + const dirTarget = path.join(typeDir, 'Installers', 'surge-synthesizer/surge', '1.3.1'); + expect(dirsCreated).toEqual([dirTarget]); + expect(fileReadJson(path.join(dirTarget, 'index.json'))).toEqual(PLUGIN); +}); + +test('installArchiveFile with a Sampler package moves the whole extracted directory into Samplers/', async () => { + const typeDir = path.join(APP_DIR, 'sampler', 'plugins'); + const filePath = path.join(APP_DIR, 'sampler', 'downloads', 'my-sampler.zip'); + const dirSource = path.join(APP_DIR, 'sampler', 'extract-source'); + dirCreate(path.dirname(filePath)); + fileCreate(filePath, 'not a real zip'); + + const archiveExtractSpy = vi.spyOn(fileHelpers, 'archiveExtract').mockImplementation(async (_f, dirPath) => { + dirCreate(dirPath); + fileCreate(path.join(dirPath, 'sample.wav'), 'dummy'); + }); + + const samplerPkgVersion = { ...PLUGIN, type: PluginType.Sampler }; + const dirsCreated = await installArchiveFile({ + typeDir, + type: RegistryType.Plugins, + slug: 'test-org/my-sampler', + versionNum: '1.0.0', + pkgVersion: samplerPkgVersion, + filePath, + dirSource, + isTestsFn: () => true, + log: () => {}, + }); + + const dirTarget = path.join(typeDir, 'Samplers', 'test-org/my-sampler', '1.0.0'); + expect(dirsCreated).toEqual([dirTarget]); + expect(fileExists(path.join(dirTarget, 'sample.wav'))).toEqual(true); + expect(fileReadJson(path.join(dirTarget, 'index.json'))).toEqual(samplerPkgVersion); + + archiveExtractSpy.mockRestore(); +}); + +test('installArchiveFile runs an embedded .pkg/.dmg installer found inside the archive instead of moving it', async () => { + const typeDir = path.join(APP_DIR, 'embedded', 'projects'); + const filePath = path.join(APP_DIR, 'embedded', 'downloads', 'bundle.zip'); + const dirSource = path.join(APP_DIR, 'embedded', 'extract-source'); + dirCreate(path.dirname(filePath)); + fileCreate(filePath, 'not a real zip'); + + const archiveExtractSpy = vi.spyOn(fileHelpers, 'archiveExtract').mockImplementation(async (_f, dirPath) => { + dirCreate(dirPath); + fileCreate(path.join(dirPath, 'installer.dmg'), 'dummy'); + }); + const fileOpenSpy = vi.spyOn(fileHelpers, 'fileOpen').mockReturnValue(undefined as any); + + const dirsCreated = await installArchiveFile({ + typeDir, + type: RegistryType.Projects, + slug: 'kmt/banwer', + versionNum: '1.0.1', + pkgVersion: PROJECT, + filePath, + dirSource, + isTestsFn: () => true, + log: () => {}, + }); + + expect(fileOpenSpy).toHaveBeenCalledWith(path.join(dirSource, 'installer.dmg')); + const dirTarget = path.join(typeDir, 'Installers', 'kmt/banwer', '1.0.1'); + expect(dirsCreated).toEqual([dirTarget]); + expect(fileReadJson(path.join(dirTarget, 'index.json'))).toEqual(PROJECT); + + archiveExtractSpy.mockRestore(); + fileOpenSpy.mockRestore(); +}); + +test('installArchiveFile for a Plugins package sorts files into format-specific subdirectories', async () => { + const typeDir = path.join(APP_DIR, 'plugin-formats', 'plugins'); + const filePath = path.join(APP_DIR, 'plugin-formats', 'downloads', 'surge.zip'); + const dirSource = path.join(APP_DIR, 'plugin-formats', 'extract-source'); + dirCreate(path.dirname(filePath)); + fileCreate(filePath, 'not a real zip'); + + const archiveExtractSpy = vi.spyOn(fileHelpers, 'archiveExtract').mockImplementation(async (_f, dirPath) => { + dirCreate(dirPath); + fileCreate(path.join(dirPath, 'surge.vst3'), 'dummy'); + }); + + const dirsCreated = await installArchiveFile({ + typeDir, + type: RegistryType.Plugins, + slug: 'surge-synthesizer/surge', + versionNum: '1.3.1', + pkgVersion: PLUGIN, + filePath, + dirSource, + isTestsFn: () => true, + log: () => {}, + }); + + const dirTarget = path.join(typeDir, 'VST3', 'surge-synthesizer/surge', '1.3.1'); + expect(dirsCreated).toEqual([dirTarget]); + expect(fileExists(path.join(dirTarget, 'surge.vst3'))).toEqual(true); + expect(fileReadJson(path.join(dirTarget, 'index.json'))).toEqual(PLUGIN); + + archiveExtractSpy.mockRestore(); +}); + +test('installArchiveFile throws when the archive contains no files matching a known install format', async () => { + const typeDir = path.join(APP_DIR, 'unmapped', 'plugins'); + const filePath = path.join(APP_DIR, 'unmapped', 'downloads', 'mystery.zip'); + const dirSource = path.join(APP_DIR, 'unmapped', 'extract-source'); + dirCreate(path.dirname(filePath)); + fileCreate(filePath, 'not a real zip'); + + const archiveExtractSpy = vi.spyOn(fileHelpers, 'archiveExtract').mockImplementation(async (_f, dirPath) => { + dirCreate(dirPath); + // No recognizable plugin format inside - just an unrelated text file. + fileCreate(path.join(dirPath, 'readme.txt'), 'nothing to install'); + }); + + await expect( + installArchiveFile({ + typeDir, + type: RegistryType.Plugins, + slug: 'test-org/mystery', + versionNum: '1.0.0', + pkgVersion: PLUGIN, + filePath, + dirSource, + isTestsFn: () => true, + log: () => {}, + }), + ).rejects.toThrow('No compatible files found to install for test-org/mystery'); + + archiveExtractSpy.mockRestore(); +}); + +test('installArchiveFile for a Presets/Projects/Apps package moves the extracted directory as-is', async () => { + const typeDir = path.join(APP_DIR, 'flat', 'presets'); + const filePath = path.join(APP_DIR, 'flat', 'downloads', 'preset.zip'); + const dirSource = path.join(APP_DIR, 'flat', 'extract-source'); + dirCreate(path.dirname(filePath)); + fileCreate(filePath, 'not a real zip'); + + const archiveExtractSpy = vi.spyOn(fileHelpers, 'archiveExtract').mockImplementation(async (_f, dirPath) => { + dirCreate(dirPath); + fileCreate(path.join(dirPath, 'preset.data'), 'dummy'); + }); + const fileExecSpy = vi.spyOn(fileHelpers, 'fileExec').mockReturnValue(undefined as any); + + const dirsCreated = await installArchiveFile({ + typeDir, + type: RegistryType.Presets, + slug: 'jh/floating-rhodes', + versionNum: '1.0.0', + pkgVersion: PRESET, + filePath, + dirSource, + isTestsFn: () => true, + log: () => {}, + }); + + const dirTarget = path.join(typeDir, 'jh/floating-rhodes', '1.0.0'); + expect(dirsCreated).toEqual([dirTarget]); + expect(fileExists(path.join(dirTarget, 'preset.data'))).toEqual(true); + expect(fileReadJson(path.join(dirTarget, 'index.json'))).toEqual(PRESET); + // 'preset.data' isn't a likely-executable extension ('', 'elf', 'exe') - no exec call for it. + expect(fileExecSpy).not.toHaveBeenCalled(); + + archiveExtractSpy.mockRestore(); + fileExecSpy.mockRestore(); +});