Files
claude-code/packages/workflow-engine/src/__tests__/agentAdapter.test.ts
claude-code-best bcd6771c48 feat(workflow): 中断系统(x 杀单 agent / K 杀整个 workflow,Dialog 二次确认)
- claudeCodeBackend 桥接 ctx.signal → runAgent.override.abortController(修 'x' 无效根因:abort 到不了内部 fetch)
- AbortError 识别为 throw WorkflowAbortedError(不再吞成 dead,workflow 能感知被 kill)
- ports.taskRegistrar 加 registerAgentAbort/unregisterAgentAbort/killAgent;service.killAgent(runId, agentId) 精确中断
- 面板键位:'x' 杀当前 agent(agents 列聚焦时) / 'K' 杀整个 workflow;Dialog 二次确认 + confirm 模式吞导航键防误触
- 新增测试 8 项(backend signal bridge / hooks inject / ports killAgent / service killAgent)

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
2026-06-14 10:38:15 +08:00

157 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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',
agentId: 1,
}
test('resolve 默认走 default adapterrun 返回结果', 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)
})