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:
2026-10-20 11:17:34 +08:00
committed by trae-solo-agent
parent 8052bf5781
commit a1fff482bf
20 changed files with 251 additions and 48 deletions
+26 -12
View File
@@ -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 })
}
})