Scope channel settings to request profile

This commit is contained in:
ekko
2026-05-24 08:37:50 +08:00
committed by ekko
parent 4f8bda9836
commit be2089e423
4 changed files with 162 additions and 99 deletions
+56 -43
View File
@@ -1,19 +1,18 @@
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 { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
const { mockRestartGateway } = vi.hoisted(() => ({
mockRestartGateway: vi.fn().mockResolvedValue(undefined),
const { mockRestartGatewayForProfile } = vi.hoisted(() => ({
mockRestartGatewayForProfile: vi.fn().mockResolvedValue({ running: true, profile: 'research' }),
}))
vi.mock('../../packages/server/src/services/hermes/hermes-cli', () => ({
restartGateway: mockRestartGateway,
vi.mock('../../packages/server/src/services/hermes/gateway-autostart', () => ({
restartGatewayForProfile: mockRestartGatewayForProfile,
}))
const originalHermesHome = process.env.HERMES_HOME
const tempHomes: string[] = []
let hermesHome = ''
const originalHermesHome = process.env.HERMES_HOME
async function loadController() {
vi.resetModules()
@@ -21,57 +20,71 @@ async function loadController() {
return import('../../packages/server/src/controllers/hermes/weixin')
}
function makeCtx(body: unknown): any {
return { request: { body }, status: 200, body: undefined }
function makeCtx(body: Record<string, any>, profile = 'research'): any {
return {
request: { body },
state: { profile: { name: profile } },
status: 200,
body: undefined,
}
}
beforeEach(async () => {
vi.clearAllMocks()
hermesHome = await mkdtemp(join(tmpdir(), 'hermes-weixin-controller-'))
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('weixin controller save', () => {
it('updates .env through the locked file store and preserves unrelated keys', async () => {
describe('weixin controller', () => {
beforeEach(async () => {
vi.clearAllMocks()
hermesHome = await mkdtemp(join(tmpdir(), 'hwui-weixin-controller-'))
await mkdir(join(hermesHome, 'profiles', 'research'), { recursive: true })
await writeFile(join(hermesHome, '.env'), [
'OPENROUTER_API_KEY=keep',
'WEIXIN_TOKEN=old-token',
'WEIXIN_ACCOUNT_ID=keep-default-account',
'WEIXIN_TOKEN=keep-default-token',
'',
].join('\n'), 'utf-8')
await writeFile(join(hermesHome, 'profiles', 'research', '.env'), [
'OPENROUTER_API_KEY=keep-research-openrouter',
'WEIXIN_ACCOUNT_ID=old-research-account',
'WEIXIN_TOKEN=old-research-token',
'',
].join('\n'), 'utf-8')
})
afterEach(async () => {
vi.resetModules()
if (originalHermesHome === undefined) delete process.env.HERMES_HOME
else process.env.HERMES_HOME = originalHermesHome
if (hermesHome) await rm(hermesHome, { recursive: true, force: true })
hermesHome = ''
})
it('saves scanned Weixin credentials to the request-scoped profile env only', async () => {
const { save } = await loadController()
const ctx = makeCtx({ account_id: 'acct-1', token: 'new-token', base_url: 'https://weixin.local' })
const ctx = makeCtx({
account_id: 'new-research-account',
token: 'new-research-token',
base_url: 'https://weixin.invalid',
})
await save(ctx)
expect(ctx.body).toEqual({ success: true })
expect(mockRestartGateway).toHaveBeenCalled()
const env = await readFile(join(hermesHome, '.env'), 'utf-8')
expect(env).toContain('OPENROUTER_API_KEY=keep')
expect(env).toContain('WEIXIN_ACCOUNT_ID=acct-1')
expect(env).toContain('WEIXIN_TOKEN=new-token')
expect(env).toContain('WEIXIN_BASE_URL=https://weixin.local')
expect(env).not.toContain('old-token')
expect(mockRestartGatewayForProfile).toHaveBeenCalledWith('research')
expect(await readFile(join(hermesHome, '.env'), 'utf-8')).toContain('WEIXIN_TOKEN=keep-default-token')
const researchEnv = await readFile(join(hermesHome, 'profiles', 'research', '.env'), 'utf-8')
expect(researchEnv).toContain('OPENROUTER_API_KEY=keep-research-openrouter')
expect(researchEnv).toContain('WEIXIN_ACCOUNT_ID=new-research-account')
expect(researchEnv).toContain('WEIXIN_TOKEN=new-research-token')
expect(researchEnv).toContain('WEIXIN_BASE_URL=https://weixin.invalid')
})
it('rejects missing required credentials without touching .env', async () => {
await writeFile(join(hermesHome, '.env'), 'OPENROUTER_API_KEY=keep\n', 'utf-8')
it('rejects missing required credentials without touching the profile env', async () => {
const { save } = await loadController()
const ctx = makeCtx({ account_id: 'acct-1' })
const ctx = makeCtx({ account_id: 'new-research-account' })
const envBefore = await readFile(join(hermesHome, 'profiles', 'research', '.env'), 'utf-8')
await save(ctx)
expect(ctx.status).toBe(400)
expect(ctx.body).toEqual({ error: 'Missing account_id or token' })
expect(mockRestartGateway).not.toHaveBeenCalled()
await expect(readFile(join(hermesHome, '.env'), 'utf-8')).resolves.toBe('OPENROUTER_API_KEY=keep\n')
expect(mockRestartGatewayForProfile).not.toHaveBeenCalled()
await expect(readFile(join(hermesHome, 'profiles', 'research', '.env'), 'utf-8')).resolves.toBe(envBefore)
})
})