Files
hermes-web-ui/tests/server/write-gate-service.test.ts
2026-06-11 23:06:02 +08:00

140 lines
5.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
const originalHermesHome = process.env.HERMES_HOME
const originalHermesAgentRoot = process.env.HERMES_AGENT_ROOT
const originalHermesBin = process.env.HERMES_BIN
const originalHermesAgentCliPython = process.env.HERMES_AGENT_CLI_PYTHON
let hermesHome = ''
async function loadService() {
vi.resetModules()
process.env.HERMES_HOME = hermesHome
return import('../../packages/server/src/services/hermes/write-gate')
}
beforeEach(async () => {
hermesHome = await mkdtemp(join(tmpdir(), 'hermes-write-gate-'))
})
afterEach(async () => {
vi.resetModules()
if (originalHermesHome === undefined) delete process.env.HERMES_HOME
else process.env.HERMES_HOME = originalHermesHome
if (originalHermesAgentRoot === undefined) delete process.env.HERMES_AGENT_ROOT
else process.env.HERMES_AGENT_ROOT = originalHermesAgentRoot
if (originalHermesBin === undefined) delete process.env.HERMES_BIN
else process.env.HERMES_BIN = originalHermesBin
if (originalHermesAgentCliPython === undefined) delete process.env.HERMES_AGENT_CLI_PYTHON
else process.env.HERMES_AGENT_CLI_PYTHON = originalHermesAgentCliPython
await rm(hermesHome, { recursive: true, force: true })
hermesHome = ''
})
describe('write gate service', () => {
it('lists pending memory and skill records from the active profile', async () => {
await mkdir(join(hermesHome, 'pending', 'memory'), { recursive: true })
await mkdir(join(hermesHome, 'pending', 'skills'), { recursive: true })
await writeFile(join(hermesHome, 'pending', 'memory', 'mem123.json'), JSON.stringify({
id: 'mem123',
subsystem: 'memory',
action: 'add',
summary: 'remember concise answers',
origin: 'foreground',
created_at: 2,
payload: { target: 'user' },
}), 'utf-8')
await writeFile(join(hermesHome, 'pending', 'skills', 'skill123.json'), JSON.stringify({
id: 'skill123',
subsystem: 'skills',
action: 'patch',
summary: 'patch demo skill',
origin: 'background_review',
created_at: 1,
payload: { name: 'demo' },
}), 'utf-8')
const { listPendingWrites } = await loadService()
const result = await listPendingWrites('default')
expect(result.counts).toEqual({ memory: 1, skills: 1 })
expect(result.records.map(record => record.id)).toEqual(['skill123', 'mem123'])
expect(result.records[0]).toMatchObject({
subsystem: 'skills',
summary: 'patch demo skill',
payload: { name: 'demo' },
})
})
it('rejects unsafe subsystem and pending ids before running Hermes Python', async () => {
const { getPendingWriteDiff } = await loadService()
await expect(getPendingWriteDiff('default', 'files', 'abc123')).rejects.toThrow('Invalid write gate subsystem')
await expect(getPendingWriteDiff('default', 'memory', '../abc')).rejects.toThrow('Invalid pending write id')
})
it('detects write approval support from a uv-backed Hermes venv shebang', async () => {
const agentRoot = join(hermesHome, 'agent')
const venvBin = join(agentRoot, 'venv', 'bin')
const externalPythonDir = join(hermesHome, 'uv-python', 'bin')
await mkdir(join(agentRoot, 'tools'), { recursive: true })
await mkdir(join(agentRoot, 'hermes_cli'), { recursive: true })
await mkdir(venvBin, { recursive: true })
await mkdir(externalPythonDir, { recursive: true })
await writeFile(join(agentRoot, 'tools', 'write_approval.py'), '', 'utf-8')
await writeFile(join(agentRoot, 'hermes_cli', 'write_approval_commands.py'), '', 'utf-8')
await writeFile(join(externalPythonDir, 'python3'), '', 'utf-8')
await symlink(join(externalPythonDir, 'python3'), join(venvBin, 'python3'))
const hermesBin = join(venvBin, 'hermes')
await writeFile(hermesBin, `#!${join(venvBin, 'python3')}\n`, 'utf-8')
process.env.HERMES_BIN = hermesBin
delete process.env.HERMES_AGENT_ROOT
const { isWriteGateSupported } = await loadService()
expect(isWriteGateSupported()).toBe(true)
})
it('detects write approval support from a Windows-style venv Scripts executable path', async () => {
const agentRoot = join(hermesHome, 'agent-win')
const scriptsDir = join(agentRoot, 'venv', 'Scripts')
await mkdir(join(agentRoot, 'tools'), { recursive: true })
await mkdir(join(agentRoot, 'hermes_cli'), { recursive: true })
await mkdir(scriptsDir, { recursive: true })
await writeFile(join(agentRoot, 'tools', 'write_approval.py'), '', 'utf-8')
await writeFile(join(agentRoot, 'hermes_cli', 'write_approval_commands.py'), '', 'utf-8')
const hermesBin = join(scriptsDir, 'hermes.exe')
await writeFile(hermesBin, '', 'utf-8')
process.env.HERMES_BIN = hermesBin
delete process.env.HERMES_AGENT_ROOT
const { isWriteGateSupported } = await loadService()
expect(isWriteGateSupported()).toBe(true)
})
it('detects write approval support from a pip-installed runtime Python', async () => {
const fakePython = join(hermesHome, 'python')
await writeFile(fakePython, [
'#!/bin/sh',
'case "$2" in',
' *"tools.write_approval"*"hermes_cli.write_approval_commands"*) exit 0 ;;',
' *) exit 1 ;;',
'esac',
'',
].join('\n'), 'utf-8')
await chmod(fakePython, 0o755)
process.env.HERMES_AGENT_CLI_PYTHON = fakePython
process.env.HERMES_BIN = join(hermesHome, 'missing-hermes')
delete process.env.HERMES_AGENT_ROOT
const { isWriteGateSupported } = await loadService()
expect(isWriteGateSupported()).toBe(true)
})
})