Files
Hermes-ui/tests/client/jobs.test.ts
T

122 lines
3.4 KiB
TypeScript
Raw Normal View History

2026-05-01 02:12:53 +02:00
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockFetch = vi.fn()
vi.stubGlobal('fetch', mockFetch)
vi.mock('@/router', () => ({
default: {
currentRoute: { value: { name: 'hermes.jobs' } },
replace: vi.fn(),
},
}))
import {
buildJobUpdateRequest,
scheduleToDisplayText,
scheduleToEditableInput,
updateJob,
} from '../../packages/client/src/api/hermes/jobs'
import type { Job } from '../../packages/client/src/api/hermes/jobs'
function makeJob(overrides: Partial<Job> = {}): Job {
return {
job_id: 'job-1',
id: 'job-1',
name: 'artifact cleanup',
prompt: 'short prompt',
skills: [],
skill: null,
model: null,
provider: null,
base_url: null,
script: null,
schedule: { kind: 'interval', minutes: 7200, display: 'every 7200m' },
schedule_display: 'every 7200m',
repeat: { times: null, completed: 0 },
enabled: true,
state: 'scheduled',
paused_at: null,
paused_reason: null,
created_at: '2026-04-30T00:00:00Z',
next_run_at: null,
last_run_at: null,
last_status: null,
last_error: null,
deliver: 'origin',
origin: null,
last_delivery_error: null,
...overrides,
}
}
describe('Hermes jobs edit payloads', () => {
beforeEach(() => {
localStorage.clear()
vi.clearAllMocks()
})
it('uses display text for interval schedules without manufacturing expr', () => {
const schedule = { kind: 'interval' as const, minutes: 7200, display: 'every 7200m' }
expect(scheduleToEditableInput(schedule, '')).toBe('every 7200m')
expect(scheduleToDisplayText(schedule, '—')).toBe('every 7200m')
})
it('keeps cron expr as the editable schedule string', () => {
const schedule = { kind: 'cron' as const, expr: '0 9 * * 1', display: '0 9 * * 1' }
expect(scheduleToEditableInput(schedule, '')).toBe('0 9 * * 1')
expect(scheduleToDisplayText(schedule, '—')).toBe('0 9 * * 1')
})
it('omits unchanged long prompts from name-only updates', () => {
const prompt = 'x'.repeat(9484)
const original = makeJob({ prompt })
const payload = buildJobUpdateRequest(original, {
name: 'artifact cleanup renamed',
schedule: 'every 7200m',
prompt,
deliver: 'origin',
repeat_times: null,
})
expect(payload).toEqual({ name: 'artifact cleanup renamed' })
expect(payload).not.toHaveProperty('prompt')
expect(payload).not.toHaveProperty('schedule')
})
it('sends changed interval schedules as raw strings', () => {
const original = makeJob()
const payload = buildJobUpdateRequest(original, {
name: original.name,
schedule: 'every 14400m',
prompt: original.prompt,
deliver: 'origin',
repeat_times: null,
})
expect(payload).toEqual({ schedule: 'every 14400m' })
})
it('does not send a PATCH body with structured schedule objects', async () => {
const returnedJob = makeJob({ name: 'artifact cleanup renamed' })
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ job: returnedJob }),
})
await updateJob('job-1', { name: 'artifact cleanup renamed', schedule: 'every 14400m' })
expect(mockFetch).toHaveBeenCalledOnce()
const [, options] = mockFetch.mock.calls[0]
expect(JSON.parse(options.body as string)).toEqual({
name: 'artifact cleanup renamed',
schedule: 'every 14400m',
})
})
})