mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-10 02:30:15 +00:00
f5a292663b
* feat: add workflow builder page * Add workflow schema and editor controls * feat: add workflow builder persistence * feat: add workflow socket and agent skills * feat: complete workflow run execution * test: fix workflow ci coverage failures --------- Co-authored-by: Codex <codex@openai.com>
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { existsSync, mkdtempSync, rmSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
|
|
const state = vi.hoisted(() => ({
|
|
db: null as DatabaseSync | null,
|
|
appHome: '',
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/db/index', () => ({
|
|
getDb: () => state.db,
|
|
jsonDelete: vi.fn(),
|
|
jsonGet: vi.fn(),
|
|
jsonGetAll: vi.fn(() => ({})),
|
|
jsonSet: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/config', () => ({
|
|
config: {
|
|
appHome: state.appHome,
|
|
},
|
|
}))
|
|
|
|
describe('workflow store', () => {
|
|
let root: string
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
root = mkdtempSync(join(tmpdir(), 'hermes-workflow-store-'))
|
|
state.appHome = join(root, 'home')
|
|
state.db = new DatabaseSync(join(root, 'workflow.db'))
|
|
const { initAllHermesTables } = await import('../../packages/server/src/db/hermes/schemas')
|
|
initAllHermesTables()
|
|
})
|
|
|
|
afterEach(() => {
|
|
state.db?.close()
|
|
state.db = null
|
|
rmSync(root, { recursive: true, force: true })
|
|
})
|
|
|
|
it('creates workflows with a profile-scoped default workspace', async () => {
|
|
const { createWorkflow, getWorkflow } = await import('../../packages/server/src/db/hermes/workflow-store')
|
|
|
|
const workflow = createWorkflow({
|
|
name: 'Research flow',
|
|
profile: 'research',
|
|
nodes: [{ id: 'agent-1' }],
|
|
edges: [],
|
|
viewport: { x: 12, y: 24, zoom: 0.8 },
|
|
})
|
|
|
|
expect(workflow.profile).toBe('research')
|
|
expect(workflow.workspace).toBe(join(state.appHome, 'workflow', 'research', workflow.id))
|
|
expect(existsSync(workflow.workspace!)).toBe(true)
|
|
expect(getWorkflow(workflow.id)).toMatchObject({
|
|
id: workflow.id,
|
|
name: 'Research flow',
|
|
profile: 'research',
|
|
nodes: [{ id: 'agent-1' }],
|
|
viewport: { x: 12, y: 24, zoom: 0.8 },
|
|
})
|
|
})
|
|
|
|
it('updates and deletes workflows', async () => {
|
|
const { createWorkflow, deleteWorkflow, getWorkflow, listWorkflows, updateWorkflow } = await import('../../packages/server/src/db/hermes/workflow-store')
|
|
const workflow = createWorkflow({ name: 'Draft', profile: 'default' })
|
|
|
|
const updated = updateWorkflow(workflow.id, {
|
|
name: 'Updated',
|
|
workspace: null,
|
|
nodes: [{ id: 'agent-2' }],
|
|
edges: [{ source: 'agent-1', target: 'agent-2' }],
|
|
viewport: { x: -120, y: 88, zoom: 1.1 },
|
|
})
|
|
|
|
expect(updated).toMatchObject({
|
|
id: workflow.id,
|
|
name: 'Updated',
|
|
nodes: [{ id: 'agent-2' }],
|
|
edges: [{ source: 'agent-1', target: 'agent-2' }],
|
|
viewport: { x: -120, y: 88, zoom: 1.1 },
|
|
})
|
|
expect(updated?.workspace).toBe(workflow.workspace)
|
|
expect(listWorkflows('default').map(item => item.id)).toContain(workflow.id)
|
|
expect(deleteWorkflow(workflow.id)).toBe(true)
|
|
expect(getWorkflow(workflow.id)).toBeNull()
|
|
})
|
|
|
|
it('lists workflow runs by workflow ordered newest first', async () => {
|
|
const { createWorkflow } = await import('../../packages/server/src/db/hermes/workflow-store')
|
|
const { createWorkflowRun, listWorkflowRuns, updateWorkflowRun } = await import('../../packages/server/src/db/hermes/workflow-run-store')
|
|
const workflow = createWorkflow({ name: 'Runs', profile: 'default' })
|
|
const other = createWorkflow({ name: 'Other', profile: 'default' })
|
|
|
|
const first = createWorkflowRun({ workflow_id: workflow.id, status: 'running', started_at: 100 })
|
|
await new Promise(resolve => setTimeout(resolve, 2))
|
|
const second = createWorkflowRun({ workflow_id: workflow.id, status: 'queued', started_at: 200 })
|
|
createWorkflowRun({ workflow_id: other.id, status: 'running' })
|
|
updateWorkflowRun(first.id, { status: 'completed', finished_at: 300 })
|
|
|
|
expect(listWorkflowRuns(workflow.id).map(run => run.id)).toEqual([second.id, first.id])
|
|
expect(listWorkflowRuns(workflow.id, 1)).toHaveLength(1)
|
|
expect(listWorkflowRuns(workflow.id)[1]).toMatchObject({
|
|
id: first.id,
|
|
status: 'completed',
|
|
finished_at: 300,
|
|
})
|
|
})
|
|
|
|
it('deletes workflow runs and their node session records', async () => {
|
|
const { createWorkflow } = await import('../../packages/server/src/db/hermes/workflow-store')
|
|
const {
|
|
createWorkflowRun,
|
|
createWorkflowRunNodeSession,
|
|
deleteWorkflowRun,
|
|
getWorkflowRun,
|
|
listWorkflowRunNodeSessions,
|
|
} = await import('../../packages/server/src/db/hermes/workflow-run-store')
|
|
const workflow = createWorkflow({ name: 'Runs', profile: 'default' })
|
|
const run = createWorkflowRun({ workflow_id: workflow.id, status: 'completed' })
|
|
createWorkflowRunNodeSession({
|
|
run_id: run.id,
|
|
workflow_id: workflow.id,
|
|
node_id: 'node-1',
|
|
session_id: 'session-1',
|
|
status: 'completed',
|
|
})
|
|
|
|
expect(listWorkflowRunNodeSessions(run.id)).toHaveLength(1)
|
|
expect(deleteWorkflowRun(run.id)).toBe(true)
|
|
|
|
expect(getWorkflowRun(run.id)).toBeNull()
|
|
expect(listWorkflowRunNodeSessions(run.id)).toEqual([])
|
|
})
|
|
})
|