chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

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.
This commit is contained in:
2026-10-16 10:34:35 +08:00
commit 1e9ea89d86
912 changed files with 287328 additions and 0 deletions
+283
View File
@@ -0,0 +1,283 @@
import { describe, expect, it, vi } from 'vitest'
const chatRunMock = vi.hoisted(() => ({
runAndWait: vi.fn(),
abortSession: vi.fn(),
}))
vi.mock('../../packages/server/src/routes/hermes/chat-run', () => ({
getChatRunServer: () => chatRunMock,
}))
vi.mock('../../packages/server/src/db/hermes/session-store', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../packages/server/src/db/hermes/session-store')>()
return {
...actual,
getSession: vi.fn(() => null),
getSessionDetail: vi.fn((sessionId: string) => ({
messages: [{ role: 'assistant', content: `output:${sessionId}` }],
})),
deleteSession: vi.fn(),
}
})
describe('workflow manager', () => {
it('returns a server-wide singleton instance', async () => {
const { WorkflowManager, getWorkflowManager } = await import('../../packages/server/src/services/workflow-manager')
const first = getWorkflowManager()
const second = getWorkflowManager()
expect(first).toBe(second)
expect(first).toBeInstanceOf(WorkflowManager)
})
it('stores and emits workflow runtime status updates', async () => {
const { WorkflowManager } = await import('../../packages/server/src/services/workflow-manager')
const manager = new WorkflowManager()
const updates: unknown[] = []
const dispose = manager.onRuntimeStatus(status => updates.push(status))
const status = manager.setRuntimeStatus('workflow-1', {
status: 'running',
runId: 'run-1',
startedAt: 123,
})
expect(status).toMatchObject({
workflowId: 'workflow-1',
status: 'running',
runId: 'run-1',
startedAt: 123,
completedAt: null,
error: null,
})
expect(manager.getRuntimeStatus('workflow-1')).toBe(status)
expect(manager.listRuntimeStatuses()).toEqual([status])
expect(updates).toEqual([status])
dispose()
manager.setRuntimeStatus('workflow-1', { status: 'completed', completedAt: 456 })
expect(updates).toEqual([status])
})
it('maps workflow node agents to the existing run backends', async () => {
const { resolveWorkflowNodeRunTarget } = await import('../../packages/server/src/services/workflow-manager')
expect(resolveWorkflowNodeRunTarget('hermes')).toEqual({
type: 'workflow',
source: 'workflow',
agent: 'hermes',
})
expect(resolveWorkflowNodeRunTarget('claude-code')).toEqual({
type: 'workflow',
source: 'workflow',
agent: 'claude',
codingAgentId: 'claude-code',
})
expect(resolveWorkflowNodeRunTarget('codex')).toEqual({
type: 'workflow',
source: 'workflow',
agent: 'codex',
codingAgentId: 'codex',
})
expect(resolveWorkflowNodeRunTarget('unknown')).toEqual({
type: 'workflow',
source: 'workflow',
agent: 'hermes',
})
})
it('requires workflow node approval only when explicitly enabled', async () => {
const { workflowNodeRequiresApproval } = await import('../../packages/server/src/services/workflow-manager')
expect(workflowNodeRequiresApproval({ data: { approvalRequired: true } })).toBe(true)
expect(workflowNodeRequiresApproval({ data: { approvalRequired: false } })).toBe(false)
expect(workflowNodeRequiresApproval({ data: {} })).toBe(false)
})
it('pauses downstream nodes until an approval-required node is approved', async () => {
const { WorkflowManager } = await import('../../packages/server/src/services/workflow-manager')
const manager = new WorkflowManager()
chatRunMock.runAndWait.mockReset()
chatRunMock.abortSession.mockReset()
chatRunMock.runAndWait.mockResolvedValue({ ok: true, output: 'done' })
const workflow = manager.create({
name: `Approval gate ${Date.now()}`,
profile: 'default',
nodes: [
{
id: 'first',
type: 'agent',
data: {
title: 'First',
agent: 'hermes',
input: 'first task',
approvalRequired: true,
},
},
{
id: 'second',
type: 'agent',
data: {
title: 'Second',
agent: 'hermes',
input: 'second task',
},
},
],
edges: [{ id: 'first-second', source: 'first', target: 'second' }],
})
try {
const runPromise = manager.runNow(workflow.id)
await vi.waitFor(() => {
expect(manager.getRuntimeStatus(workflow.id).nodeStatuses.first).toBe('pending_approval')
})
expect(chatRunMock.runAndWait).toHaveBeenCalledTimes(1)
const runId = manager.getRuntimeStatus(workflow.id).runId
expect(runId).toBeTruthy()
expect(manager.approveNode(workflow.id, runId!, 'first', true)).toBe(true)
await expect(runPromise).resolves.toMatchObject({
run: { status: 'completed' },
})
expect(chatRunMock.runAndWait).toHaveBeenCalledTimes(2)
expect(manager.getRuntimeStatus(workflow.id).nodeStatuses.second).toBe('completed')
} finally {
await manager.delete(workflow.id)
}
})
it('keeps parallel pending approvals open after one node is rejected', async () => {
const { WorkflowManager } = await import('../../packages/server/src/services/workflow-manager')
const manager = new WorkflowManager()
chatRunMock.runAndWait.mockReset()
chatRunMock.abortSession.mockReset()
chatRunMock.runAndWait.mockResolvedValue({ ok: true, output: 'done' })
const workflow = manager.create({
name: `Parallel approvals ${Date.now()}`,
profile: 'default',
nodes: [
{
id: 'first',
type: 'agent',
data: { title: 'First', agent: 'hermes', input: 'first task', approvalRequired: true },
},
{
id: 'second',
type: 'agent',
data: { title: 'Second', agent: 'hermes', input: 'second task', approvalRequired: true },
},
{
id: 'join',
type: 'agent',
data: { title: 'Join', agent: 'hermes', input: 'join task' },
},
],
edges: [
{ id: 'first-join', source: 'first', target: 'join' },
{ id: 'second-join', source: 'second', target: 'join' },
],
})
try {
const runPromise = manager.runNow(workflow.id)
await vi.waitFor(() => {
const statuses = manager.getRuntimeStatus(workflow.id).nodeStatuses
expect(statuses.first).toBe('pending_approval')
expect(statuses.second).toBe('pending_approval')
})
expect(chatRunMock.runAndWait).toHaveBeenCalledTimes(2)
const runId = manager.getRuntimeStatus(workflow.id).runId
expect(runId).toBeTruthy()
expect(manager.approveNode(workflow.id, runId!, 'first', false)).toBe(true)
await vi.waitFor(() => {
const statuses = manager.getRuntimeStatus(workflow.id).nodeStatuses
expect(statuses.first).toBe('approval_rejected')
expect(statuses.second).toBe('pending_approval')
})
expect(manager.approveNode(workflow.id, runId!, 'second', true)).toBe(true)
await expect(runPromise).resolves.toMatchObject({
run: { status: 'failed' },
})
const finalStatuses = manager.getRuntimeStatus(workflow.id).nodeStatuses
expect(finalStatuses.first).toBe('approval_rejected')
expect(finalStatuses.second).toBe('completed')
expect(finalStatuses.join).toBe('canceled')
expect(chatRunMock.runAndWait).toHaveBeenCalledTimes(2)
} finally {
await manager.delete(workflow.id)
}
})
it('reruns incomplete external upstream dependencies for downstream joins', async () => {
const { WorkflowManager } = await import('../../packages/server/src/services/workflow-manager')
const {
createWorkflowRun,
createWorkflowRunNodeSession,
listWorkflowRunNodeSessions,
} = await import('../../packages/server/src/db/hermes/workflow-run-store')
const manager = new WorkflowManager()
chatRunMock.runAndWait.mockReset()
chatRunMock.abortSession.mockReset()
chatRunMock.runAndWait.mockResolvedValue({ ok: true, output: 'done' })
const snapshotNodes = [
{ id: 'entry-a', type: 'agent', data: { title: 'Entry A', agent: 'hermes', input: 'a' } },
{ id: 'entry-b', type: 'agent', data: { title: 'Entry B', agent: 'hermes', input: 'b' } },
{ id: 'join', type: 'agent', data: { title: 'Join', agent: 'hermes', input: 'join' } },
]
const snapshotEdges = [
{ id: 'entry-a-join', source: 'entry-a', target: 'join' },
{ id: 'entry-b-join', source: 'entry-b', target: 'join' },
]
const workflow = manager.create({
name: `Rerun dependencies ${Date.now()}`,
profile: 'default',
nodes: snapshotNodes,
edges: snapshotEdges,
})
const run = createWorkflowRun({
workflow_id: workflow.id,
profile: 'default',
status: 'canceled',
snapshot_nodes: snapshotNodes,
snapshot_edges: snapshotEdges,
started_at: Date.now(),
})
for (const [sequence, nodeId] of ['entry-a', 'entry-b', 'join'].entries()) {
createWorkflowRunNodeSession({
run_id: run.id,
workflow_id: workflow.id,
node_id: nodeId,
session_id: `canceled-${nodeId}`,
profile: 'default',
agent: 'hermes',
status: 'canceled',
sequence,
started_at: Date.now(),
finished_at: Date.now(),
})
}
try {
await expect(manager.rerunFromNode(workflow.id, run.id, 'entry-a')).resolves.toMatchObject({
run: { status: 'completed' },
})
expect(chatRunMock.runAndWait).toHaveBeenCalledTimes(3)
expect(listWorkflowRunNodeSessions(run.id).map(session => [session.node_id, session.status])).toEqual([
['entry-a', 'completed'],
['entry-b', 'completed'],
['join', 'completed'],
])
} finally {
await manager.delete(workflow.id)
}
})
})