mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35: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:
155
packages/workflow-engine/src/__tests__/agentAdapter.test.ts
Normal file
155
packages/workflow-engine/src/__tests__/agentAdapter.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import {
|
||||
AgentAdapterRegistry,
|
||||
AdapterNotFoundError,
|
||||
type AgentAdapter,
|
||||
} from '../agentAdapter.js'
|
||||
import { createHostHandle } from '../ports.js'
|
||||
import type { AgentRunParams, AgentRunResult } from '../types.js'
|
||||
|
||||
function makeAdapter(
|
||||
id: string,
|
||||
result: AgentRunResult = {
|
||||
kind: 'ok',
|
||||
output: `out-${id}`,
|
||||
usage: { outputTokens: 1 },
|
||||
},
|
||||
): AgentAdapter {
|
||||
return {
|
||||
id,
|
||||
capabilities: { structuredOutput: true },
|
||||
async run() {
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const P = (over: Partial<AgentRunParams> = {}): AgentRunParams => ({
|
||||
prompt: 'p',
|
||||
...over,
|
||||
})
|
||||
|
||||
const CTX = {
|
||||
host: createHostHandle(null),
|
||||
signal: new AbortController().signal,
|
||||
runId: 'r',
|
||||
}
|
||||
|
||||
test('resolve 默认走 default adapter,run 返回结果', async () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('a'))
|
||||
.register(makeAdapter('b'))
|
||||
.default('a')
|
||||
expect(reg.resolve(P()).id).toBe('a')
|
||||
const r = await reg.resolve(P()).run(P(), CTX)
|
||||
expect(r.kind).toBe('ok')
|
||||
})
|
||||
|
||||
test('route agentType 命中优先于 default', () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('default'))
|
||||
.register(makeAdapter('research'))
|
||||
.route({ kind: 'agentType', agentType: 'researcher', adapter: 'research' })
|
||||
.default('default')
|
||||
expect(reg.resolve(P({ agentType: 'researcher' })).id).toBe('research')
|
||||
expect(reg.resolve(P({ agentType: 'other' })).id).toBe('default')
|
||||
})
|
||||
|
||||
test('route model 前缀匹配', () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('cheap'))
|
||||
.register(makeAdapter('strong'))
|
||||
.route({ kind: 'model', pattern: 'claude-opus', adapter: 'strong' })
|
||||
.default('cheap')
|
||||
expect(reg.resolve(P({ model: 'claude-opus-4' })).id).toBe('strong')
|
||||
expect(reg.resolve(P({ model: 'claude-sonnet-4' })).id).toBe('cheap')
|
||||
expect(reg.resolve(P()).id).toBe('cheap') // 无 model → default
|
||||
})
|
||||
|
||||
test('route custom 谓词', () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('main'))
|
||||
.register(makeAdapter('special'))
|
||||
.route({
|
||||
kind: 'custom',
|
||||
match: p => p.prompt.includes('VIP'),
|
||||
adapter: 'special',
|
||||
})
|
||||
.default('main')
|
||||
expect(reg.resolve(P({ prompt: 'handle VIP case' })).id).toBe('special')
|
||||
expect(reg.resolve(P({ prompt: 'normal' })).id).toBe('main')
|
||||
})
|
||||
|
||||
test('规则按顺序匹配(先命中先用)', () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('a'))
|
||||
.register(makeAdapter('b'))
|
||||
.route({ kind: 'agentType', agentType: 'x', adapter: 'a' })
|
||||
.route({ kind: 'agentType', agentType: 'x', adapter: 'b' })
|
||||
expect(reg.resolve(P({ agentType: 'x' })).id).toBe('a')
|
||||
})
|
||||
|
||||
test('规则命中的 adapter 未注册 → 跳过该规则继续匹配', () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('real'))
|
||||
.route({ kind: 'agentType', agentType: 'x', adapter: 'ghost' })
|
||||
.route({ kind: 'agentType', agentType: 'x', adapter: 'real' })
|
||||
expect(reg.resolve(P({ agentType: 'x' })).id).toBe('real')
|
||||
})
|
||||
|
||||
test('无匹配且无 default → AdapterNotFoundError', () => {
|
||||
const reg = new AgentAdapterRegistry().register(makeAdapter('a'))
|
||||
expect(() => reg.resolve(P())).toThrow(AdapterNotFoundError)
|
||||
})
|
||||
|
||||
test('default 指向未注册的 adapter → 仍抛(不静默回退)', () => {
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(makeAdapter('a'))
|
||||
.default('missing')
|
||||
expect(() => reg.resolve(P())).toThrow(AdapterNotFoundError)
|
||||
})
|
||||
|
||||
test('has / get', () => {
|
||||
const reg = new AgentAdapterRegistry().register(makeAdapter('a'))
|
||||
expect(reg.has('a')).toBe(true)
|
||||
expect(reg.has('b')).toBe(false)
|
||||
expect(reg.get('a')?.id).toBe('a')
|
||||
expect(reg.get('b')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('initializeAll / disposeAll 触发 lifecycle(跳过未实现)', async () => {
|
||||
const events: string[] = []
|
||||
const withLifecycle: AgentAdapter = {
|
||||
id: 'a',
|
||||
capabilities: { structuredOutput: false },
|
||||
async run() {
|
||||
return { kind: 'ok', output: 'x', usage: { outputTokens: 1 } }
|
||||
},
|
||||
async initialize() {
|
||||
events.push('init-a')
|
||||
},
|
||||
async dispose() {
|
||||
events.push('dispose-a')
|
||||
},
|
||||
}
|
||||
const noLifecycle = makeAdapter('b') // 无 initialize/dispose
|
||||
const reg = new AgentAdapterRegistry()
|
||||
.register(withLifecycle)
|
||||
.register(noLifecycle)
|
||||
await reg.initializeAll()
|
||||
await reg.disposeAll()
|
||||
expect(events).toEqual(['init-a', 'dispose-a'])
|
||||
})
|
||||
|
||||
test('capabilities 声明可读', () => {
|
||||
const adapter: AgentAdapter = {
|
||||
id: 'a',
|
||||
capabilities: { structuredOutput: true, tools: true, stream: false },
|
||||
async run() {
|
||||
return { kind: 'ok', output: 'x', usage: { outputTokens: 1 } }
|
||||
},
|
||||
}
|
||||
expect(adapter.capabilities.structuredOutput).toBe(true)
|
||||
expect(adapter.capabilities.tools).toBe(true)
|
||||
expect(adapter.capabilities.stream).toBe(false)
|
||||
})
|
||||
Reference in New Issue
Block a user