Files
claude-code/src/workflow/__tests__/notifications.test.ts
claude-code-best 54d2bf6f12 feat(workflow): 复刻 ultracode 手册并修复 worktree/inline/opt-in 三处缺口
围绕 ultracode skill 审查 agent 系统一致性后:
- ultracode.ts: 用系统提示版完整 Workflow 编排手册替换中文精简版
- HIGH#1 isolation:'worktree': claudeCodeBackend.run() 用 createAgentWorktree +
  runWithCwdOverride 包裹 runAgent + finally 清理实现真正的 cwd 隔离;slug 用
  sha256(runId:agentId) 派生以匹配 cleanupStaleAgentWorktrees 清理正则
  (修 runId 为 w+base36 非 UUID 导致的泄漏盲区);worktree.ts 注释同步修正
- HIGH#2 inline 持久化: 新增 persistInlineScript,WorkflowTool + service 两条
  inline 路径对称持久化到 .claude/workflow-runs/<runId>/script.js,返回可复用
  scriptPath(闭环 inline→编辑→scriptPath 重提迭代循环)
- HIGH#3 opt-in 分工: ultracode/WorkflowTool/effort 注明 session reminder 由
  harness 注入,repo 内无 ultracode 信号,保持 feature('WORKFLOW_SCRIPTS') +
  isEnabled 两层 gate,不自造注入
- 测试: 新增 persistInline.test.ts;扩展 claudeCodeBackend(isolation 4 用例)/
  WorkflowTool(inline)/service(scriptPath)/ultracode(harness)

含配套 workflow engine/panel 完善与 run-state-persistence design doc。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 23:04:33 +08:00

177 lines
5.0 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 { 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,
startedAt: Date.now(),
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),
)
// 第一次 emitlistener 记录初始 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)
})
})