Files
Hermes-ui/tests/server/config-helpers-file-lock.test.ts
T
ekko 9a9416c99c Fix bridge history, profile models, and Windows gateway handling (#845)
* feat: support profile-aware group chat bridge flows

* feat: route cron jobs through hermes cli

* Fix group chat routing and isolate bridge tests

* Add Grok image-to-video media skill

* Default Grok videos to media directory

* Fix bridge profile fallback and cron repeat clearing

* Refine bridge chat and gateway platform handling

* Filter bridge tool-call text deltas

* Preserve structured bridge chat history

* Prepare beta release build artifacts

* Fix Windows run profile resolution

* Fix Windows path compatibility checks

* Fix profile-scoped model page display

* Hide Windows subprocess windows for jobs and updates

* Hide Windows file backend subprocess windows

* Avoid Windows gateway restart lock conflicts

* Treat Windows gateway lock as running on startup

* Force release Windows gateway lock on restart

* Tighten Windows gateway lock cleanup

* Update chat e2e source expectation

* Bump package version to 0.5.30

---------

Co-authored-by: Codex <codex@openai.com>
2026-05-19 16:09:59 +08:00

147 lines
4.9 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('rejects invalid .env keys instead of writing keyless lines', async () => {
const envPath = join(hermesHome, '.env')
await writeFile(envPath, 'OPENROUTER_API_KEY=keep\n', 'utf-8')
const { saveEnvValue } = await loadHelpers()
await expect(saveEnvValue('', 'leaked-value')).rejects.toThrow('Invalid .env key')
await expect(saveEnvValue('=BROKEN', 'leaked-value')).rejects.toThrow('Invalid .env key')
const env = await readFile(envPath, 'utf-8')
expect(env).toBe('OPENROUTER_API_KEY=keep\n')
expect(env).not.toContain('=leaked-value')
})
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' })
})
it('strips api_server config before gateway restart', async () => {
const { stripLegacyApiServerGatewayConfig } = await loadHelpers()
const result = stripLegacyApiServerGatewayConfig({
model: { default: 'glm-5.1' },
platforms: {
api_server: {
enabled: true,
key: '',
cors_origins: '*',
extra: {
port: 8642,
host: '127.0.0.1',
},
},
feishu: {
enabled: true,
},
},
})
expect(result.changed).toBe(true)
expect(result.config).toEqual({
model: { default: 'glm-5.1' },
platforms: {
feishu: {
enabled: true,
},
},
})
})
it('removes custom api_server fields as well', async () => {
const { stripLegacyApiServerGatewayConfig } = await loadHelpers()
const result = stripLegacyApiServerGatewayConfig({
platforms: {
api_server: {
key: 'custom-key',
cors_origins: 'https://example.com',
extra: {
port: 8642,
host: '127.0.0.1',
mode: 'custom',
},
},
},
})
expect(result.changed).toBe(true)
expect(result.config).toEqual({})
})
})