runtime-paths.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  5. const mockElectronApp = vi.hoisted(() => ({
  6. isPackaged: false,
  7. getAppPath: () => process.cwd(),
  8. getVersion: () => '0.6.11',
  9. getLocale: () => 'en',
  10. }))
  11. vi.mock('electron', () => ({
  12. app: mockElectronApp,
  13. }))
  14. const originalEnv = { ...process.env }
  15. const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
  16. const tempDirs: string[] = []
  17. function tempDir(): string {
  18. const dir = mkdtempSync(join(tmpdir(), 'hermes-desktop-runtime-paths-'))
  19. tempDirs.push(dir)
  20. return dir
  21. }
  22. function createRuntime(root: string, version: string) {
  23. if (process.platform === 'win32') {
  24. mkdirSync(join(root, 'python', 'Scripts'), { recursive: true })
  25. mkdirSync(join(root, 'node'), { recursive: true })
  26. mkdirSync(join(root, 'git', 'cmd'), { recursive: true })
  27. writeFileSync(join(root, 'python', 'python.exe'), '')
  28. writeFileSync(join(root, 'python', 'Scripts', 'hermes.cmd'), '')
  29. writeFileSync(join(root, 'node', 'node.exe'), '')
  30. writeFileSync(join(root, 'git', 'cmd', 'git.exe'), '')
  31. } else {
  32. mkdirSync(join(root, 'python', 'bin'), { recursive: true })
  33. mkdirSync(join(root, 'node', 'bin'), { recursive: true })
  34. writeFileSync(join(root, 'python', 'bin', 'python3'), '')
  35. writeFileSync(join(root, 'python', 'bin', 'hermes'), '')
  36. writeFileSync(join(root, 'node', 'bin', 'node'), '')
  37. }
  38. writeFileSync(join(root, 'runtime-manifest.json'), JSON.stringify({
  39. schema: 1,
  40. platform: process.platform,
  41. hermesAgentVersion: version,
  42. }))
  43. }
  44. function createRuntimeWithoutManifest(root: string) {
  45. if (process.platform === 'win32') {
  46. mkdirSync(join(root, 'python', 'Scripts'), { recursive: true })
  47. mkdirSync(join(root, 'node'), { recursive: true })
  48. mkdirSync(join(root, 'git', 'cmd'), { recursive: true })
  49. writeFileSync(join(root, 'python', 'python.exe'), '')
  50. writeFileSync(join(root, 'python', 'Scripts', 'hermes.cmd'), '')
  51. writeFileSync(join(root, 'node', 'node.exe'), '')
  52. writeFileSync(join(root, 'git', 'cmd', 'git.exe'), '')
  53. } else {
  54. mkdirSync(join(root, 'python', 'bin'), { recursive: true })
  55. mkdirSync(join(root, 'node', 'bin'), { recursive: true })
  56. writeFileSync(join(root, 'python', 'bin', 'python3'), '')
  57. writeFileSync(join(root, 'python', 'bin', 'hermes'), '')
  58. writeFileSync(join(root, 'node', 'bin', 'node'), '')
  59. }
  60. }
  61. function setPlatform(platform: NodeJS.Platform): void {
  62. Object.defineProperty(process, 'platform', { value: platform })
  63. }
  64. describe('desktop runtime paths', () => {
  65. beforeEach(() => {
  66. vi.resetModules()
  67. process.env = { ...originalEnv }
  68. const resourcesPath = tempDir()
  69. process.resourcesPath = resourcesPath
  70. process.env.HERMES_WEB_UI_HOME = tempDir()
  71. mockElectronApp.isPackaged = false
  72. mockElectronApp.getAppPath = () => process.cwd()
  73. })
  74. afterEach(() => {
  75. process.env = { ...originalEnv }
  76. if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform)
  77. vi.resetModules()
  78. for (const dir of tempDirs.splice(0)) {
  79. rmSync(dir, { recursive: true, force: true })
  80. }
  81. })
  82. it('uses the downloaded runtime in packaged builds even when stale install resources exist', async () => {
  83. mkdirSync(join(process.resourcesPath, 'python'), { recursive: true })
  84. mkdirSync(join(process.resourcesPath, 'node'), { recursive: true })
  85. mkdirSync(join(process.resourcesPath, 'git'), { recursive: true })
  86. const { resolveRuntimeResourceDir } = await import('../../packages/desktop/src/main/runtime-paths')
  87. const runtimeRoot = tempDir()
  88. expect(resolveRuntimeResourceDir('python', true, process.resourcesPath, runtimeRoot)).toBe(join(runtimeRoot, 'python'))
  89. expect(resolveRuntimeResourceDir('node', true, process.resourcesPath, runtimeRoot)).toBe(join(runtimeRoot, 'node'))
  90. expect(resolveRuntimeResourceDir('git', true, process.resourcesPath, runtimeRoot)).toBe(join(runtimeRoot, 'git'))
  91. })
  92. it('uses app resources for development runtime paths', async () => {
  93. const appPath = tempDir()
  94. const { resolveRuntimeResourceDir, runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  95. const runtimeRoot = tempDir()
  96. expect(resolveRuntimeResourceDir('python', false, appPath, runtimeRoot)).toBe(join(appPath, 'resources', 'python', runtimePlatformKey()))
  97. expect(resolveRuntimeResourceDir('node', false, appPath, runtimeRoot)).toBe(join(appPath, 'resources', 'node', runtimePlatformKey()))
  98. expect(resolveRuntimeResourceDir('git', false, appPath, runtimeRoot)).toBe(join(appPath, 'resources', 'git', runtimePlatformKey()))
  99. })
  100. it('uses explicit runtime override for development runtime paths', async () => {
  101. const runtimeRoot = tempDir()
  102. process.env.HERMES_DESKTOP_RUNTIME_DIR = runtimeRoot
  103. createRuntime(runtimeRoot, '0.17.0')
  104. const { pythonDir, nodeDir, gitDir } = await import('../../packages/desktop/src/main/paths')
  105. expect(pythonDir()).toBe(join(runtimeRoot, 'python'))
  106. expect(nodeDir()).toBe(join(runtimeRoot, 'node'))
  107. expect(gitDir()).toBe(join(runtimeRoot, 'git'))
  108. })
  109. it('uses active-version.json paths for startup while keeping the current target runtime path', async () => {
  110. const homeDir = tempDir()
  111. const appPath = tempDir()
  112. const runtimeDir = tempDir()
  113. const storageRoot = tempDir()
  114. const webUiDir = join(storageRoot, 'webui', '0.6.10')
  115. const staleAbsoluteWebUiDir = tempDir()
  116. process.env.HERMES_WEB_UI_HOME = homeDir
  117. mockElectronApp.getAppPath = () => appPath
  118. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  119. createRuntime(runtimeDir, '0.15.1')
  120. mkdirSync(join(webUiDir, 'dist', 'server'), { recursive: true })
  121. writeFileSync(join(webUiDir, 'dist', 'server', 'index.js'), '')
  122. mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
  123. writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
  124. schema: 1,
  125. hermesRuntimeVersion: '0.15.1',
  126. webUiVersion: '0.6.10',
  127. runtimeDirectory: runtimeDir,
  128. runtimeRootDirectory: storageRoot,
  129. webUiDirectory: staleAbsoluteWebUiDir,
  130. platform: runtimePlatformKey(),
  131. }))
  132. const { desktopRuntimeDir, targetDesktopRuntimeDir, webuiDir } = await import('../../packages/desktop/src/main/paths')
  133. expect(desktopRuntimeDir()).toBe(runtimeDir)
  134. expect(webuiDir()).toBe(webUiDir)
  135. expect(targetDesktopRuntimeDir()).toBe(join(storageRoot, 'hermes', '0.19.0', runtimePlatformKey()))
  136. })
  137. it('falls back to the bundled Web UI when the active Web UI directory is incomplete', async () => {
  138. const homeDir = tempDir()
  139. const activeWebUiDir = join(homeDir, 'webui', '0.6.26')
  140. const bundledWebUiDir = join(process.resourcesPath, 'webui')
  141. process.env.HERMES_WEB_UI_HOME = homeDir
  142. mockElectronApp.isPackaged = true
  143. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  144. mkdirSync(activeWebUiDir, { recursive: true })
  145. mkdirSync(join(bundledWebUiDir, 'dist', 'server'), { recursive: true })
  146. writeFileSync(join(bundledWebUiDir, 'dist', 'server', 'index.js'), '')
  147. mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
  148. writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
  149. schema: 1,
  150. webUiVersion: '0.6.26',
  151. webUiDirectory: activeWebUiDir,
  152. platform: runtimePlatformKey(),
  153. }))
  154. const { webuiDir, webuiServerEntry } = await import('../../packages/desktop/src/main/paths')
  155. expect(webuiDir()).toBe(bundledWebUiDir)
  156. expect(webuiServerEntry()).toBe(join(bundledWebUiDir, 'dist', 'server', 'index.js'))
  157. const active = JSON.parse(readFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), 'utf-8'))
  158. expect(active.webUiDirectory).toBeUndefined()
  159. expect(active.webUiVersion).toBeUndefined()
  160. })
  161. it('does not scan or delete Web UI versions from the legacy directory', async () => {
  162. const homeDir = tempDir()
  163. const appPath = tempDir()
  164. const legacyWebUiDir = join(homeDir, 'webui', '0.6.22')
  165. const currentWebUiDir = join(homeDir, 'webui', '0.6.23')
  166. process.env.HERMES_WEB_UI_HOME = homeDir
  167. mockElectronApp.getAppPath = () => appPath
  168. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  169. mkdirSync(join(legacyWebUiDir, 'dist', 'server'), { recursive: true })
  170. writeFileSync(join(legacyWebUiDir, 'package.json'), JSON.stringify({ version: '0.6.22' }))
  171. writeFileSync(join(legacyWebUiDir, 'dist', 'server', 'index.js'), '')
  172. mkdirSync(currentWebUiDir, { recursive: true })
  173. writeFileSync(join(currentWebUiDir, 'package.json'), JSON.stringify({ version: '0.6.23' }))
  174. mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
  175. writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
  176. schema: 1,
  177. webUiVersion: '0.6.22',
  178. webUiDirectory: legacyWebUiDir,
  179. platform: runtimePlatformKey(),
  180. }))
  181. const { webuiDir } = await import('../../packages/desktop/src/main/paths')
  182. expect(webuiDir()).not.toBe(legacyWebUiDir)
  183. expect(existsSync(legacyWebUiDir)).toBe(true)
  184. expect(existsSync(currentWebUiDir)).toBe(true)
  185. })
  186. it('falls back to the newest installed runtime when the active runtime was deleted', async () => {
  187. const homeDir = tempDir()
  188. const deletedRuntimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', 'missing')
  189. process.env.HERMES_WEB_UI_HOME = homeDir
  190. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  191. const platformKey = runtimePlatformKey()
  192. const runtime015 = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', platformKey)
  193. const runtime016 = join(homeDir, 'desktop-runtime', 'hermes', '0.16.0', platformKey)
  194. createRuntime(runtime015, '0.15.2')
  195. createRuntime(runtime016, '0.16.0')
  196. mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
  197. writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
  198. schema: 1,
  199. hermesRuntimeVersion: '0.15.2',
  200. runtimeDirectory: deletedRuntimeDir,
  201. platform: platformKey,
  202. }))
  203. const { desktopRuntimeDir } = await import('../../packages/desktop/src/main/paths')
  204. expect(desktopRuntimeDir()).toBe(runtime016)
  205. })
  206. it('keeps using the active local runtime even when the packaged runtime version is newer', async () => {
  207. const homeDir = tempDir()
  208. process.env.HERMES_WEB_UI_HOME = homeDir
  209. process.env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG = 'hermes-0.16.0-runtime'
  210. mockElectronApp.isPackaged = true
  211. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  212. const platformKey = runtimePlatformKey()
  213. const runtime015 = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', platformKey)
  214. const runtime016 = join(homeDir, 'desktop-runtime', 'hermes', '0.16.0', platformKey)
  215. createRuntime(runtime015, '0.15.2')
  216. createRuntime(runtime016, '0.16.0')
  217. mkdirSync(join(homeDir, 'desktop-runtime'), { recursive: true })
  218. writeFileSync(join(homeDir, 'desktop-runtime', 'active-version.json'), JSON.stringify({
  219. schema: 1,
  220. hermesRuntimeVersion: '0.15.2',
  221. runtimeDirectory: runtime015,
  222. platform: platformKey,
  223. }))
  224. const { desktopRuntimeDir, targetDesktopRuntimeDir } = await import('../../packages/desktop/src/main/paths')
  225. expect(desktopRuntimeDir()).toBe(runtime015)
  226. expect(targetDesktopRuntimeDir()).toBe(runtime016)
  227. })
  228. it('uses installed runtime directories under desktop-runtime/hermes when executable files are present', async () => {
  229. const homeDir = tempDir()
  230. process.env.HERMES_WEB_UI_HOME = homeDir
  231. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  232. const runtimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', runtimePlatformKey())
  233. const targetRuntimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.19.0', runtimePlatformKey())
  234. createRuntimeWithoutManifest(runtimeDir)
  235. const { desktopRuntimeDir, targetDesktopRuntimeDir } = await import('../../packages/desktop/src/main/paths')
  236. expect(desktopRuntimeDir()).toBe(runtimeDir)
  237. expect(targetDesktopRuntimeDir()).toBe(targetRuntimeDir)
  238. })
  239. it('treats hermes.cmd as the required Windows runtime entry point', async () => {
  240. setPlatform('win32')
  241. const homeDir = tempDir()
  242. process.env.HERMES_WEB_UI_HOME = homeDir
  243. mockElectronApp.isPackaged = true
  244. const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
  245. const runtimeDir = join(homeDir, 'desktop-runtime', 'hermes', '0.15.2', runtimePlatformKey())
  246. createRuntime(runtimeDir, '0.15.2')
  247. const { desktopRuntimeDir, hermesBin } = await import('../../packages/desktop/src/main/paths')
  248. expect(desktopRuntimeDir()).toBe(runtimeDir)
  249. expect(hermesBin()).toBe(join(runtimeDir, 'python', 'Scripts', 'hermes.cmd'))
  250. expect(existsSync(join(runtimeDir, 'python', 'Scripts', 'hermes.exe'))).toBe(false)
  251. })
  252. })
粤ICP备19079148号