| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
- const mockElectronApp = vi.hoisted(() => ({
- isPackaged: false,
- getAppPath: () => process.cwd(),
- getVersion: () => '0.6.11',
- getLocale: () => 'en',
- }))
- vi.mock('electron', () => ({
- app: mockElectronApp,
- }))
- const originalEnv = { ...process.env }
- const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
- const tempDirs: string[] = []
- function tempDir(): string {
- const dir = mkdtempSync(join(tmpdir(), 'hermes-desktop-runtime-paths-'))
- tempDirs.push(dir)
- return dir
- }
- function createRuntime(root: string, version: string) {
- if (process.platform === 'win32') {
- mkdirSync(join(root, 'python', 'Scripts'), { recursive: true })
- mkdirSync(join(root, 'node'), { recursive: true })
- mkdirSync(join(root, 'git', 'cmd'), { recursive: true })
- writeFileSync(join(root, 'python', 'python.exe'), '')
- writeFileSync(join(root, 'python', 'Scripts', 'hermes.cmd'), '')
- writeFileSync(join(root, 'node', 'node.exe'), '')
- writeFileSync(join(root, 'git', 'cmd', 'git.exe'), '')
- } else {
- mkdirSync(join(root, 'python', 'bin'), { recursive: true })
- mkdirSync(join(root, 'node', 'bin'), { recursive: true })
- writeFileSync(join(root, 'python', 'bin', 'python3'), '')
- writeFileSync(join(root, 'python', 'bin', 'hermes'), '')
- writeFileSync(join(root, 'node', 'bin', 'node'), '')
- }
- writeFileSync(join(root, 'runtime-manifest.json'), JSON.stringify({
- schema: 1,
- platform: process.platform,
- hermesAgentVersion: version,
- }))
- }
- function createRuntimeWithoutManifest(root: string) {
- if (process.platform === 'win32') {
- mkdirSync(join(root, 'python', 'Scripts'), { recursive: true })
- mkdirSync(join(root, 'node'), { recursive: true })
- mkdirSync(join(root, 'git', 'cmd'), { recursive: true })
- writeFileSync(join(root, 'python', 'python.exe'), '')
- writeFileSync(join(root, 'python', 'Scripts', 'hermes.cmd'), '')
- writeFileSync(join(root, 'node', 'node.exe'), '')
- writeFileSync(join(root, 'git', 'cmd', 'git.exe'), '')
- } else {
- mkdirSync(join(root, 'python', 'bin'), { recursive: true })
- mkdirSync(join(root, 'node', 'bin'), { recursive: true })
- writeFileSync(join(root, 'python', 'bin', 'python3'), '')
- writeFileSync(join(root, 'python', 'bin', 'hermes'), '')
- writeFileSync(join(root, 'node', 'bin', 'node'), '')
- }
- }
- function setPlatform(platform: NodeJS.Platform): void {
- Object.defineProperty(process, 'platform', { value: platform })
- }
- describe('desktop runtime paths', () => {
- beforeEach(() => {
- vi.resetModules()
- process.env = { ...originalEnv }
- const resourcesPath = tempDir()
- process.resourcesPath = resourcesPath
- process.env.HERMES_WEB_UI_HOME = tempDir()
- mockElectronApp.isPackaged = false
- mockElectronApp.getAppPath = () => process.cwd()
- })
- afterEach(() => {
- process.env = { ...originalEnv }
- if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform)
- vi.resetModules()
- for (const dir of tempDirs.splice(0)) {
- rmSync(dir, { recursive: true, force: true })
- }
- })
- it('uses the downloaded runtime in packaged builds even when stale install resources exist', async () => {
- mkdirSync(join(process.resourcesPath, 'python'), { recursive: true })
- mkdirSync(join(process.resourcesPath, 'node'), { recursive: true })
- mkdirSync(join(process.resourcesPath, 'git'), { recursive: true })
- const { resolveRuntimeResourceDir } = await import('../../packages/desktop/src/main/runtime-paths')
- const runtimeRoot = tempDir()
- expect(resolveRuntimeResourceDir('python', true, process.resourcesPath, runtimeRoot)).toBe(join(runtimeRoot, 'python'))
- expect(resolveRuntimeResourceDir('node', true, process.resourcesPath, runtimeRoot)).toBe(join(runtimeRoot, 'node'))
- expect(resolveRuntimeResourceDir('git', true, process.resourcesPath, runtimeRoot)).toBe(join(runtimeRoot, 'git'))
- })
- it('uses app resources for development runtime paths', async () => {
- const appPath = tempDir()
- const { resolveRuntimeResourceDir, runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- const runtimeRoot = tempDir()
- expect(resolveRuntimeResourceDir('python', false, appPath, runtimeRoot)).toBe(join(appPath, 'resources', 'python', runtimePlatformKey()))
- expect(resolveRuntimeResourceDir('node', false, appPath, runtimeRoot)).toBe(join(appPath, 'resources', 'node', runtimePlatformKey()))
- expect(resolveRuntimeResourceDir('git', false, appPath, runtimeRoot)).toBe(join(appPath, 'resources', 'git', runtimePlatformKey()))
- })
- it('uses explicit runtime override for development runtime paths', async () => {
- const runtimeRoot = tempDir()
- process.env.HERMES_DESKTOP_RUNTIME_DIR = runtimeRoot
- createRuntime(runtimeRoot, '0.17.0')
- const { pythonDir, nodeDir, gitDir } = await import('../../packages/desktop/src/main/paths')
- expect(pythonDir()).toBe(join(runtimeRoot, 'python'))
- expect(nodeDir()).toBe(join(runtimeRoot, 'node'))
- expect(gitDir()).toBe(join(runtimeRoot, 'git'))
- })
- it('uses active-version.json paths for startup while keeping the current target runtime path', async () => {
- const homeDir = tempDir()
- const appPath = tempDir()
- const runtimeDir = tempDir()
- const storageRoot = tempDir()
- const webUiDir = join(storageRoot, 'webui', '0.6.10')
- const staleAbsoluteWebUiDir = tempDir()
- process.env.HERMES_WEB_UI_HOME = homeDir
- mockElectronApp.getAppPath = () => appPath
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- createRuntime(runtimeDir, '0.15.1')
- mkdirSync(join(webUiDir, 'dist', 'server'), { recursive: true })
- writeFileSync(join(webUiDir, 'dist', 'server', 'index.js'), '')
- mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
- writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
- schema: 1,
- hermesRuntimeVersion: '0.15.1',
- webUiVersion: '0.6.10',
- runtimeDirectory: runtimeDir,
- runtimeRootDirectory: storageRoot,
- webUiDirectory: staleAbsoluteWebUiDir,
- platform: runtimePlatformKey(),
- }))
- const { desktopRuntimeDir, targetDesktopRuntimeDir, webuiDir } = await import('../../packages/desktop/src/main/paths')
- expect(desktopRuntimeDir()).toBe(runtimeDir)
- expect(webuiDir()).toBe(webUiDir)
- expect(targetDesktopRuntimeDir()).toBe(join(storageRoot, 'hermes', '0.19.0', runtimePlatformKey()))
- })
- it('falls back to the bundled Web UI when the active Web UI directory is incomplete', async () => {
- const homeDir = tempDir()
- const activeWebUiDir = join(homeDir, 'webui', '0.6.26')
- const bundledWebUiDir = join(process.resourcesPath, 'webui')
- process.env.HERMES_WEB_UI_HOME = homeDir
- mockElectronApp.isPackaged = true
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- mkdirSync(activeWebUiDir, { recursive: true })
- mkdirSync(join(bundledWebUiDir, 'dist', 'server'), { recursive: true })
- writeFileSync(join(bundledWebUiDir, 'dist', 'server', 'index.js'), '')
- mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
- writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
- schema: 1,
- webUiVersion: '0.6.26',
- webUiDirectory: activeWebUiDir,
- platform: runtimePlatformKey(),
- }))
- const { webuiDir, webuiServerEntry } = await import('../../packages/desktop/src/main/paths')
- expect(webuiDir()).toBe(bundledWebUiDir)
- expect(webuiServerEntry()).toBe(join(bundledWebUiDir, 'dist', 'server', 'index.js'))
- const active = JSON.parse(readFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), 'utf-8'))
- expect(active.webUiDirectory).toBeUndefined()
- expect(active.webUiVersion).toBeUndefined()
- })
- it('does not scan or delete Web UI versions from the legacy directory', async () => {
- const homeDir = tempDir()
- const appPath = tempDir()
- const legacyWebUiDir = join(homeDir, 'webui', '0.6.22')
- const currentWebUiDir = join(homeDir, 'webui', '0.6.23')
- process.env.HERMES_WEB_UI_HOME = homeDir
- mockElectronApp.getAppPath = () => appPath
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- mkdirSync(join(legacyWebUiDir, 'dist', 'server'), { recursive: true })
- writeFileSync(join(legacyWebUiDir, 'package.json'), JSON.stringify({ version: '0.6.22' }))
- writeFileSync(join(legacyWebUiDir, 'dist', 'server', 'index.js'), '')
- mkdirSync(currentWebUiDir, { recursive: true })
- writeFileSync(join(currentWebUiDir, 'package.json'), JSON.stringify({ version: '0.6.23' }))
- mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
- writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
- schema: 1,
- webUiVersion: '0.6.22',
- webUiDirectory: legacyWebUiDir,
- platform: runtimePlatformKey(),
- }))
- const { webuiDir } = await import('../../packages/desktop/src/main/paths')
- expect(webuiDir()).not.toBe(legacyWebUiDir)
- expect(existsSync(legacyWebUiDir)).toBe(true)
- expect(existsSync(currentWebUiDir)).toBe(true)
- })
- it('falls back to the newest installed runtime when the active runtime was deleted', async () => {
- const homeDir = tempDir()
- const deletedRuntimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', 'missing')
- process.env.HERMES_WEB_UI_HOME = homeDir
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- const platformKey = runtimePlatformKey()
- const runtime015 = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', platformKey)
- const runtime016 = join(homeDir, 'desktop-runtime', 'hermes', '0.16.0', platformKey)
- createRuntime(runtime015, '0.15.2')
- createRuntime(runtime016, '0.16.0')
- mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
- writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
- schema: 1,
- hermesRuntimeVersion: '0.15.2',
- runtimeDirectory: deletedRuntimeDir,
- platform: platformKey,
- }))
- const { desktopRuntimeDir } = await import('../../packages/desktop/src/main/paths')
- expect(desktopRuntimeDir()).toBe(runtime016)
- })
- it('keeps using the active local runtime even when the packaged runtime version is newer', async () => {
- const homeDir = tempDir()
- process.env.HERMES_WEB_UI_HOME = homeDir
- process.env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG = 'hermes-0.16.0-runtime'
- mockElectronApp.isPackaged = true
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- const platformKey = runtimePlatformKey()
- const runtime015 = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', platformKey)
- const runtime016 = join(homeDir, 'desktop-runtime', 'hermes', '0.16.0', platformKey)
- createRuntime(runtime015, '0.15.2')
- createRuntime(runtime016, '0.16.0')
- mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
- writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
- schema: 1,
- hermesRuntimeVersion: '0.15.2',
- runtimeDirectory: runtime015,
- platform: platformKey,
- }))
- const { desktopRuntimeDir, targetDesktopRuntimeDir } = await import('../../packages/desktop/src/main/paths')
- expect(desktopRuntimeDir()).toBe(runtime015)
- expect(targetDesktopRuntimeDir()).toBe(runtime016)
- })
- it('uses installed runtime directories under desktop-runtime/hermes when executable files are present', async () => {
- const homeDir = tempDir()
- process.env.HERMES_WEB_UI_HOME = homeDir
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- const runtimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', runtimePlatformKey())
- const targetRuntimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.19.0', runtimePlatformKey())
- createRuntimeWithoutManifest(runtimeDir)
- const { desktopRuntimeDir, targetDesktopRuntimeDir } = await import('../../packages/desktop/src/main/paths')
- expect(desktopRuntimeDir()).toBe(runtimeDir)
- expect(targetDesktopRuntimeDir()).toBe(targetRuntimeDir)
- })
- it('treats hermes.cmd as the required Windows runtime entry point', async () => {
- setPlatform('win32')
- const homeDir = tempDir()
- process.env.HERMES_WEB_UI_HOME = homeDir
- mockElectronApp.isPackaged = true
- const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
- const runtimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', runtimePlatformKey())
- createRuntime(runtimeDir, '0.15.2')
- const { desktopRuntimeDir, hermesBin } = await import('../../packages/desktop/src/main/paths')
- expect(desktopRuntimeDir()).toBe(runtimeDir)
- expect(hermesBin()).toBe(join(runtimeDir, 'python', 'Scripts', 'hermes.cmd'))
- expect(existsSync(join(runtimeDir, 'python', 'Scripts', 'hermes.exe'))).toBe(false)
- })
- })
|