make the test suite and openapi generator platform-independent
Windows checkouts turned LF into CRLF (core.autocrlf=true), and several tests plus the openapi generator hardcoded LF or POSIX path separators. The result was a suite that was green on Linux CI but red on Windows, and an openapi.json whose /api/hermes/* routes silently disappeared when generated on Windows. Pin the repository to LF via .gitattributes, strip shebangs before Vite's SSR transform, and key route files by their posix path. Add a manually triggered macOS verification workflow so the same checks run on a self-hosted macOS runner.
This commit is contained in:
@@ -147,8 +147,19 @@ describe('agent bridge manager command resolution', () => {
|
||||
it('uses an isolated default bridge endpoint while running under Vitest', async () => {
|
||||
const { DEFAULT_AGENT_BRIDGE_ENDPOINT } = await import('../../packages/server/src/services/hermes/agent-bridge/client')
|
||||
|
||||
expect(DEFAULT_AGENT_BRIDGE_ENDPOINT).toContain(`hermes-agent-bridge-test-${process.pid}`)
|
||||
// Vitest 下必须用「每个测试进程独立」的桥接端点,否则并行 worker 会抢同一个
|
||||
// 生产 bridge。Windows 没有 unix socket 路径,测试端点走带 pid 偏移的 TCP 端口;
|
||||
// POSIX 走带 pid 的 socket 文件。两种形态都要和生产的固定端点不同。
|
||||
expect(DEFAULT_AGENT_BRIDGE_ENDPOINT).not.toBe('ipc:///tmp/hermes-agent-bridge.sock')
|
||||
expect(DEFAULT_AGENT_BRIDGE_ENDPOINT).not.toBe('tcp://127.0.0.1:18765')
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const port = Number(DEFAULT_AGENT_BRIDGE_ENDPOINT.split(':').pop())
|
||||
expect(port).toBeGreaterThanOrEqual(28000)
|
||||
expect(port).toBeLessThanOrEqual(37999)
|
||||
} else {
|
||||
expect(DEFAULT_AGENT_BRIDGE_ENDPOINT).toContain(`hermes-agent-bridge-test-${process.pid}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('honors the bridge connect retry environment override', async () => {
|
||||
|
||||
@@ -205,7 +205,11 @@ describe('gateway-runner supervision', () => {
|
||||
startGatewayRunManaged('/usr/bin/hermes', { profileDir: '/tmp/delete-me' })
|
||||
expect(fakeChildren).toHaveLength(1)
|
||||
|
||||
const retired = retireManagedGatewayForProfile('/tmp/delete-me', { timeoutMs: 5000 })
|
||||
// 固定 platform:停止网关的手段随平台不同(POSIX 走 child.kill('SIGTERM'),
|
||||
// Windows 走 taskkill 杀进程树),不固定的话这个用例会随运行平台变形。
|
||||
// Windows 分支由同文件末尾的 "kills the managed gateway process tree on
|
||||
// Windows shutdown" 单独覆盖。
|
||||
const retired = retireManagedGatewayForProfile('/tmp/delete-me', { timeoutMs: 5000, platform: 'linux' })
|
||||
expect(fakeChildren[0].killSignals).toEqual(['SIGTERM'])
|
||||
|
||||
fakeChildren[0].emit('exit', 0, 'SIGTERM')
|
||||
@@ -226,7 +230,7 @@ describe('gateway-runner supervision', () => {
|
||||
startGatewayRunManaged('/usr/bin/hermes', { profileDir: '/tmp/shutdown-b' })
|
||||
expect(fakeChildren).toHaveLength(2)
|
||||
|
||||
const shutdown = shutdownManagedGateways({ timeoutMs: 5000 })
|
||||
const shutdown = shutdownManagedGateways({ timeoutMs: 5000, platform: 'linux' })
|
||||
|
||||
expect(fakeChildren[0].killSignals).toEqual(['SIGTERM'])
|
||||
expect(fakeChildren[1].killSignals).toEqual(['SIGTERM'])
|
||||
@@ -266,7 +270,7 @@ describe('gateway-runner supervision', () => {
|
||||
|
||||
startGatewayRunManaged('/usr/bin/hermes', { profileDir: '/tmp/shutdown-d' })
|
||||
|
||||
const shutdown = shutdownManagedGateways({ timeoutMs: 1000 })
|
||||
const shutdown = shutdownManagedGateways({ timeoutMs: 1000, platform: 'linux' })
|
||||
expect(fakeChildren[0].killSignals).toEqual(['SIGTERM'])
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
|
||||
@@ -162,8 +162,12 @@ describe('group chat history windows', () => {
|
||||
expect(storage.getMessagesForContext('room-1')).toHaveLength(160)
|
||||
expect(latest?.totalTokens).toBe(expectedTotalTokens)
|
||||
expect(storage.getRoom('room-1')?.totalTokens).toBe(expectedTotalTokens)
|
||||
})
|
||||
}, 60_000)
|
||||
|
||||
// 这个用例故意写 501 条消息来触发 prune(保留 500 条)。而
|
||||
// saveMessageAndRefreshRoom 每写一条都要重读整份转录并重新计一遍 token,
|
||||
// 单测本身就是 O(n²):单独跑约 1.3s,全量并行时 CPU 被抢,很容易撞上
|
||||
// Vitest 默认的 5s 上限。这是重测试,不是卡死 —— 显式放宽超时。
|
||||
it('preserves snapshot summary tokens when the snapshot anchor was pruned from retained history', () => {
|
||||
const storage = groupServer.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
@@ -189,7 +193,7 @@ describe('group chat history windows', () => {
|
||||
expect(storage.getContextSnapshot('room-1')?.lastMessageId).toBe('msg-1')
|
||||
expect(latest?.totalTokens).toBe(expectedTotalTokens)
|
||||
expect(storage.getRoom('room-1')?.totalTokens).toBe(expectedTotalTokens)
|
||||
})
|
||||
}, 60_000)
|
||||
|
||||
it('uses the full context transcript for the final AgentClient room estimate', async () => {
|
||||
const messages = Array.from({ length: 160 }, (_value, index) => ({
|
||||
|
||||
@@ -4,7 +4,9 @@ import { join } from 'path'
|
||||
import YAML from 'js-yaml'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('Hermes plugin configuration', () => {
|
||||
// 假 python 是 #!/bin/sh 脚本,插件发现依赖 POSIX 的 shebang 解析链路。
|
||||
// Windows 上没有 shebang 执行语义,整组跳过而不是伪造结果。
|
||||
describe.skipIf(process.platform === 'win32')('Hermes plugin configuration', () => {
|
||||
const originalEnv = { ...process.env }
|
||||
let tempDir = ''
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('Hermes plugin discovery environment', () => {
|
||||
// 这个用例的假 python 是一段 #!/bin/sh 脚本,验证的是「从 hermes 的 shebang
|
||||
// 解析出 venv python」这条 POSIX 链路。Windows 没有 shebang 执行语义,
|
||||
// venv 布局也是 .venv\Scripts\python.exe,这条链路在那边根本不存在,
|
||||
// 所以在 Windows 上整组跳过,而不是伪造一个假的 Windows 结果。
|
||||
describe.skipIf(process.platform === 'win32')('Hermes plugin discovery environment', () => {
|
||||
const originalEnv = { ...process.env }
|
||||
let tempDir = ''
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { join } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const originalWebUiHome = process.env.HERMES_WEB_UI_HOME
|
||||
@@ -21,10 +21,14 @@ describe('media controller', () => {
|
||||
process.env.HERMES_WEB_UI_HOME = '/tmp/hermes-ui-test-home'
|
||||
const { defaultImageOutputPath, defaultMediaOutputPath } = await import('../../packages/server/src/controllers/hermes/media')
|
||||
|
||||
expect(defaultMediaOutputPath('req_123')).toBe(join('/tmp/hermes-ui-test-home', 'media', 'req_123.mp4'))
|
||||
expect(defaultMediaOutputPath('bad/request:id')).toBe(join('/tmp/hermes-ui-test-home', 'media', 'bad_request_id.mp4'))
|
||||
expect(defaultImageOutputPath('img_123')).toBe(join('/tmp/hermes-ui-test-home', 'media', 'img_123.png'))
|
||||
expect(defaultImageOutputPath('bad/request:id', 1)).toBe(join('/tmp/hermes-ui-test-home', 'media', 'bad_request_id-2.png'))
|
||||
// 生产侧 getWebUiHome() 用 resolve() 落地 appHome,Windows 上 resolve('/tmp/x')
|
||||
// = 'D:\tmp\x',而 join('/tmp/x') = '\tmp\x'(join 不补盘符)。断言必须跟着
|
||||
// 生产语义走,否则只在 Windows 上红、Linux CI 上却是绿的。
|
||||
const mediaRoot = join(resolve('/tmp/hermes-ui-test-home'), 'media')
|
||||
expect(defaultMediaOutputPath('req_123')).toBe(join(mediaRoot, 'req_123.mp4'))
|
||||
expect(defaultMediaOutputPath('bad/request:id')).toBe(join(mediaRoot, 'bad_request_id.mp4'))
|
||||
expect(defaultImageOutputPath('img_123')).toBe(join(mediaRoot, 'img_123.png'))
|
||||
expect(defaultImageOutputPath('bad/request:id', 1)).toBe(join(mediaRoot, 'bad_request_id-2.png'))
|
||||
})
|
||||
|
||||
it('generates images through the requested configured custom provider', async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { join } from 'path'
|
||||
|
||||
const {
|
||||
mockReadFile,
|
||||
@@ -145,8 +146,10 @@ describe('model catalog cache', () => {
|
||||
mockGlobalFetch.mockResolvedValue({ ok: false, status: 404, json: async () => ({}) })
|
||||
vi.stubGlobal('fetch', mockGlobalFetch)
|
||||
mockReadFile.mockImplementation(async (path: string) => {
|
||||
if (path === '/hermes/default/.env') return 'OPENROUTER_API_KEY=default-openrouter\n'
|
||||
if (path === '/hermes/team/.env') {
|
||||
// 生产侧用 join(profileDir, '.env') 拼路径,Windows 上是反斜杠,
|
||||
// 断言必须用 join() 才能跨平台对上。
|
||||
if (path === join('/hermes/default', '.env')) return 'OPENROUTER_API_KEY=default-openrouter\n'
|
||||
if (path === join('/hermes/team', '.env')) {
|
||||
return [
|
||||
'OPENROUTER_API_KEY=team-openrouter',
|
||||
'DEEPSEEK_API_KEY=team-deepseek',
|
||||
@@ -240,8 +243,8 @@ describe('model catalog cache', () => {
|
||||
return []
|
||||
})
|
||||
mockReadFile.mockImplementation(async (path: string) => {
|
||||
if (path === '/hermes/default/.env') return ''
|
||||
if (path === '/hermes/default/auth.json') {
|
||||
if (path === join('/hermes/default', '.env')) return ''
|
||||
if (path === join('/hermes/default', 'auth.json')) {
|
||||
return JSON.stringify({
|
||||
providers: {
|
||||
'openai-codex': { tokens: { access_token: 'codex-token' } },
|
||||
|
||||
@@ -264,8 +264,14 @@ describe('legacy brand removal (ekko / hermes-web-ui / hermes-studio)', () => {
|
||||
const marker = `const ${name} = [`
|
||||
const start = harness.indexOf(marker)
|
||||
expect(start, `${HARNESS_SCRIPT} 里找不到 ${name},本测试需要同步更新`).toBeGreaterThan(-1)
|
||||
const end = harness.indexOf('\n]\n', start)
|
||||
expect(end, `${HARNESS_SCRIPT} 里的 ${name} 数组没有正确闭合`).toBeGreaterThan(start)
|
||||
// 行尾不能写死 '\n]':Windows 上 checkout 会把 LF 转成 CRLF
|
||||
// (core.autocrlf=true),写死 LF 的话这个测试只会在 Windows 上红,
|
||||
// Linux CI 上却是绿的。仓库的 .gitattributes 已把行尾统一成 LF,但
|
||||
// 在它被加进来之前 checkout 过的工作区仍留着 CRLF,所以这里两种都要认。
|
||||
const rest = harness.slice(start)
|
||||
const closeMatch = /\r?\n\]/.exec(rest)
|
||||
expect(closeMatch, `${HARNESS_SCRIPT} 里的 ${name} 数组没有正确闭合`).not.toBeNull()
|
||||
const end = start + closeMatch!.index
|
||||
return harness.slice(start + marker.length - 1, end + 2)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from 'path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
||||
class FakeChild extends EventEmitter {
|
||||
pid: number
|
||||
@@ -39,6 +40,7 @@ afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.resetModules()
|
||||
process.env = { ...originalEnv }
|
||||
if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform)
|
||||
fakeChildren = []
|
||||
})
|
||||
|
||||
@@ -46,6 +48,11 @@ describe('profile delete managed gateway lifecycle', () => {
|
||||
it('reproduces #1633 and proves delete prep suppresses managed respawn', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
// 固定 platform:停止网关的手段随平台不同(POSIX 走 child.kill('SIGTERM'),
|
||||
// Windows 走 taskkill 杀进程树)。不固定的话这个用例在 Windows 上会因为
|
||||
// taskkill 是异步的而读不到 killSignals,而且会真的拿假 pid 去杀进程。
|
||||
// Windows 分支由 gateway-respawn.test.ts 单独覆盖。
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' })
|
||||
const home = await mkdtemp(join(tmpdir(), 'wui-1633-'))
|
||||
process.env.HERMES_HOME = home
|
||||
process.env.HERMES_BIN = '/usr/bin/hermes'
|
||||
|
||||
@@ -102,6 +102,10 @@ vi.mock('../../packages/server/src/middleware/user-auth', () => ({
|
||||
issueModelRunJwt: issueModelRunJwtMock,
|
||||
}))
|
||||
|
||||
// 生产侧用 join(getProfileDir(profile), 'workspace') 拼默认工作区,Windows 上是
|
||||
// 反斜杠分隔。断言必须跟着走 join,否则只有 Linux 是绿的。
|
||||
const DEFAULT_PROFILE_WORKSPACE = join('/tmp/hermes-bridge-final-context/default', 'workspace')
|
||||
|
||||
function makeSocket() {
|
||||
return {
|
||||
connected: true,
|
||||
@@ -292,7 +296,7 @@ describe('bridge run final context usage', () => {
|
||||
{
|
||||
model: 'gpt-test',
|
||||
provider: 'openai',
|
||||
workspace: '/tmp/hermes-bridge-final-context/default/workspace',
|
||||
workspace: DEFAULT_PROFILE_WORKSPACE,
|
||||
},
|
||||
)
|
||||
expect(bridge.contextEstimate.mock.calls[0][2]).toContain('system prompt')
|
||||
@@ -660,7 +664,7 @@ describe('bridge run final context usage', () => {
|
||||
expect(createSessionMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'session-1',
|
||||
source: 'global_agent',
|
||||
workspace: '/tmp/hermes-bridge-final-context/default/workspace',
|
||||
workspace: DEFAULT_PROFILE_WORKSPACE,
|
||||
}))
|
||||
expect(state.source).toBe('global_agent')
|
||||
})
|
||||
@@ -1104,7 +1108,7 @@ describe('bridge run final context usage', () => {
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
storage_message: '/plan build the feature',
|
||||
workspace: '/tmp/hermes-bridge-final-context/default/workspace',
|
||||
workspace: DEFAULT_PROFILE_WORKSPACE,
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1229,7 +1233,7 @@ describe('bridge run final context usage', () => {
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
storage_message: '[IMPORTANT: expanded skill prompt]',
|
||||
workspace: '/tmp/hermes-bridge-final-context/default/workspace',
|
||||
workspace: DEFAULT_PROFILE_WORKSPACE,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const {
|
||||
mockCopyFile,
|
||||
@@ -41,14 +42,18 @@ describe('SafeFileStore backup fallback', () => {
|
||||
const { SafeFileStore } = await import('../../packages/server/src/services/safe-file-store')
|
||||
const store = new SafeFileStore()
|
||||
|
||||
// SafeFileStore.normalizePath() 走 resolve(),Windows 上 resolve('/tmp/config.yaml')
|
||||
// = 'D:\tmp\config.yaml'。断言必须按生产语义写死,否则只在 Windows 上红。
|
||||
const target = resolve('/tmp/config.yaml')
|
||||
|
||||
await store.writeText('/tmp/config.yaml', 'model:\n default: new\n', { backup: true })
|
||||
|
||||
expect(mockCopyFile).toHaveBeenCalledTimes(2)
|
||||
expect(mockCopyFile).toHaveBeenNthCalledWith(1, '/tmp/config.yaml', '/tmp/config.yaml.bak')
|
||||
expect(mockCopyFile.mock.calls[1][0]).toBe('/tmp/config.yaml')
|
||||
expect(mockCopyFile.mock.calls[1][1]).toMatch(/^\/tmp\/config\.yaml\.bak\.\d+\./)
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining('/tmp/config.yaml.tmp.'), 'model:\n default: new\n', 'utf-8')
|
||||
expect(mockRename).toHaveBeenCalledWith(expect.stringContaining('/tmp/config.yaml.tmp.'), '/tmp/config.yaml')
|
||||
expect(mockCopyFile).toHaveBeenNthCalledWith(1, target, `${target}.bak`)
|
||||
expect(mockCopyFile.mock.calls[1][0]).toBe(target)
|
||||
expect(mockCopyFile.mock.calls[1][1].startsWith(`${target}.bak.`)).toBe(true)
|
||||
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(`${target}.tmp.`), 'model:\n default: new\n', 'utf-8')
|
||||
expect(mockRename).toHaveBeenCalledWith(expect.stringContaining(`${target}.tmp.`), target)
|
||||
})
|
||||
|
||||
it('preserves explicit backup path failures', async () => {
|
||||
|
||||
@@ -1302,7 +1302,7 @@ describe('session conversations controller', () => {
|
||||
expect(localUpdateSessionMock).toHaveBeenCalledWith('session-1', {
|
||||
model: 'grok-4',
|
||||
provider: 'xai',
|
||||
workspace: '/tmp/hermes-test/default/workspace',
|
||||
workspace: join('/tmp/hermes-test/default', 'workspace'),
|
||||
})
|
||||
expect(bridgeSwitchSessionModelMock).not.toHaveBeenCalled()
|
||||
expect(ctx.body).toEqual({ ok: true })
|
||||
@@ -1331,7 +1331,7 @@ describe('session conversations controller', () => {
|
||||
expect(localUpdateSessionMock).toHaveBeenCalledWith('session-1', {
|
||||
model: 'claude-sonnet-4-6',
|
||||
provider: 'claude-oauth',
|
||||
workspace: '/tmp/hermes-test/travel/workspace',
|
||||
workspace: join('/tmp/hermes-test/travel', 'workspace'),
|
||||
})
|
||||
expect(bridgeSwitchSessionModelMock).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { join } from 'path'
|
||||
|
||||
const allMock = vi.fn()
|
||||
const indexAllMock = vi.fn()
|
||||
@@ -71,7 +72,7 @@ describe('session DB summaries', () => {
|
||||
const mod = await import('../../packages/server/src/db/hermes/sessions-db')
|
||||
const rows = await mod.listSessionSummaries(undefined, 50)
|
||||
|
||||
expect(databaseSyncMock).toHaveBeenCalledWith('/tmp/hermes-profile/state.db', { open: true, readOnly: true })
|
||||
expect(databaseSyncMock).toHaveBeenCalledWith(join('/tmp/hermes-profile', 'state.db'), { open: true, readOnly: true })
|
||||
expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining("s.source != 'tool'"))
|
||||
expect(allMock).toHaveBeenCalledWith(200)
|
||||
expect(closeMock).toHaveBeenCalled()
|
||||
|
||||
@@ -35,6 +35,26 @@ async function loadController() {
|
||||
return import('../../packages/server/src/controllers/hermes/skills')
|
||||
}
|
||||
|
||||
/**
|
||||
* 把「用户主目录」指向临时目录,并返回恢复函数。
|
||||
*
|
||||
* Node 的 os.homedir() 在 Windows 上优先读 USERPROFILE,在 POSIX 上优先读 HOME。
|
||||
* 只设 HOME 会让 Windows 落到真实用户目录,夹具就找不到,测试只在 Linux 上是绿的。
|
||||
* 两个都设,跨平台行为一致。
|
||||
*/
|
||||
function useHomeDir(root: string): () => void {
|
||||
const previousHome = process.env.HOME
|
||||
const previousUserProfile = process.env.USERPROFILE
|
||||
process.env.HOME = root
|
||||
process.env.USERPROFILE = root
|
||||
return () => {
|
||||
if (previousHome == null) delete process.env.HOME
|
||||
else process.env.HOME = previousHome
|
||||
if (previousUserProfile == null) delete process.env.USERPROFILE
|
||||
else process.env.USERPROFILE = previousUserProfile
|
||||
}
|
||||
}
|
||||
|
||||
function multipartBody(boundary: string, parts: Array<{ name: string; value: string; filename?: string; filenameStar?: string; contentType?: string }>): Buffer {
|
||||
const chunks: Buffer[] = []
|
||||
for (const part of parts) {
|
||||
@@ -200,7 +220,6 @@ describe('skills controller', () => {
|
||||
|
||||
it('lists Codex user and system skills for the codex target', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-codex-skills-'))
|
||||
const previousHome = process.env.HOME
|
||||
const userSkillDir = join(root, '.agents', 'skills', 'user-skill')
|
||||
const systemSkillDir = join(root, '.codex', 'skills', '.system', 'system-skill')
|
||||
|
||||
@@ -208,7 +227,7 @@ describe('skills controller', () => {
|
||||
await mkdir(systemSkillDir, { recursive: true })
|
||||
await writeFile(join(userSkillDir, 'SKILL.md'), '# User Skill\nuser codex skill\n', 'utf-8')
|
||||
await writeFile(join(systemSkillDir, 'SKILL.md'), '# System Skill\nsystem codex skill\n', 'utf-8')
|
||||
process.env.HOME = root
|
||||
const restoreHome = useHomeDir(root)
|
||||
|
||||
try {
|
||||
const { list } = await loadController()
|
||||
@@ -227,21 +246,19 @@ describe('skills controller', () => {
|
||||
})
|
||||
expect(mockReadConfigYamlForProfile).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
if (previousHome == null) delete process.env.HOME
|
||||
else process.env.HOME = previousHome
|
||||
restoreHome()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('reads Codex system skill details for the codex target', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-codex-system-skill-'))
|
||||
const previousHome = process.env.HOME
|
||||
const systemSkillDir = join(root, '.codex', 'skills', '.system', 'imagegen')
|
||||
|
||||
await mkdir(join(systemSkillDir, 'references'), { recursive: true })
|
||||
await writeFile(join(systemSkillDir, 'SKILL.md'), '# Imagegen\nsystem image skill\n', 'utf-8')
|
||||
await writeFile(join(systemSkillDir, 'references', 'usage.md'), 'usage notes\n', 'utf-8')
|
||||
process.env.HOME = root
|
||||
const restoreHome = useHomeDir(root)
|
||||
mockListFilesRecursive.mockResolvedValue([
|
||||
{ path: 'SKILL.md', isDir: false },
|
||||
{ path: 'references/usage.md', isDir: false },
|
||||
@@ -271,8 +288,7 @@ describe('skills controller', () => {
|
||||
|
||||
expect(filesCtx.body).toEqual({ files: [{ path: 'references/usage.md', isDir: false }] })
|
||||
} finally {
|
||||
if (previousHome == null) delete process.env.HOME
|
||||
else process.env.HOME = previousHome
|
||||
restoreHome()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -477,7 +493,6 @@ describe('skills controller', () => {
|
||||
|
||||
it('updates Codex user skills but not Codex system skills', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'hermes-ui-update-codex-skill-'))
|
||||
const previousHome = process.env.HOME
|
||||
const userSkillDir = join(root, '.agents', 'skills', 'user-skill')
|
||||
const systemSkillDir = join(root, '.codex', 'skills', '.system', 'system-skill')
|
||||
|
||||
@@ -485,7 +500,7 @@ describe('skills controller', () => {
|
||||
await mkdir(systemSkillDir, { recursive: true })
|
||||
await writeFile(join(userSkillDir, 'SKILL.md'), '# User Skill\n', 'utf-8')
|
||||
await writeFile(join(systemSkillDir, 'SKILL.md'), '# System Skill\n', 'utf-8')
|
||||
process.env.HOME = root
|
||||
const restoreHome = useHomeDir(root)
|
||||
|
||||
try {
|
||||
const { updateSkill } = await loadController()
|
||||
@@ -516,8 +531,7 @@ describe('skills controller', () => {
|
||||
expect(systemCtx.status).toBe(404)
|
||||
expect(systemCtx.body).toEqual({ error: 'Skill not found' })
|
||||
} finally {
|
||||
if (previousHome == null) delete process.env.HOME
|
||||
else process.env.HOME = previousHome
|
||||
restoreHome()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const chatRunMock = vi.hoisted(() => ({
|
||||
runAndWait: vi.fn(),
|
||||
@@ -22,6 +22,14 @@ vi.mock('../../packages/server/src/db/hermes/session-store', async (importOrigin
|
||||
})
|
||||
|
||||
describe('workflow manager', () => {
|
||||
beforeAll(async () => {
|
||||
// 建表只在 server bootstrap(packages/server/src/index.ts)里执行,单测不走
|
||||
// bootstrap。workflows 表能不能用,取决于别的测试文件有没有先建过它 ——
|
||||
// 这是顺序依赖的偶发失败。这里显式建表,让本文件自给自足。
|
||||
const { initAllHermesTables } = await import('../../packages/server/src/db/hermes/schemas')
|
||||
initAllHermesTables()
|
||||
})
|
||||
|
||||
it('returns a server-wide singleton instance', async () => {
|
||||
const { WorkflowManager, getWorkflowManager } = await import('../../packages/server/src/services/workflow-manager')
|
||||
|
||||
|
||||
@@ -117,7 +117,9 @@ describe('write gate service', () => {
|
||||
expect(isWriteGateSupported()).toBe(true)
|
||||
})
|
||||
|
||||
it('detects write approval support from a pip-installed runtime Python', async () => {
|
||||
// 假 python 是 #!/bin/sh 脚本,能力探测要真的把它跑起来。Windows 没有
|
||||
// shebang 执行语义,这个用例只在 POSIX 上有意义。
|
||||
it.skipIf(process.platform === 'win32')('detects write approval support from a pip-installed runtime Python', async () => {
|
||||
const fakePython = join(hermesHome, 'python')
|
||||
await writeFile(fakePython, [
|
||||
'#!/bin/sh',
|
||||
|
||||
Reference in New Issue
Block a user