Files
hermes-ui/tests/server/config-helpers-file-lock.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

96 lines
3.6 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' })
})
})