Files
hermes-ui/tests/client/jobs.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

156 lines
4.4 KiB
TypeScript

// @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 { listCronRuns } from '../../packages/client/src/api/hermes/cron-history'
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',
skills: [],
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',
skills: [],
repeat_times: null,
})
expect(payload).toEqual({ schedule: 'every 14400m' })
})
it('sends changed skill selections', () => {
const original = makeJob({ skills: ['planner'] })
const payload = buildJobUpdateRequest(original, {
name: original.name,
schedule: 'every 7200m',
prompt: original.prompt,
deliver: 'origin',
skills: ['planner', 'reviewer'],
repeat_times: null,
})
expect(payload).toEqual({ skills: ['planner', 'reviewer'] })
})
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',
})
})
it('sends active profile header when loading job run history', async () => {
localStorage.setItem('hermes_active_profile_name', 'research')
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ runs: [] }),
})
await listCronRuns('job-1')
expect(mockFetch).toHaveBeenCalledOnce()
const [url, options] = mockFetch.mock.calls[0]
expect(url).toBe('/api/cron-history?jobId=job-1')
expect(options.headers['X-Hermes-Profile']).toBe('research')
})
})