83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises'
|
||
|
|
import { tmpdir } from 'os'
|
||
|
|
import { join } from 'path'
|
||
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||
|
|
import YAML from 'js-yaml'
|
||
|
|
|
||
|
|
const originalHermesHome = process.env.HERMES_HOME
|
||
|
|
const tempHomes: string[] = []
|
||
|
|
let hermesHome = ''
|
||
|
|
|
||
|
|
async function loadHelpers() {
|
||
|
|
vi.resetModules()
|
||
|
|
process.env.HERMES_HOME = hermesHome
|
||
|
|
return import('../../packages/server/src/services/config-helpers')
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
hermesHome = await mkdtemp(join(tmpdir(), 'hermes-config-helpers-'))
|
||
|
|
tempHomes.push(hermesHome)
|
||
|
|
await mkdir(hermesHome, { recursive: true })
|
||
|
|
})
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
vi.resetModules()
|
||
|
|
if (originalHermesHome === undefined) delete process.env.HERMES_HOME
|
||
|
|
else process.env.HERMES_HOME = originalHermesHome
|
||
|
|
await Promise.all(tempHomes.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||
|
|
hermesHome = ''
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('config-helpers locked file updates', () => {
|
||
|
|
it('merges concurrent config.yaml updates by re-reading under the file lock', async () => {
|
||
|
|
await writeFile(join(hermesHome, 'config.yaml'), 'model:\n default: old\n', 'utf-8')
|
||
|
|
const { updateConfigYaml } = await loadHelpers()
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
updateConfigYaml(async (cfg) => {
|
||
|
|
await new Promise(resolve => setTimeout(resolve, 25))
|
||
|
|
cfg.model.default = 'glm-5.1'
|
||
|
|
return cfg
|
||
|
|
}),
|
||
|
|
updateConfigYaml((cfg) => {
|
||
|
|
cfg.platforms = cfg.platforms || {}
|
||
|
|
cfg.platforms.api_server = { extra: { port: 8648 } }
|
||
|
|
return cfg
|
||
|
|
}),
|
||
|
|
])
|
||
|
|
|
||
|
|
const config = YAML.load(await readFile(join(hermesHome, 'config.yaml'), 'utf-8')) as any
|
||
|
|
expect(config.model.default).toBe('glm-5.1')
|
||
|
|
expect(config.platforms.api_server.extra.port).toBe(8648)
|
||
|
|
await expect(readFile(join(hermesHome, 'config.yaml.bak'), 'utf-8')).resolves.toContain('model:')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('serializes concurrent .env updates without losing keys', async () => {
|
||
|
|
await writeFile(join(hermesHome, '.env'), 'OPENROUTER_API_KEY=keep\n', 'utf-8')
|
||
|
|
const { saveEnvValue } = await loadHelpers()
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
saveEnvValue('DEEPSEEK_API_KEY', 'deepseek'),
|
||
|
|
saveEnvValue('MOONSHOT_API_KEY', 'moonshot'),
|
||
|
|
])
|
||
|
|
|
||
|
|
const env = await readFile(join(hermesHome, '.env'), 'utf-8')
|
||
|
|
expect(env).toContain('OPENROUTER_API_KEY=keep')
|
||
|
|
expect(env).toContain('DEEPSEEK_API_KEY=deepseek')
|
||
|
|
expect(env).toContain('MOONSHOT_API_KEY=moonshot')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('skips writing config.yaml when an updater returns write false', async () => {
|
||
|
|
const configPath = join(hermesHome, 'config.yaml')
|
||
|
|
await writeFile(configPath, 'model:\n default: old\n', 'utf-8')
|
||
|
|
const before = await readFile(configPath, 'utf-8')
|
||
|
|
const { updateConfigYaml } = await loadHelpers()
|
||
|
|
|
||
|
|
const result = await updateConfigYaml((cfg) => ({ data: cfg, result: 'unchanged', write: false }))
|
||
|
|
|
||
|
|
expect(result).toBe('unchanged')
|
||
|
|
await expect(readFile(configPath, 'utf-8')).resolves.toBe(before)
|
||
|
|
await expect(readFile(`${configPath}.bak`, 'utf-8')).rejects.toMatchObject({ code: 'ENOENT' })
|
||
|
|
})
|
||
|
|
})
|