import { beforeEach, describe, expect, it, vi } from 'vitest' const managerMock = vi.hoisted(() => ({ get: vi.fn(), deleteRun: vi.fn(), rerunFromNode: vi.fn(), runNow: vi.fn(), stopRun: vi.fn(), approveNode: vi.fn(), })) const listWorkflowRunsMock = vi.hoisted(() => vi.fn()) const listWorkflowRunNodeSessionsMock = vi.hoisted(() => vi.fn()) const listUserProfilesMock = vi.hoisted(() => vi.fn()) vi.mock('../../packages/server/src/services/workflow-manager', () => ({ getWorkflowManager: () => managerMock, })) vi.mock('../../packages/server/src/db/hermes/users-store', () => ({ listUserProfiles: listUserProfilesMock, })) vi.mock('../../packages/server/src/db/hermes/workflow-run-store', () => ({ listWorkflowRunNodeSessions: listWorkflowRunNodeSessionsMock, listWorkflowRuns: listWorkflowRunsMock, })) function ctx(overrides: Record = {}) { return { params: {}, query: {}, request: { body: {} }, state: {}, status: 200, body: undefined, ...overrides, } as any } describe('workflow controller', () => { beforeEach(() => { managerMock.get.mockReset() managerMock.deleteRun.mockReset() managerMock.rerunFromNode.mockReset() managerMock.runNow.mockReset() managerMock.stopRun.mockReset() managerMock.approveNode.mockReset() listWorkflowRunNodeSessionsMock.mockReset() listWorkflowRunsMock.mockReset() listUserProfilesMock.mockReset() listUserProfilesMock.mockReturnValue([]) }) it('lists run records for a workflow', async () => { managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) listWorkflowRunsMock.mockReturnValue([{ id: 'run-1', workflow_id: 'workflow-1', status: 'completed' }]) listWorkflowRunNodeSessionsMock.mockReturnValue([{ id: 'node-session-1', node_id: 'node-1', status: 'completed' }]) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1' }, query: { limit: '25' } }) await mod.listRuns(c) expect(listWorkflowRunsMock).toHaveBeenCalledWith('workflow-1', 25) expect(listWorkflowRunNodeSessionsMock).toHaveBeenCalledWith('run-1') expect(c.body).toEqual({ runs: [{ id: 'run-1', workflow_id: 'workflow-1', status: 'completed', node_sessions: [{ id: 'node-session-1', node_id: 'node-1', status: 'completed' }], }], }) }) it('runs a workflow through the workflow manager', async () => { const user = { id: 'user-1', role: 'super_admin' } managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) managerMock.runNow.mockResolvedValue({ run: { id: 'run-1', status: 'completed' }, nodeSessions: [] }) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1' }, request: { body: { start_node_ids: ['node-1', 12, 'node-2'], input: 'go', timeout_ms: '1000' } }, state: { user }, }) await mod.runNow(c) expect(managerMock.runNow).toHaveBeenCalledWith('workflow-1', { profile: 'default', user, startNodeIds: ['node-1', 'node-2'], input: 'go', timeoutMs: 1000, }) expect(c.status).toBe(202) expect(c.body).toEqual({ ok: true, status: 'accepted' }) }) it('stops a workflow run through the workflow manager', async () => { managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) managerMock.stopRun.mockResolvedValue({ id: 'run-1', workflow_id: 'workflow-1', status: 'canceled' }) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1', runId: 'run-1' } }) await mod.stopRun(c) expect(managerMock.stopRun).toHaveBeenCalledWith('workflow-1', 'run-1', 'Workflow run canceled by user') expect(c.body).toEqual({ ok: true, run: { id: 'run-1', workflow_id: 'workflow-1', status: 'canceled' }, }) }) it('reruns a workflow run from a node through the workflow manager', async () => { const user = { id: 'user-1', role: 'super_admin' } managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) managerMock.rerunFromNode.mockResolvedValue({ run: { id: 'run-1', status: 'completed' }, nodeSessions: [] }) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1', runId: 'run-1' }, request: { body: { node_id: 'node-2', preserve_start_node: true, timeout_ms: '1000' } }, state: { user }, }) await mod.rerunFromNode(c) expect(managerMock.rerunFromNode).toHaveBeenCalledWith('workflow-1', 'run-1', 'node-2', { profile: 'default', user, preserveStartNode: true, timeoutMs: 1000, }) expect(c.status).toBe(202) expect(c.body).toEqual({ ok: true, status: 'accepted' }) }) it('deletes a workflow run through the workflow manager', async () => { managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) managerMock.deleteRun.mockResolvedValue(true) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1', runId: 'run-1' } }) await mod.deleteRun(c) expect(managerMock.deleteRun).toHaveBeenCalledWith('workflow-1', 'run-1') expect(c.body).toEqual({ ok: true }) }) it('approves a pending workflow node through the workflow manager', async () => { managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) managerMock.approveNode.mockReturnValue(true) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1', runId: 'run-1', nodeId: 'node-1' }, request: { body: { approved: true } }, }) await mod.approveNode(c) expect(managerMock.approveNode).toHaveBeenCalledWith('workflow-1', 'run-1', 'node-1', true) expect(managerMock.stopRun).not.toHaveBeenCalled() expect(c.body).toEqual({ ok: true }) }) it('stops a workflow run when a pending workflow node is rejected', async () => { managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'default' }) managerMock.approveNode.mockReturnValue(true) managerMock.stopRun.mockResolvedValue({ id: 'run-1', workflow_id: 'workflow-1', status: 'canceled' }) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1', runId: 'run-1', nodeId: 'node-1' }, request: { body: { approved: false } }, }) await mod.approveNode(c) expect(managerMock.approveNode).toHaveBeenCalledWith('workflow-1', 'run-1', 'node-1', false) expect(managerMock.stopRun).toHaveBeenCalledWith('workflow-1', 'run-1', 'Workflow node approval rejected') expect(c.body).toEqual({ ok: true }) }) it('rejects workflow runs for unavailable profiles', async () => { managerMock.get.mockReturnValue({ id: 'workflow-1', profile: 'secret' }) listUserProfilesMock.mockReturnValue([{ profile_name: 'default' }]) const mod = await import('../../packages/server/src/controllers/hermes/workflows') const c = ctx({ params: { id: 'workflow-1' }, state: { user: { id: 'user-1', role: 'user' } }, }) await mod.runNow(c) expect(c.status).toBe(403) expect(managerMock.runNow).not.toHaveBeenCalled() }) })