mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
feat(workflow): add workflow engine, /workflows panel, /ultracode skill
将 feat/sdk-backend 分支中 workflow 相关的 20 个 commit 压缩为单 commit: - 工作流引擎核心:phase / agent / parallel / pipeline 编排原语(packages/workflow-engine/) - /workflows 面板:三区焦点布局(顶部 run tabs + 左侧 phase 侧栏 + 右侧 agent 列表) - /ultracode skill:多 agent workflow 编排入口 - 进度存储 / journal / notification 系统 - WorkflowService 生命周期管理 + SentryErrorBoundary - 脚本沙箱:禁用 dynamic import()、JSON args 防御性归一化 - journal 与 named-workflow 路径统一在 projectRoot - 错误处理:parallel/pipeline hooks 错误日志、failure routing、semaphore abort - workflow 工具升级为 core 工具 + PascalCase 命名 Co-Authored-By: glm-5.1 <zai-org@claude-code-best.win>
This commit is contained in:
175
src/workflow/__tests__/notifications.test.ts
Normal file
175
src/workflow/__tests__/notifications.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import type { RunProgress } from '../progress/store.js'
|
||||
import type { WorkflowService } from '../service.js'
|
||||
|
||||
function makeMockService(runs: RunProgress[]): {
|
||||
service: WorkflowService
|
||||
emit: () => void
|
||||
setRuns: (runs: RunProgress[]) => void
|
||||
} {
|
||||
let current = runs
|
||||
const listeners = new Set<() => void>()
|
||||
return {
|
||||
service: {
|
||||
ports: {},
|
||||
launch: async () => ({ runId: 'x' }),
|
||||
kill: () => {},
|
||||
listRuns: () => current,
|
||||
getRun: () => undefined,
|
||||
subscribe: (fn: () => void) => {
|
||||
listeners.add(fn)
|
||||
return () => {
|
||||
listeners.delete(fn)
|
||||
}
|
||||
},
|
||||
listNamed: async () => [],
|
||||
} as unknown as WorkflowService,
|
||||
emit: () => {
|
||||
for (const fn of listeners) fn()
|
||||
},
|
||||
setRuns: r => {
|
||||
current = r
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function makeRun(
|
||||
runId: string,
|
||||
status: RunProgress['status'],
|
||||
overrides: Partial<RunProgress> = {},
|
||||
): RunProgress {
|
||||
return {
|
||||
runId,
|
||||
workflowName: 'wf',
|
||||
status,
|
||||
phases: [],
|
||||
declaredPhases: [],
|
||||
currentPhase: null,
|
||||
agents: [],
|
||||
agentCount: 0,
|
||||
updatedAt: Date.now(),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('installWorkflowNotifications', () => {
|
||||
test('running → completed 触发通知(含 workflow 名)', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([
|
||||
makeRun('r1', 'running'),
|
||||
])
|
||||
const calls: string[] = []
|
||||
const unsubscribe = installWorkflowNotifications(service, msg =>
|
||||
calls.push(msg),
|
||||
)
|
||||
|
||||
// 第一次 emit:listener 记录初始 running 状态,不发通知
|
||||
emit()
|
||||
expect(calls.length).toBe(0)
|
||||
|
||||
setRuns([makeRun('r1', 'completed')])
|
||||
emit()
|
||||
|
||||
expect(calls.length).toBe(1)
|
||||
expect(calls[0]).toMatch(/task-notification/)
|
||||
expect(calls[0]).toMatch(/completed successfully/)
|
||||
expect(calls[0]).toMatch(/"wf"/)
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
test('running → failed 触发通知,含 error 文字', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([
|
||||
makeRun('r1', 'running'),
|
||||
])
|
||||
const calls: string[] = []
|
||||
installWorkflowNotifications(service, msg => calls.push(msg))
|
||||
|
||||
emit() // 记录初始 running
|
||||
setRuns([makeRun('r1', 'failed', { error: 'agent X boom' })])
|
||||
emit()
|
||||
|
||||
expect(calls.length).toBe(1)
|
||||
expect(calls[0]).toMatch(/failed/)
|
||||
expect(calls[0]).toMatch(/agent X boom/)
|
||||
})
|
||||
|
||||
test('running → killed 触发通知', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([
|
||||
makeRun('r1', 'running'),
|
||||
])
|
||||
const calls: string[] = []
|
||||
installWorkflowNotifications(service, msg => calls.push(msg))
|
||||
|
||||
emit() // 记录初始 running
|
||||
setRuns([makeRun('r1', 'killed')])
|
||||
emit()
|
||||
|
||||
expect(calls.length).toBe(1)
|
||||
expect(calls[0]).toMatch(/was stopped/)
|
||||
})
|
||||
|
||||
test('初次见到 run(无 prev)不发通知(避免启动时通知历史 run)', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([])
|
||||
const calls: string[] = []
|
||||
installWorkflowNotifications(service, msg => calls.push(msg))
|
||||
|
||||
// 启动后第一次 emit,看到 r1 已 completed——不应通知(不是从 running 转换来)
|
||||
setRuns([makeRun('r1', 'completed')])
|
||||
emit()
|
||||
|
||||
expect(calls.length).toBe(0)
|
||||
})
|
||||
|
||||
test('running → running 不发通知', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([
|
||||
makeRun('r1', 'running'),
|
||||
])
|
||||
const calls: string[] = []
|
||||
installWorkflowNotifications(service, msg => calls.push(msg))
|
||||
|
||||
emit() // 记录初始 running
|
||||
setRuns([makeRun('r1', 'running', { agentCount: 1 })])
|
||||
emit()
|
||||
|
||||
expect(calls.length).toBe(0)
|
||||
})
|
||||
|
||||
test('已 completed 的 run 再次 emit 不重复通知', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([
|
||||
makeRun('r1', 'running'),
|
||||
])
|
||||
const calls: string[] = []
|
||||
installWorkflowNotifications(service, msg => calls.push(msg))
|
||||
|
||||
emit() // 记录初始 running
|
||||
setRuns([makeRun('r1', 'completed')])
|
||||
emit()
|
||||
expect(calls.length).toBe(1)
|
||||
|
||||
emit()
|
||||
expect(calls.length).toBe(1)
|
||||
})
|
||||
|
||||
test('unsubscribe 后不再发通知', async () => {
|
||||
const { installWorkflowNotifications } = await import('../notifications.js')
|
||||
const { service, emit, setRuns } = makeMockService([
|
||||
makeRun('r1', 'running'),
|
||||
])
|
||||
const calls: string[] = []
|
||||
const unsubscribe = installWorkflowNotifications(service, msg =>
|
||||
calls.push(msg),
|
||||
)
|
||||
|
||||
emit() // 记录初始 running
|
||||
unsubscribe()
|
||||
setRuns([makeRun('r1', 'completed')])
|
||||
emit()
|
||||
|
||||
expect(calls.length).toBe(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user