Scope files jobs and plugins to request profile

This commit is contained in:
ekko
2026-05-24 09:25:52 +08:00
committed by ekko
parent 289a958684
commit 9708a6a521
23 changed files with 353 additions and 117 deletions
+43 -4
View File
@@ -3,10 +3,14 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
const profileDirState = vi.hoisted(() => ({ value: '' }))
const profileDirState = vi.hoisted(() => ({
value: '',
dirs: {} as Record<string, string>,
}))
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
getActiveProfileDir: () => profileDirState.value,
getActiveProfileName: () => 'default',
getProfileDir: (profile: string) => profileDirState.dirs[profile] || profileDirState.value,
}))
function createCtx(overrides: Record<string, any> = {}) {
@@ -19,8 +23,8 @@ function createCtx(overrides: Record<string, any> = {}) {
} as any
}
function writeJobs(jobs: unknown[]) {
const cronDir = join(profileDirState.value, 'cron')
function writeJobs(jobs: unknown[], profileDir = profileDirState.value) {
const cronDir = join(profileDir, 'cron')
mkdirSync(cronDir, { recursive: true })
writeFileSync(join(cronDir, 'jobs.json'), JSON.stringify({ jobs }))
}
@@ -29,10 +33,45 @@ describe('Hermes cron history controller', () => {
beforeEach(() => {
vi.resetModules()
profileDirState.value = mkdtempSync(join(tmpdir(), 'hwui-cron-history-'))
profileDirState.dirs = { default: profileDirState.value }
})
afterEach(() => {
if (profileDirState.value) rmSync(profileDirState.value, { recursive: true, force: true })
for (const dir of Object.values(profileDirState.dirs)) {
if (dir !== profileDirState.value) rmSync(dir, { recursive: true, force: true })
}
})
it('reads run history from the request profile directory', async () => {
const researchDir = mkdtempSync(join(tmpdir(), 'hwui-cron-history-research-'))
profileDirState.dirs.research = researchDir
writeJobs([
{
id: 'default-job',
name: 'Default job',
last_run_at: '2026-05-05T01:00:00+00:00',
},
])
writeJobs([
{
id: 'research-job',
name: 'Research job',
last_run_at: '2026-05-05T02:00:00+00:00',
},
], researchDir)
const { listRuns } = await import('../../packages/server/src/controllers/hermes/cron-history')
const ctx = createCtx({ state: { profile: { name: 'research' } } })
await listRuns(ctx)
expect(ctx.body.runs).toEqual([
expect.objectContaining({
jobId: 'research-job',
runTime: '2026-05-05 02:00:00',
}),
])
})
it('surfaces scheduler metadata when a job ran without an output artifact', async () => {
+6 -2
View File
@@ -33,10 +33,12 @@ describe('file routes path metadata', () => {
const { fileRoutes } = await import('../../packages/server/src/routes/hermes/files')
const layer = fileRoutes.stack.find((entry: any) => entry.path === '/api/hermes/files/list')
const ctx: any = { query: { path: 'logs' }, body: null }
const ctx: any = { query: { path: 'logs' }, state: { profile: { name: 'research' } }, body: null }
await layer.stack[0](ctx)
expect(createFileProviderMock).toHaveBeenCalledWith('research')
expect(resolveHermesPathMock).toHaveBeenCalledWith('logs', 'research')
expect(provider.listDir).toHaveBeenCalledWith('/home/agent/.hermes/logs')
expect(ctx.body).toEqual({
path: 'logs',
@@ -65,10 +67,12 @@ describe('file routes path metadata', () => {
const { fileRoutes } = await import('../../packages/server/src/routes/hermes/files')
const layer = fileRoutes.stack.find((entry: any) => entry.path === '/api/hermes/files/stat')
const ctx: any = { query: { path: 'logs/app.log' }, body: null }
const ctx: any = { query: { path: 'logs/app.log' }, state: { profile: { name: 'research' } }, body: null }
await layer.stack[0](ctx)
expect(createFileProviderMock).toHaveBeenCalledWith('research')
expect(resolveHermesPathMock).toHaveBeenCalledWith('logs/app.log', 'research')
expect(ctx.body).toEqual({
name: 'app.log',
path: 'logs/app.log',
+63
View File
@@ -0,0 +1,63 @@
import { Readable } from 'stream'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mkdirMock = vi.hoisted(() => vi.fn())
const writeFileMock = vi.hoisted(() => vi.fn())
vi.mock('fs/promises', async () => {
const actual = await vi.importActual<typeof import('fs/promises')>('fs/promises')
return {
...actual,
mkdir: mkdirMock,
writeFile: writeFileMock,
}
})
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
getActiveProfileName: vi.fn(() => 'default'),
}))
vi.mock('../../packages/server/src/services/hermes/upload-paths', () => ({
getProfileUploadDir: vi.fn((profile: string) => `/tmp/hermes-web-ui/upload/${profile}`),
}))
function multipartBody(boundary: string, name: string, content: string): Buffer {
return Buffer.from([
`--${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${name}"`,
'Content-Type: text/plain',
'',
content,
`--${boundary}--`,
'',
].join('\r\n'))
}
describe('upload controller', () => {
beforeEach(() => {
vi.clearAllMocks()
mkdirMock.mockResolvedValue(undefined)
writeFileMock.mockResolvedValue(undefined)
})
it('stores chat uploads under the request-scoped profile upload directory', async () => {
const boundary = 'test-boundary'
const { handleUpload } = await import('../../packages/server/src/controllers/upload')
const ctx: any = {
get: vi.fn((header: string) => header === 'content-type' ? `multipart/form-data; boundary=${boundary}` : ''),
req: Readable.from([multipartBody(boundary, 'note.txt', 'hello')]),
state: { profile: { name: 'research' } },
body: undefined,
status: 200,
}
await handleUpload(ctx)
expect(mkdirMock).toHaveBeenCalledWith('/tmp/hermes-web-ui/upload/research', { recursive: true })
expect(writeFileMock).toHaveBeenCalledOnce()
const [savedPath, data] = writeFileMock.mock.calls[0]
expect(savedPath).toMatch(/^\/tmp\/hermes-web-ui\/upload\/research\/[a-f0-9]+\.txt$/)
expect(data.toString('utf-8')).toBe('hello')
expect(ctx.body.files[0]).toMatchObject({ name: 'note.txt', path: savedPath })
})
})