mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-08 17:50:25 +00:00
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
import { createReadStream, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { createServer, type Server } from 'node:http'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import * as tar from 'tar'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const mockElectronApp = vi.hoisted(() => ({
|
|
isPackaged: false,
|
|
getAppPath: () => process.cwd(),
|
|
getVersion: () => '0.6.21',
|
|
getLocale: () => 'en',
|
|
}))
|
|
|
|
vi.mock('electron', () => ({
|
|
app: mockElectronApp,
|
|
}))
|
|
|
|
const originalEnv = { ...process.env }
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
|
const tempDirs: string[] = []
|
|
const servers: Server[] = []
|
|
|
|
function tempDir(prefix: string): string {
|
|
const dir = mkdtempSync(join(tmpdir(), prefix))
|
|
tempDirs.push(dir)
|
|
return dir
|
|
}
|
|
|
|
function createRuntimeFiles(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'), '')
|
|
}
|
|
writeFileSync(join(root, 'runtime-manifest.json'), JSON.stringify({
|
|
schema: 1,
|
|
platform: process.platform,
|
|
hermesAgentVersion: '0.17.0',
|
|
asset: { name: 'hermes-runtime-test.tar.gz' },
|
|
}))
|
|
}
|
|
|
|
function setPlatform(platform: NodeJS.Platform): void {
|
|
Object.defineProperty(process, 'platform', { value: platform })
|
|
}
|
|
|
|
async function createRuntimeArchive(): Promise<string> {
|
|
const source = tempDir('hermes-runtime-source-')
|
|
const archive = join(tempDir('hermes-runtime-archive-'), 'hermes-runtime-test.tar.gz')
|
|
createRuntimeFiles(source)
|
|
await tar.c({ gzip: true, cwd: source, file: archive }, ['.'])
|
|
return archive
|
|
}
|
|
|
|
async function serveFile(file: string): Promise<string> {
|
|
const server = createServer((request, response) => {
|
|
if (request.url !== '/hermes-runtime-test.tar.gz') {
|
|
response.writeHead(404)
|
|
response.end()
|
|
return
|
|
}
|
|
response.writeHead(200, { 'content-type': 'application/gzip' })
|
|
createReadStream(file).pipe(response)
|
|
})
|
|
servers.push(server)
|
|
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
|
const address = server.address()
|
|
if (!address || typeof address === 'string') throw new Error('test server did not bind to a port')
|
|
return `http://127.0.0.1:${address.port}/hermes-runtime-test.tar.gz`
|
|
}
|
|
|
|
describe('desktop runtime manager', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
process.env = { ...originalEnv }
|
|
process.env.HERMES_WEB_UI_HOME = tempDir('hermes-runtime-home-')
|
|
process.env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG = 'hermes-0.17.0-runtime'
|
|
mockElectronApp.isPackaged = false
|
|
mockElectronApp.getAppPath = () => process.cwd()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
process.env = { ...originalEnv }
|
|
if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform)
|
|
vi.resetModules()
|
|
await Promise.all(servers.splice(0).map(server => new Promise<void>(resolve => server.close(() => resolve()))))
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('downloads through a unique temp file instead of reusing a stale runtime .download path', async () => {
|
|
const archive = await createRuntimeArchive()
|
|
process.env.HERMES_DESKTOP_RUNTIME_URL = await serveFile(archive)
|
|
|
|
const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
|
|
const staleDownloadPath = join(
|
|
process.env.HERMES_WEB_UI_HOME!,
|
|
'desktop-runtime',
|
|
'hermes',
|
|
'0.17.0',
|
|
'hermes-runtime-test.tar.gz.download',
|
|
)
|
|
mkdirSync(staleDownloadPath, { recursive: true })
|
|
|
|
const { ensureDesktopRuntime } = await import('../../packages/desktop/src/main/runtime-manager')
|
|
await ensureDesktopRuntime()
|
|
|
|
const runtimeRoot = join(
|
|
process.env.HERMES_WEB_UI_HOME!,
|
|
'desktop-runtime',
|
|
'hermes',
|
|
'0.17.0',
|
|
runtimePlatformKey(),
|
|
)
|
|
expect(existsSync(staleDownloadPath)).toBe(true)
|
|
expect(existsSync(join(runtimeRoot, 'runtime-manifest.json'))).toBe(true)
|
|
expect(existsSync(join(process.env.HERMES_WEB_UI_HOME!, 'desktop-runtime', 'active-version.json'))).toBe(true)
|
|
})
|
|
|
|
it('accepts Windows runtime archives that contain hermes.cmd without hermes.exe', async () => {
|
|
setPlatform('win32')
|
|
const archive = await createRuntimeArchive()
|
|
process.env.HERMES_DESKTOP_RUNTIME_URL = await serveFile(archive)
|
|
|
|
const { runtimePlatformKey } = await import('../../packages/desktop/src/main/runtime-paths')
|
|
const { ensureDesktopRuntime } = await import('../../packages/desktop/src/main/runtime-manager')
|
|
await ensureDesktopRuntime()
|
|
|
|
const runtimeRoot = join(
|
|
process.env.HERMES_WEB_UI_HOME!,
|
|
'desktop-runtime',
|
|
'hermes',
|
|
'0.17.0',
|
|
runtimePlatformKey(),
|
|
)
|
|
expect(existsSync(join(runtimeRoot, 'python', 'Scripts', 'hermes.cmd'))).toBe(true)
|
|
expect(existsSync(join(runtimeRoot, 'python', 'Scripts', 'hermes.exe'))).toBe(false)
|
|
})
|
|
})
|