@meiyukichan/dsh-sandore 0.1.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/LICENSE +201 -0
  2. package/cordis.patch.yml +39 -0
  3. package/dsh.plugin.json +16 -0
  4. package/lib/client/CommandEditor.d.ts +19 -0
  5. package/lib/client/DagEditor.d.ts +22 -0
  6. package/lib/client/DefinitionDetail.d.ts +13 -0
  7. package/lib/client/DefinitionList.d.ts +19 -0
  8. package/lib/client/DiscussionInput.d.ts +63 -0
  9. package/lib/client/DragHandle.d.ts +16 -0
  10. package/lib/client/EditorPane.d.ts +13 -0
  11. package/lib/client/FlowEditor.d.ts +31 -0
  12. package/lib/client/InstanceList.d.ts +24 -0
  13. package/lib/client/NewInstanceDialog.d.ts +19 -0
  14. package/lib/client/PreviewPanel.d.ts +13 -0
  15. package/lib/client/RunView.d.ts +79 -0
  16. package/lib/client/StepChatPanel.d.ts +34 -0
  17. package/lib/client/TreeNav.d.ts +27 -0
  18. package/lib/client/UnifiedTimeline.d.ts +11 -0
  19. package/lib/client/WorkflowButton.d.ts +11 -0
  20. package/lib/client/WorkflowList.d.ts +18 -0
  21. package/lib/client/WorkflowOverlay.d.ts +6 -0
  22. package/lib/client/chat/AssistantMarkdown.d.ts +22 -0
  23. package/lib/client/chat/CompactionItem.d.ts +24 -0
  24. package/lib/client/chat/ContextBody.d.ts +136 -0
  25. package/lib/client/chat/ContextInjectionRow.d.ts +25 -0
  26. package/lib/client/chat/GenericCommandCard.d.ts +10 -0
  27. package/lib/client/chat/MessageIconActions.d.ts +41 -0
  28. package/lib/client/chat/MessageItem.d.ts +65 -0
  29. package/lib/client/chat/ReasoningRow.d.ts +13 -0
  30. package/lib/client/chat/TurnTailNodeView.d.ts +29 -0
  31. package/lib/client/chat/UserMessageRow.d.ts +18 -0
  32. package/lib/client/chat/attachment/ImageLightbox.d.ts +26 -0
  33. package/lib/client/chat/attachment/MessageImage.d.ts +56 -0
  34. package/lib/client/chat/attachment/image-attachment.d.ts +41 -0
  35. package/lib/client/chat/attachment/image-labels.d.ts +21 -0
  36. package/lib/client/chat/message-chrome.d.ts +51 -0
  37. package/lib/client/chat/reference/ReferenceIcon.d.ts +20 -0
  38. package/lib/client/chat/turn-assistant.d.ts +12 -0
  39. package/lib/client/chat/turn-metrics.d.ts +42 -0
  40. package/lib/client/chat/use-calendar-day.d.ts +5 -0
  41. package/lib/client/chat/use-throttled-visual-update.d.ts +7 -0
  42. package/lib/client/chat-flow.d.ts +110 -0
  43. package/lib/client/composer-selects.d.ts +17 -0
  44. package/lib/client/dag-editor-model.d.ts +141 -0
  45. package/lib/client/describe-error.d.ts +9 -0
  46. package/lib/client/index.d.ts +6 -0
  47. package/lib/client/locales.d.ts +777 -0
  48. package/lib/client/model-faces.d.ts +156 -0
  49. package/lib/client/open-discussion.d.ts +24 -0
  50. package/lib/client/panel-model.d.ts +34 -0
  51. package/lib/client/remote.d.ts +34 -0
  52. package/lib/client/run-view-model.d.ts +304 -0
  53. package/lib/client/sandore-save.d.ts +41 -0
  54. package/lib/client/sandore-sse.d.ts +15 -0
  55. package/lib/client/step-chat-retry.d.ts +44 -0
  56. package/lib/client/tool/GenericToolCard.d.ts +28 -0
  57. package/lib/client/tool/ToolRow.d.ts +95 -0
  58. package/lib/client/tool/diff-card-model.d.ts +63 -0
  59. package/lib/client/tool/read-card-model.d.ts +67 -0
  60. package/lib/client/tool/search-card-model.d.ts +94 -0
  61. package/lib/client/tool/terminal-card-model.d.ts +86 -0
  62. package/lib/client/tool/tool-call-model.d.ts +74 -0
  63. package/lib/client/tool/web-card-model.d.ts +44 -0
  64. package/lib/client/use-focus-refresh.d.ts +1 -0
  65. package/lib/client/workflow-face.d.ts +68 -0
  66. package/lib/client/workflow-store.d.ts +80 -0
  67. package/lib/client/workspace-path.d.ts +15 -0
  68. package/lib/client.js +38389 -0
  69. package/lib/index.js +8854 -0
  70. package/lib/microflow/branch-evaluator.d.ts +290 -0
  71. package/lib/microflow/concurrency-limiter.d.ts +39 -0
  72. package/lib/microflow/errors.d.ts +39 -0
  73. package/lib/microflow/file-utils.d.ts +210 -0
  74. package/lib/microflow/finalize-manager.d.ts +414 -0
  75. package/lib/microflow/fork-session.d.ts +151 -0
  76. package/lib/microflow/index.d.ts +17 -0
  77. package/lib/microflow/internal.d.ts +56 -0
  78. package/lib/microflow/loop-manager.d.ts +127 -0
  79. package/lib/microflow/rules-resolver.d.ts +28 -0
  80. package/lib/microflow/stage-executor.d.ts +362 -0
  81. package/lib/microflow/step-navigator.d.ts +108 -0
  82. package/lib/microflow/step-runner.d.ts +190 -0
  83. package/lib/microflow/subagents.d.ts +380 -0
  84. package/lib/tool/index.d.ts +65 -0
  85. package/lib/typert.host.d.ts +16 -0
  86. package/lib/typert.host.js +25 -0
  87. package/lib/typert.remote-client.d.ts +114 -0
  88. package/lib/typert.remote-client.js +399 -0
  89. package/lib/workflow/ask-tool.d.ts +78 -0
  90. package/lib/workflow/dag-navigator.d.ts +136 -0
  91. package/lib/workflow/domain.d.ts +304 -0
  92. package/lib/workflow/engine-surface.d.ts +70 -0
  93. package/lib/workflow/errors.d.ts +13 -0
  94. package/lib/workflow/host-agent.d.ts +53 -0
  95. package/lib/workflow/index.d.ts +23 -0
  96. package/lib/workflow/interaction-manager.d.ts +170 -0
  97. package/lib/workflow/lifecycle-manager.d.ts +111 -0
  98. package/lib/workflow/persistence.d.ts +35 -0
  99. package/lib/workflow/plugin.d.ts +27 -0
  100. package/lib/workflow/reactive-state.d.ts +27 -0
  101. package/lib/workflow/recovery-manager.d.ts +158 -0
  102. package/lib/workflow/remote.d.ts +139 -0
  103. package/lib/workflow/schema.d.ts +1917 -0
  104. package/lib/workflow/seam.d.ts +254 -0
  105. package/lib/workflow/shell.d.ts +127 -0
  106. package/lib/workflow/sse.d.ts +16 -0
  107. package/lib/workflow/typert/typert.host.d.ts +16 -0
  108. package/lib/workflow/typert/typert.remote-client.d.ts +114 -0
  109. package/lib/workflow/types.d.ts +187 -0
  110. package/lib/workflow/wire.d.ts +256 -0
  111. package/lib/workflow/workflow-engine.d.ts +373 -0
  112. package/lib/workflow/yaml-error.d.ts +22 -0
  113. package/lib/workflow/yaml-parser.d.ts +23 -0
  114. package/package.json +140 -0
  115. package/packages/client/CommandEditor.tsx +73 -0
  116. package/packages/client/DagEditor.module.css +711 -0
  117. package/packages/client/DagEditor.tsx +1203 -0
  118. package/packages/client/DefinitionDetail.module.css +110 -0
  119. package/packages/client/DefinitionDetail.tsx +126 -0
  120. package/packages/client/DefinitionList.tsx +173 -0
  121. package/packages/client/DiscussionInput.tsx +763 -0
  122. package/packages/client/DragHandle.tsx +92 -0
  123. package/packages/client/EditorPane.tsx +70 -0
  124. package/packages/client/FlowEditor.tsx +227 -0
  125. package/packages/client/InstanceList.tsx +250 -0
  126. package/packages/client/NewInstanceDialog.module.css +244 -0
  127. package/packages/client/NewInstanceDialog.tsx +478 -0
  128. package/packages/client/PreviewPanel.tsx +55 -0
  129. package/packages/client/RunView.module.css +246 -0
  130. package/packages/client/RunView.tsx +314 -0
  131. package/packages/client/StepChatPanel.tsx +486 -0
  132. package/packages/client/TreeNav.tsx +294 -0
  133. package/packages/client/UnifiedTimeline.tsx +484 -0
  134. package/packages/client/WorkflowButton.module.css +72 -0
  135. package/packages/client/WorkflowButton.tsx +50 -0
  136. package/packages/client/WorkflowList.tsx +158 -0
  137. package/packages/client/WorkflowOverlay.module.css +390 -0
  138. package/packages/client/WorkflowOverlay.tsx +136 -0
  139. package/packages/client/WorkflowPanel.module.css +247 -0
  140. package/packages/client/chat/AssistantMarkdown.module.css +38 -0
  141. package/packages/client/chat/AssistantMarkdown.tsx +120 -0
  142. package/packages/client/chat/ChatView.module.css +198 -0
  143. package/packages/client/chat/CompactionItem.tsx +85 -0
  144. package/packages/client/chat/ContextBody.module.css +162 -0
  145. package/packages/client/chat/ContextBody.tsx +598 -0
  146. package/packages/client/chat/ContextInjectionRow.module.css +63 -0
  147. package/packages/client/chat/ContextInjectionRow.tsx +78 -0
  148. package/packages/client/chat/GenericCommandCard.module.css +87 -0
  149. package/packages/client/chat/GenericCommandCard.tsx +76 -0
  150. package/packages/client/chat/MessageIconActions.module.css +84 -0
  151. package/packages/client/chat/MessageIconActions.tsx +147 -0
  152. package/packages/client/chat/MessageItem.module.css +293 -0
  153. package/packages/client/chat/MessageItem.tsx +384 -0
  154. package/packages/client/chat/ReasoningRow.module.css +82 -0
  155. package/packages/client/chat/ReasoningRow.tsx +70 -0
  156. package/packages/client/chat/TurnTailNodeView.module.css +10 -0
  157. package/packages/client/chat/TurnTailNodeView.tsx +63 -0
  158. package/packages/client/chat/UserMessageRow.module.css +75 -0
  159. package/packages/client/chat/UserMessageRow.tsx +92 -0
  160. package/packages/client/chat/accessibility.module.css +9 -0
  161. package/packages/client/chat/attachment/ImageLightbox.module.css +45 -0
  162. package/packages/client/chat/attachment/ImageLightbox.tsx +70 -0
  163. package/packages/client/chat/attachment/MessageImage.module.css +70 -0
  164. package/packages/client/chat/attachment/MessageImage.tsx +136 -0
  165. package/packages/client/chat/attachment/image-attachment.ts +98 -0
  166. package/packages/client/chat/attachment/image-labels.ts +34 -0
  167. package/packages/client/chat/message-chrome.ts +96 -0
  168. package/packages/client/chat/reference/ReferenceIcon.tsx +40 -0
  169. package/packages/client/chat/turn-assistant.ts +15 -0
  170. package/packages/client/chat/turn-metrics.ts +102 -0
  171. package/packages/client/chat/use-calendar-day.ts +27 -0
  172. package/packages/client/chat/use-throttled-visual-update.ts +44 -0
  173. package/packages/client/chat-flow.spec.ts +357 -0
  174. package/packages/client/chat-flow.ts +278 -0
  175. package/packages/client/composer-selects.module.css +376 -0
  176. package/packages/client/composer-selects.tsx +531 -0
  177. package/packages/client/dag-editor-model.spec.ts +173 -0
  178. package/packages/client/dag-editor-model.ts +359 -0
  179. package/packages/client/describe-error.spec.ts +18 -0
  180. package/packages/client/describe-error.ts +12 -0
  181. package/packages/client/index.ts +144 -0
  182. package/packages/client/input.module.css +854 -0
  183. package/packages/client/is-conditional-check.spec.ts +34 -0
  184. package/packages/client/locales.ts +789 -0
  185. package/packages/client/model-faces.ts +225 -0
  186. package/packages/client/open-discussion.spec.ts +39 -0
  187. package/packages/client/open-discussion.ts +32 -0
  188. package/packages/client/panel-model.spec.ts +152 -0
  189. package/packages/client/panel-model.ts +72 -0
  190. package/packages/client/preview.module.css +95 -0
  191. package/packages/client/react-dom.d.ts +11 -0
  192. package/packages/client/reactflow-theme.css +91 -0
  193. package/packages/client/remote.spec.ts +41 -0
  194. package/packages/client/remote.ts +46 -0
  195. package/packages/client/run-view-model.spec.ts +641 -0
  196. package/packages/client/run-view-model.ts +636 -0
  197. package/packages/client/sandore-save.spec.ts +146 -0
  198. package/packages/client/sandore-save.ts +97 -0
  199. package/packages/client/sandore-sse.ts +59 -0
  200. package/packages/client/step-chat-retry.spec.ts +70 -0
  201. package/packages/client/step-chat-retry.ts +65 -0
  202. package/packages/client/timeline.module.css +503 -0
  203. package/packages/client/tool/GenericToolCard.tsx +99 -0
  204. package/packages/client/tool/ToolRow.module.css +308 -0
  205. package/packages/client/tool/ToolRow.tsx +328 -0
  206. package/packages/client/tool/diff-card-model.ts +103 -0
  207. package/packages/client/tool/read-card-model.ts +86 -0
  208. package/packages/client/tool/search-card-model.ts +166 -0
  209. package/packages/client/tool/terminal-card-model.ts +225 -0
  210. package/packages/client/tool/tool-call-model.ts +267 -0
  211. package/packages/client/tool/web-card-model.ts +80 -0
  212. package/packages/client/tree-nav.module.css +282 -0
  213. package/packages/client/use-focus-refresh.ts +23 -0
  214. package/packages/client/workflow-face.ts +72 -0
  215. package/packages/client/workflow-store.spec.ts +49 -0
  216. package/packages/client/workflow-store.ts +161 -0
  217. package/packages/client/workspace-path.ts +27 -0
  218. package/packages/css-modules.d.ts +6 -0
  219. package/packages/microflow/branch-evaluator.spec.ts +745 -0
  220. package/packages/microflow/branch-evaluator.ts +1037 -0
  221. package/packages/microflow/concurrency-limiter.ts +70 -0
  222. package/packages/microflow/errors.ts +64 -0
  223. package/packages/microflow/file-utils.spec.ts +604 -0
  224. package/packages/microflow/file-utils.ts +521 -0
  225. package/packages/microflow/finalize-manager.spec.ts +1977 -0
  226. package/packages/microflow/finalize-manager.ts +1464 -0
  227. package/packages/microflow/fork-session.spec.ts +184 -0
  228. package/packages/microflow/fork-session.ts +316 -0
  229. package/packages/microflow/index.ts +17 -0
  230. package/packages/microflow/internal.ts +166 -0
  231. package/packages/microflow/loop-manager.spec.ts +294 -0
  232. package/packages/microflow/loop-manager.ts +255 -0
  233. package/packages/microflow/rules-resolver.spec.ts +147 -0
  234. package/packages/microflow/rules-resolver.ts +58 -0
  235. package/packages/microflow/stage-executor.spec.ts +1577 -0
  236. package/packages/microflow/stage-executor.ts +1478 -0
  237. package/packages/microflow/step-navigator.spec.ts +301 -0
  238. package/packages/microflow/step-navigator.ts +300 -0
  239. package/packages/microflow/step-runner.spec.ts +1132 -0
  240. package/packages/microflow/step-runner.ts +752 -0
  241. package/packages/microflow/subagents.spec.ts +215 -0
  242. package/packages/microflow/subagents.ts +422 -0
  243. package/packages/tests/fixtures/test-workflow.yaml +128 -0
  244. package/packages/tests/mocks/agents.ts +206 -0
  245. package/packages/tests/mocks/llm.ts +46 -0
  246. package/packages/tests/mocks/session-title.ts +31 -0
  247. package/packages/tests/mocks/sessions.ts +131 -0
  248. package/packages/tests/mocks/shell.ts +60 -0
  249. package/packages/tests/mocks/storage-domain.ts +101 -0
  250. package/packages/tests/mocks/subagents.ts +197 -0
  251. package/packages/tests/mocks/subprocess.ts +59 -0
  252. package/packages/tests/mocks/user-questions.ts +33 -0
  253. package/packages/tests/mocks/workspace.ts +52 -0
  254. package/packages/tool/index.spec.ts +287 -0
  255. package/packages/tool/index.ts +348 -0
  256. package/packages/workflow/ask-tool.ts +173 -0
  257. package/packages/workflow/dag-navigator.spec.ts +236 -0
  258. package/packages/workflow/dag-navigator.ts +425 -0
  259. package/packages/workflow/domain.ts +26 -0
  260. package/packages/workflow/engine-surface.ts +71 -0
  261. package/packages/workflow/errors.ts +13 -0
  262. package/packages/workflow/host-agent.spec.ts +92 -0
  263. package/packages/workflow/host-agent.ts +62 -0
  264. package/packages/workflow/index.ts +25 -0
  265. package/packages/workflow/interaction-manager.spec.ts +390 -0
  266. package/packages/workflow/interaction-manager.ts +397 -0
  267. package/packages/workflow/lifecycle-manager.spec.ts +683 -0
  268. package/packages/workflow/lifecycle-manager.ts +288 -0
  269. package/packages/workflow/persistence.spec.ts +109 -0
  270. package/packages/workflow/persistence.ts +64 -0
  271. package/packages/workflow/plugin.ts +39 -0
  272. package/packages/workflow/reactive-state.spec.ts +69 -0
  273. package/packages/workflow/reactive-state.ts +52 -0
  274. package/packages/workflow/recovery-manager.spec.ts +606 -0
  275. package/packages/workflow/recovery-manager.ts +471 -0
  276. package/packages/workflow/remote.spec.ts +1045 -0
  277. package/packages/workflow/remote.ts +479 -0
  278. package/packages/workflow/schema.spec.ts +303 -0
  279. package/packages/workflow/schema.ts +562 -0
  280. package/packages/workflow/seam.ts +293 -0
  281. package/packages/workflow/shell.ts +142 -0
  282. package/packages/workflow/sse.ts +110 -0
  283. package/packages/workflow/typert/typert.host.d.ts +16 -0
  284. package/packages/workflow/typert/typert.host.js +25 -0
  285. package/packages/workflow/typert/typert.remote-client.d.ts +114 -0
  286. package/packages/workflow/typert/typert.remote-client.js +399 -0
  287. package/packages/workflow/types.ts +258 -0
  288. package/packages/workflow/wire.ts +267 -0
  289. package/packages/workflow/workflow-engine-e2e.spec.ts +262 -0
  290. package/packages/workflow/workflow-engine-real-e2e.spec.ts +922 -0
  291. package/packages/workflow/workflow-engine.spec.ts +4389 -0
  292. package/packages/workflow/workflow-engine.ts +2133 -0
  293. package/packages/workflow/yaml-error.ts +29 -0
  294. package/packages/workflow/yaml-parser.spec.ts +757 -0
  295. package/packages/workflow/yaml-parser.ts +287 -0
@@ -0,0 +1,1132 @@
1
+ /**
2
+ * step-runner.ts(StepRunner)的单元测试:shell 成功/非零退出/超时/中止/stdin
3
+ * 注入,prompt 经可续接子代理成功/终端失败/provider-model 三层回退/透传变量替换,
4
+ * agent/skill/mcp 可续接子代理/fillParams,skill/mcp 名称校验与前缀。双通道 ask
5
+ * 重构(4.4)后不再有 `[[SANDORE_ASK]]` 标记块转接/引导——子代理提问走
6
+ * child-scope ask_user_question 工具(workflow/ask-tool.ts),本 spec 覆盖标记块
7
+ * 按普通输出透传 + 无 toolFilter deny + 无引导注入。
8
+ * @module dsh-sandore/microflow/src/step-runner.spec
9
+ */
10
+ import { Context } from '@deepseek-ai/cordis';
11
+ import type { ContentBlock, LlmRuntime } from '@deepseek-ai/dsh-llm';
12
+ import { describe, expect, it } from 'vitest';
13
+
14
+ import type { WorkflowStep } from '../workflow/schema';
15
+ import { assistantMessageEvent, FakeAgent, FakeAgentRegistry } from '../tests/mocks/agents.ts';
16
+ import { textChunks } from '../tests/mocks/llm.ts';
17
+ import { FakeLlm } from '../tests/mocks/llm.ts';
18
+ import { FakeShell } from '../tests/mocks/shell.ts';
19
+ import { FakeSubagents } from '../tests/mocks/subagents.ts';
20
+ import { FakeSubprocess } from '../tests/mocks/subprocess.ts';
21
+ import { StepRunner } from './step-runner.ts';
22
+ import type { StepRunRequest, StepRunnerConfig } from './step-runner.ts';
23
+ import type { SubagentSettlement, SubagentStopReason } from './subagents.ts';
24
+
25
+ /** 提取文本 ContentBlock 的纯文本(非文本块返回空串)。 */
26
+ function textOf(block: ContentBlock | undefined): string {
27
+ return block !== undefined && block.type === 'text' ? block.text : '';
28
+ }
29
+
30
+ /** 组装 shell Step 定义(可选 Step 级 timeout,单位分钟)。 */
31
+ function shellStep(command: string, timeout?: number): WorkflowStep {
32
+ return {
33
+ id: 'step-sh',
34
+ command: { type: 'shell', value: { command } },
35
+ ...(timeout === undefined ? {} : { timeout }),
36
+ };
37
+ }
38
+
39
+ /** 组装带 inputFrom 的 shell Step 定义。 */
40
+ function shellStepWithInputFrom(command: string): WorkflowStep {
41
+ return {
42
+ id: 'step-sh',
43
+ command: { type: 'shell', value: { command } },
44
+ inputFrom: [{ id: 'in-1', path: 'step-p' }],
45
+ };
46
+ }
47
+
48
+ /** 组装 prompt Step 定义(可选 providerId/modelId/promptText)。 */
49
+ function promptStep(options?: { providerId?: string; modelId?: string; promptText?: string }): WorkflowStep {
50
+ return {
51
+ id: 'step-p',
52
+ command: {
53
+ type: 'prompt',
54
+ value: {
55
+ promptText: options?.promptText ?? 'summarize the input',
56
+ ...(options?.providerId === undefined ? {} : { providerId: options.providerId }),
57
+ ...(options?.modelId === undefined ? {} : { modelId: options.modelId }),
58
+ },
59
+ },
60
+ };
61
+ }
62
+
63
+ /** 组装 agent Step 定义(可选 providerId/modelId/agentParamsTemplate)。 */
64
+ function agentStep(options?: { providerId?: string; modelId?: string; agentParamsTemplate?: string }): WorkflowStep {
65
+ return {
66
+ id: 'step-a',
67
+ command: {
68
+ type: 'agent',
69
+ value: {
70
+ promptText: 'code-reviewer',
71
+ ...(options?.agentParamsTemplate === undefined ? {} : { agentParamsTemplate: options.agentParamsTemplate }),
72
+ ...(options?.providerId === undefined ? {} : { providerId: options.providerId }),
73
+ ...(options?.modelId === undefined ? {} : { modelId: options.modelId }),
74
+ },
75
+ },
76
+ };
77
+ }
78
+
79
+ /** 组装 skill Step 定义。 */
80
+ function skillStep(promptText: string): WorkflowStep {
81
+ return {
82
+ id: 'step-sk',
83
+ command: { type: 'skill', value: { promptText } },
84
+ };
85
+ }
86
+
87
+ /** 组装 mcp Step 定义。 */
88
+ function mcpStep(promptText: string): WorkflowStep {
89
+ return {
90
+ id: 'step-m',
91
+ command: { type: 'mcp', value: { promptText } },
92
+ };
93
+ }
94
+
95
+ /** 组装默认 Step 执行请求。 */
96
+ function request(step: WorkflowStep, overrides?: Partial<StepRunRequest>): StepRunRequest {
97
+ return {
98
+ step,
99
+ executedId: 'exec-1',
100
+ workdir: '/tmp',
101
+ originalInput: 'do the thing',
102
+ previousOutputs: [],
103
+ stageId: 'stg-1',
104
+ ...overrides,
105
+ };
106
+ }
107
+
108
+ /** 组装 StepRunner 测试环境(注入 mock llm/shell/subagents/agents/subprocess)。 */
109
+ function harness(config: StepRunnerConfig = { defaultTimeoutMs: 60000 }): {
110
+ ctx: Context;
111
+ llm: FakeLlm;
112
+ shell: FakeShell;
113
+ subagents: FakeSubagents;
114
+ agents: FakeAgentRegistry;
115
+ subprocess: FakeSubprocess;
116
+ runner: StepRunner;
117
+ } {
118
+ const ctx = new Context();
119
+ const llm = new FakeLlm();
120
+ const shell = new FakeShell();
121
+ const subagents = new FakeSubagents();
122
+ const agents = new FakeAgentRegistry();
123
+ const subprocess = new FakeSubprocess();
124
+ ctx.provide('llm', llm as unknown as LlmRuntime);
125
+ ctx.provide('shell', shell);
126
+ ctx.provide('subagents', subagents);
127
+ ctx.provide('agents', agents);
128
+ ctx.provide('subprocess', subprocess);
129
+ return { ctx, llm, shell, subagents, agents, subprocess, runner: new StepRunner(ctx, config) };
130
+ }
131
+
132
+ describe('StepRunner shell', () => {
133
+ it('wraps stdout.text as a text ContentBlock on exitCode 0', async () => {
134
+ const { shell, runner } = harness();
135
+ shell.runHandler = async spec => ({
136
+ exitCode: 0,
137
+ signal: null,
138
+ timedOut: false,
139
+ aborted: false,
140
+ timeoutMs: spec.timeoutMs,
141
+ stdout: { text: 'hello', truncated: false },
142
+ stderr: { text: '', truncated: false },
143
+ });
144
+
145
+ await expect(runner.run(request(shellStep('echo hello')))).resolves.toEqual({
146
+ output: [{ type: 'text', text: 'hello' }],
147
+ });
148
+ });
149
+
150
+ it('throws STEP_EXECUTION with stderr summary on nonzero exit', async () => {
151
+ const { shell, runner } = harness();
152
+ shell.runHandler = async spec => ({
153
+ exitCode: 1,
154
+ signal: null,
155
+ timedOut: false,
156
+ aborted: false,
157
+ timeoutMs: spec.timeoutMs,
158
+ stdout: { text: '', truncated: false },
159
+ stderr: { text: 'boom', truncated: false },
160
+ });
161
+
162
+ await expect(runner.run(request(shellStep('false')))).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
163
+ await expect(runner.run(request(shellStep('false')))).rejects.toThrow(/boom/);
164
+ });
165
+
166
+ it('maps timedOut to STEP_TIMEOUT', async () => {
167
+ const { shell, runner } = harness({ defaultTimeoutMs: 1 });
168
+ shell.runHandler = async spec => ({
169
+ exitCode: null,
170
+ signal: 'SIGTERM',
171
+ timedOut: true,
172
+ aborted: false,
173
+ timeoutMs: spec.timeoutMs,
174
+ stdout: { text: '', truncated: false },
175
+ stderr: { text: '', truncated: false },
176
+ });
177
+
178
+ await expect(runner.run(request(shellStep('sleep 10')))).rejects.toMatchObject({ code: 'STEP_TIMEOUT' });
179
+ });
180
+
181
+ it('derives timeoutMs from the Step timeout in minutes when present', async () => {
182
+ const { shell, runner } = harness({ defaultTimeoutMs: 60000 });
183
+ shell.runHandler = async spec => ({
184
+ exitCode: null,
185
+ signal: 'SIGTERM',
186
+ timedOut: true,
187
+ aborted: false,
188
+ timeoutMs: spec.timeoutMs,
189
+ stdout: { text: '', truncated: false },
190
+ stderr: { text: '', truncated: false },
191
+ });
192
+
193
+ // Step 定义 timeout: 0.001 分钟 → 60ms,优先于引擎 defaultTimeoutMs。
194
+ await expect(runner.run(request(shellStep('sleep 10', 0.001)))).rejects.toMatchObject({ code: 'STEP_TIMEOUT' });
195
+ expect(shell.specs[0]?.timeoutMs).toBe(60);
196
+ });
197
+
198
+ it('maps aborted to STEP_EXECUTION', async () => {
199
+ const { shell, runner } = harness();
200
+ shell.runHandler = async spec => ({
201
+ exitCode: null,
202
+ signal: 'SIGTERM',
203
+ timedOut: false,
204
+ aborted: true,
205
+ timeoutMs: spec.timeoutMs,
206
+ stdout: { text: '', truncated: false },
207
+ stderr: { text: '', truncated: false },
208
+ });
209
+
210
+ await expect(runner.run(request(shellStep('sleep 10')))).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
211
+ });
212
+
213
+ it('passes JSON stdin through shell.run when inputFrom resolves to content', async () => {
214
+ const { subprocess, shell, runner } = harness();
215
+ const previousOutputs = [{ name: 'prev', id: 'step-p', returnPrompt: 'hello', files: [] }];
216
+ shell.runHandler = async spec => ({
217
+ exitCode: 0,
218
+ signal: null,
219
+ timedOut: false,
220
+ aborted: false,
221
+ timeoutMs: spec.timeoutMs,
222
+ stdout: { text: 'ok', truncated: false },
223
+ stderr: { text: '', truncated: false },
224
+ });
225
+
226
+ await runner.run(request(shellStepWithInputFrom('node read-stdin.js'), { previousOutputs }));
227
+
228
+ expect(subprocess.specs).toHaveLength(0);
229
+ expect(shell.specs).toHaveLength(1);
230
+ expect(shell.specs[0]?.stdin).toEqual(JSON.stringify(previousOutputs));
231
+ expect(shell.specs[0]?.command).toBe('node read-stdin.js');
232
+ });
233
+
234
+ it('still uses shell.run for a simple shell step without stdin/env injection', async () => {
235
+ const { subprocess, shell, runner } = harness();
236
+ shell.runHandler = async spec => ({
237
+ exitCode: 0,
238
+ signal: null,
239
+ timedOut: false,
240
+ aborted: false,
241
+ timeoutMs: spec.timeoutMs,
242
+ stdout: { text: 'hello', truncated: false },
243
+ stderr: { text: '', truncated: false },
244
+ });
245
+
246
+ await expect(runner.run(request(shellStep('echo hello')))).resolves.toEqual({
247
+ output: [{ type: 'text', text: 'hello' }],
248
+ });
249
+ expect(shell.specs).toHaveLength(1);
250
+ expect(subprocess.specs).toHaveLength(0);
251
+ });
252
+
253
+ it('passes merged env through shell.run when non-empty passThroughVars are present', async () => {
254
+ const { subprocess, shell, runner } = harness();
255
+ shell.runHandler = async spec => ({
256
+ exitCode: 0,
257
+ signal: null,
258
+ timedOut: false,
259
+ aborted: false,
260
+ timeoutMs: spec.timeoutMs,
261
+ stdout: { text: 'ok', truncated: false },
262
+ stderr: { text: '', truncated: false },
263
+ });
264
+
265
+ await runner.run(
266
+ request(shellStep('echo $projectName'), { passThroughVars: { projectName: 'sandore', empty: '' } }),
267
+ );
268
+
269
+ expect(subprocess.specs).toHaveLength(0);
270
+ expect(shell.specs).toHaveLength(1);
271
+ expect(shell.specs[0]?.env).toEqual({ projectName: 'sandore' });
272
+ });
273
+
274
+ it('injects in-loop-turn and all-loop-turn into shell env when loopTurn > 0', async () => {
275
+ const { subprocess, shell, runner } = harness();
276
+ shell.runHandler = async spec => ({
277
+ exitCode: 0,
278
+ signal: null,
279
+ timedOut: false,
280
+ aborted: false,
281
+ timeoutMs: spec.timeoutMs,
282
+ stdout: { text: 'ok', truncated: false },
283
+ stderr: { text: '', truncated: false },
284
+ });
285
+
286
+ await runner.run(
287
+ request(shellStep('echo hi'), {
288
+ passThroughVars: { projectName: 'sandore' },
289
+ loopTurn: 3,
290
+ loopMaxIterations: 5,
291
+ }),
292
+ );
293
+
294
+ expect(shell.specs[0]?.env).toEqual({
295
+ projectName: 'sandore',
296
+ 'in-loop-turn': '3',
297
+ 'all-loop-turn': '5',
298
+ });
299
+ });
300
+
301
+ it('does not inject empty passThroughVars and stays on the simple path when nothing non-empty remains', async () => {
302
+ const { subprocess, shell, runner } = harness();
303
+ shell.runHandler = async spec => ({
304
+ exitCode: 0,
305
+ signal: null,
306
+ timedOut: false,
307
+ aborted: false,
308
+ timeoutMs: spec.timeoutMs,
309
+ stdout: { text: '', truncated: false },
310
+ stderr: { text: '', truncated: false },
311
+ });
312
+
313
+ await runner.run(request(shellStep('echo hi'), { passThroughVars: { empty: '' } }));
314
+
315
+ expect(subprocess.specs).toHaveLength(0);
316
+ expect(shell.specs).toHaveLength(1);
317
+ });
318
+
319
+ it('maps shell timedOut to STEP_TIMEOUT', async () => {
320
+ const { subprocess, shell, runner } = harness();
321
+ shell.runHandler = async () => ({
322
+ exitCode: null,
323
+ signal: 'SIGTERM',
324
+ timedOut: true,
325
+ aborted: false,
326
+ timeoutMs: 60000,
327
+ stdout: { text: '', truncated: false },
328
+ stderr: { text: '', truncated: false },
329
+ });
330
+
331
+ await expect(
332
+ runner.run(request(shellStep('sleep 10'), { passThroughVars: { a: '1' } })),
333
+ ).rejects.toMatchObject({ code: 'STEP_TIMEOUT' });
334
+ });
335
+
336
+ it('replaces ${projectName} in the shell command with its passThroughVar value', async () => {
337
+ const { subprocess, shell, runner } = harness();
338
+ shell.runHandler = async spec => ({
339
+ exitCode: 0,
340
+ signal: null,
341
+ timedOut: false,
342
+ aborted: false,
343
+ timeoutMs: spec.timeoutMs,
344
+ stdout: { text: 'ok', truncated: false },
345
+ stderr: { text: '', truncated: false },
346
+ });
347
+
348
+ await runner.run(request(shellStep('echo ${projectName}'), { passThroughVars: { projectName: 'sandore' } }));
349
+
350
+ expect(subprocess.specs).toHaveLength(0);
351
+ expect(shell.specs[0]?.command).toBe('echo sandore');
352
+ });
353
+
354
+ it('keeps quoted arguments intact in the shell command string', async () => {
355
+ const { subprocess, shell, runner } = harness();
356
+ shell.runHandler = async spec => ({
357
+ exitCode: 0,
358
+ signal: null,
359
+ timedOut: false,
360
+ aborted: false,
361
+ timeoutMs: spec.timeoutMs,
362
+ stdout: { text: 'ok', truncated: false },
363
+ stderr: { text: '', truncated: false },
364
+ });
365
+
366
+ await runner.run(request(shellStep('echo "hello world"'), { passThroughVars: { a: '1' } }));
367
+
368
+ expect(subprocess.specs).toHaveLength(0);
369
+ expect(shell.specs[0]?.command).toBe('echo "hello world"');
370
+ });
371
+ });
372
+
373
+ describe('StepRunner prompt', () => {
374
+ it('runs via continuable subagent and returns sessionMeta with real childId', async () => {
375
+ const { subagents, agents, runner } = harness({
376
+ defaultTimeoutMs: 60000,
377
+ defaultProvider: 'p',
378
+ defaultModel: 'm',
379
+ });
380
+ const child = new FakeAgent('child-1');
381
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'summary' }]));
382
+ agents.register(child);
383
+
384
+ const result = await runner.run(request(promptStep()));
385
+
386
+ // 决策 D3:prompt 经 startContinuable 拉起可续接子代理(非临时 llm.stream)。
387
+ expect(subagents.continuableStarts).toHaveLength(1);
388
+ expect(textOf(subagents.continuableStarts[0]?.request.prompt[0])).toContain('summarize the input');
389
+ expect(result.output).toEqual([{ type: 'text', text: 'summary' }]);
390
+ expect(result.sessionMeta).toEqual({ childId: 'child-1', synthetic: false, command: 'prompt' });
391
+ });
392
+
393
+ it('keeps only the latest 10 session message lines in sessionMessages (截断持久化体积)', async () => {
394
+ const { agents, runner } = harness({
395
+ defaultTimeoutMs: 60000,
396
+ defaultProvider: 'p',
397
+ defaultModel: 'm',
398
+ });
399
+ const child = new FakeAgent('child-1');
400
+ // 15 条 assistant/message 事件 → 15 条消息行;采集后应只保留最新 10 条
401
+ // (StepChatPanel 实时订阅不读 sessionMessages,合成会话兜底也只展示最近 N 条)。
402
+ for (let index = 0; index < 15; index += 1) {
403
+ child.session.append(assistantMessageEvent([{ type: 'text', text: `msg-${index}` }], index));
404
+ }
405
+ agents.register(child);
406
+
407
+ const result = await runner.run(request(promptStep()));
408
+
409
+ expect(result.sessionMessages).toHaveLength(10);
410
+ expect(result.sessionMessages?.map(message => message.text)).toEqual([
411
+ 'msg-5', 'msg-6', 'msg-7', 'msg-8', 'msg-9',
412
+ 'msg-10', 'msg-11', 'msg-12', 'msg-13', 'msg-14',
413
+ ]);
414
+ });
415
+
416
+ it('throws STEP_EXECUTION when the subagent settle errors (model failure → failed)', async () => {
417
+ const child = new FakeAgent('child-1');
418
+ const ctrl = settleController();
419
+ const { agents, runner } = settleHarness(ctrl);
420
+ agents.register(child);
421
+
422
+ const runP = runner.run(request(promptStep()));
423
+ ctrl.settle('child-1', 'error');
424
+ await expect(runP).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
425
+ });
426
+
427
+ it('throws STEP_EXECUTION with no LLM provider available when directory is empty', async () => {
428
+ const { runner } = harness();
429
+ await expect(runner.run(request(promptStep()))).rejects.toThrow(/no LLM provider available/);
430
+ });
431
+
432
+ it('prefers the step definition providerId over config and directory', async () => {
433
+ const { subagents, agents, runner } = harness({
434
+ defaultTimeoutMs: 60000,
435
+ defaultProvider: 'b',
436
+ defaultModel: 'b-model',
437
+ });
438
+ const child = new FakeAgent('child-1');
439
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
440
+ agents.register(child);
441
+
442
+ await runner.run(request(promptStep({ providerId: 'a', modelId: 'a-model' })));
443
+
444
+ // provider 参数是 DSH subagent provider 注册名(缺省 'fork'),与模型路由解耦;
445
+ // 模型 provider 经 agentOptions 承载。
446
+ expect(subagents.continuableStarts[0]?.provider).toBe('fork');
447
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.provider).toBe('a');
448
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.model).toBe('a-model');
449
+ });
450
+
451
+ it('falls back to config provider/model when the step omits them', async () => {
452
+ const { subagents, agents, runner } = harness({
453
+ defaultTimeoutMs: 60000,
454
+ defaultProvider: 'b',
455
+ defaultModel: 'b-model',
456
+ });
457
+ const child = new FakeAgent('child-1');
458
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
459
+ agents.register(child);
460
+
461
+ await runner.run(request(promptStep()));
462
+
463
+ expect(subagents.continuableStarts[0]?.provider).toBe('fork');
464
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.provider).toBe('b');
465
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.model).toBe('b-model');
466
+ });
467
+
468
+ it('keeps the DSH subagent provider decoupled from the model provider in agentOptions', async () => {
469
+ const { subagents, agents, runner } = harness({
470
+ defaultTimeoutMs: 60000,
471
+ defaultProvider: 'p',
472
+ defaultModel: 'm',
473
+ });
474
+ const child = new FakeAgent('child-1');
475
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
476
+ agents.register(child);
477
+
478
+ await runner.run(request(promptStep({ providerId: 'p', modelId: 'm' })));
479
+
480
+ // 回归防护:startContinuable 的 provider 参数必须是 DSH subagent provider 注册名
481
+ // (缺省 'fork'),绝不能是模型 provider id('p')——否则真实 DSH 运行时报
482
+ // `no subagent provider registered for "p"`。模型路由只经 agentOptions 承载。
483
+ expect(subagents.continuableStarts[0]?.provider).toBe('fork');
484
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.provider).toBe('p');
485
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.model).toBe('m');
486
+ });
487
+
488
+ it('honors a config subagentProvider override while keeping model routing in agentOptions', async () => {
489
+ const { subagents, agents, runner } = harness({
490
+ defaultTimeoutMs: 60000,
491
+ defaultProvider: 'p',
492
+ defaultModel: 'm',
493
+ subagentProvider: 'spawn',
494
+ });
495
+ const child = new FakeAgent('child-1');
496
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
497
+ agents.register(child);
498
+
499
+ await runner.run(request(promptStep({ providerId: 'p', modelId: 'm' })));
500
+
501
+ expect(subagents.continuableStarts[0]?.provider).toBe('spawn');
502
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.provider).toBe('p');
503
+ expect(subagents.continuableStarts[0]?.request.agentOptions?.model).toBe('m');
504
+ });
505
+
506
+ it('replaces ${projectName} with passThroughVar and leaves unknown vars intact', async () => {
507
+ const { subagents, agents, runner } = harness({
508
+ defaultTimeoutMs: 60000,
509
+ defaultProvider: 'p',
510
+ defaultModel: 'm',
511
+ });
512
+ const child = new FakeAgent('child-1');
513
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
514
+ agents.register(child);
515
+
516
+ await runner.run(
517
+ request(promptStep({ promptText: 'build ${projectName} for ${unknown}' }), {
518
+ passThroughVars: { projectName: 'sandore' },
519
+ }),
520
+ );
521
+
522
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
523
+ expect(promptText).toContain('sandore');
524
+ expect(promptText).not.toContain('${projectName}');
525
+ expect(promptText).toContain('${unknown}');
526
+ });
527
+ });
528
+
529
+ describe('StepRunner agent', () => {
530
+ it('runs via continuable subagent and returns sessionMeta with real childId', async () => {
531
+ const { subagents, agents, runner } = harness();
532
+ const child = new FakeAgent('child-1');
533
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'agent output' }]));
534
+ agents.register(child);
535
+
536
+ const result = await runner.run(request(agentStep({ providerId: 'p', modelId: 'm' })));
537
+
538
+ expect(subagents.continuableStarts).toHaveLength(1);
539
+ expect(textOf(subagents.continuableStarts[0]?.request.prompt[0])).toContain('code-reviewer');
540
+ expect(result.output).toEqual([{ type: 'text', text: 'agent output' }]);
541
+ expect(result.sessionMeta).toEqual({ childId: 'child-1', synthetic: false, command: 'agent' });
542
+ });
543
+
544
+ it('prefixes agent prompt with 使用 Agent: when name is in agents', async () => {
545
+ const { subagents, agents, runner } = harness({
546
+ defaultTimeoutMs: 60000,
547
+ defaultProvider: 'p',
548
+ defaultModel: 'm',
549
+ agents: ['code-reviewer'],
550
+ });
551
+ const child = new FakeAgent('child-1');
552
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
553
+ agents.register(child);
554
+
555
+ await runner.run(request(agentStep()));
556
+
557
+ expect(textOf(subagents.continuableStarts[0]?.request.prompt[0])).toContain('使用 Agent:code-reviewer');
558
+ });
559
+
560
+ it('injects 当前Step参数 sub-section when agentParamsTemplate is defined', async () => {
561
+ const { subagents, agents, runner } = harness({
562
+ defaultTimeoutMs: 60000,
563
+ defaultProvider: 'p',
564
+ defaultModel: 'm',
565
+ });
566
+ // v6:fillParams 也走可续接子代理——child-1 是 fillParams 子代理(输出填充结果),
567
+ // child-2 是主 step 子代理(输出 'done')。startContinuable 顺序:fillParams → 主 step。
568
+ const fillChild = new FakeAgent('child-1');
569
+ fillChild.session.append(assistantMessageEvent([{ type: 'text', text: '{"lintErrorCount": 3}' }]));
570
+ agents.register(fillChild);
571
+ const child = new FakeAgent('child-2');
572
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
573
+ agents.register(child);
574
+
575
+ await runner.run(request(agentStep({ agentParamsTemplate: '审查:${lintErrorCount} 个错误' })));
576
+
577
+ // fillParams 子代理收到填充提示词(含参数模板原文)。
578
+ expect(textOf(subagents.continuableStarts[0]?.request.prompt[0])).toContain('审查:${lintErrorCount} 个错误');
579
+ // 主 step 子代理提示词:<Params> 内为 fillParams 输出。
580
+ const promptText = textOf(subagents.continuableStarts[1]?.request.prompt[0]);
581
+ expect(promptText).toContain('<Params>');
582
+ expect(promptText).toContain('{"lintErrorCount": 3}');
583
+ });
584
+
585
+ it('falls back to agentParamsTemplate raw text when fillParams subagent produces no output', async () => {
586
+ const { subagents, agents, runner } = harness({
587
+ defaultTimeoutMs: 60000,
588
+ defaultProvider: 'p',
589
+ defaultModel: 'm',
590
+ });
591
+ // fillParams 子代理(child-1)无 assistant 输出 → 回退模板原文。
592
+ const fillChild = new FakeAgent('child-1');
593
+ agents.register(fillChild);
594
+ const child = new FakeAgent('child-2');
595
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
596
+ agents.register(child);
597
+
598
+ await expect(runner.run(request(agentStep({ agentParamsTemplate: 'TEMPLATE_RAW' })))).resolves.toBeDefined();
599
+
600
+ const promptText = textOf(subagents.continuableStarts[1]?.request.prompt[0]);
601
+ expect(promptText).toContain('<Params>');
602
+ expect(promptText).toContain('TEMPLATE_RAW');
603
+ });
604
+
605
+ it('falls back to agentParamsTemplate raw text when fillParams subagent settle is not completed', async () => {
606
+ const ctrl = settleController();
607
+ const { subagents, agents, runner } = harness({
608
+ defaultTimeoutMs: 60000,
609
+ defaultProvider: 'p',
610
+ defaultModel: 'm',
611
+ awaitSubagentSettlement: ctrl.awaitSubagentSettlement,
612
+ });
613
+ // fillParams 子代理(child-1)settle aborted → 回退模板原文。
614
+ const fillChild = new FakeAgent('child-1');
615
+ agents.register(fillChild);
616
+ const child = new FakeAgent('child-2');
617
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
618
+ agents.register(child);
619
+
620
+ const runP = runner.run(request(agentStep({ agentParamsTemplate: 'TEMPLATE_RAW' })));
621
+ ctrl.settle('child-1', 'aborted');
622
+ ctrl.settle('child-2', 'completed');
623
+ await expect(runP).resolves.toBeDefined();
624
+
625
+ const promptText = textOf(subagents.continuableStarts[1]?.request.prompt[0]);
626
+ expect(promptText).toContain('<Params>');
627
+ expect(promptText).toContain('TEMPLATE_RAW');
628
+ });
629
+
630
+ it('falls back to agentParamsTemplate raw text when fillParams subagent start throws', async () => {
631
+ const { subagents, agents, runner } = harness({
632
+ defaultTimeoutMs: 60000,
633
+ defaultProvider: 'p',
634
+ defaultModel: 'm',
635
+ });
636
+ // fillParams 子代理启动抛错(mock startContinuable 首次调用抛)→ 回退模板原文。
637
+ const originalStart = subagents.startContinuable.bind(subagents);
638
+ let first = true;
639
+ subagents.startContinuable = (spec => {
640
+ if (first) {
641
+ first = false;
642
+ throw new Error('start exploded');
643
+ }
644
+ return originalStart(spec);
645
+ }) as typeof subagents.startContinuable;
646
+ const child = new FakeAgent('child-1');
647
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
648
+ agents.register(child);
649
+
650
+ await expect(runner.run(request(agentStep({ agentParamsTemplate: 'TEMPLATE_RAW' })))).resolves.toBeDefined();
651
+
652
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
653
+ expect(promptText).toContain('<Params>');
654
+ expect(promptText).toContain('TEMPLATE_RAW');
655
+ });
656
+ });
657
+
658
+ describe('StepRunner 双通道 ask 重构(4.4:[[SANDORE_ASK]] 机制已删除)', () => {
659
+ it('treats a [[SANDORE_ASK]] marker block as ordinary output (no delegation hook)', async () => {
660
+ const child = new FakeAgent('child-1');
661
+ child.session.append(assistantMessageEvent([
662
+ { type: 'text', text: '[[SANDORE_ASK]]{"questions":[{"id":"q1","question":"Q?"}]}[[/SANDORE_ASK]]' },
663
+ ]));
664
+ const { agents, runner } = harness({
665
+ defaultTimeoutMs: 60000,
666
+ defaultProvider: 'p',
667
+ defaultModel: 'm',
668
+ });
669
+ agents.register(child);
670
+
671
+ const result = await runner.run(request(agentStep()));
672
+
673
+ // 标记块机制已删:输出原样透传(子代理提问走 child-scope ask_user_question 工具)。
674
+ expect(result.output).toEqual([
675
+ { type: 'text', text: '[[SANDORE_ASK]]{"questions":[{"id":"q1","question":"Q?"}]}[[/SANDORE_ASK]]' },
676
+ ]);
677
+ expect(result.sessionMeta).toEqual({ childId: 'child-1', synthetic: false, command: 'agent' });
678
+ });
679
+
680
+ it('does not inject the [[SANDORE_ASK]] guidance section into the subagent prompt', async () => {
681
+ const { subagents, agents, runner } = harness({
682
+ defaultTimeoutMs: 60000,
683
+ defaultProvider: 'p',
684
+ defaultModel: 'm',
685
+ });
686
+ const child = new FakeAgent('child-1');
687
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
688
+ agents.register(child);
689
+
690
+ await runner.run(request(promptStep()));
691
+
692
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
693
+ // 引导节已删:不再出现「向用户提问」/ [[SANDORE_ASK]] 标记说明。
694
+ expect(promptText).not.toContain('## 向用户提问');
695
+ expect(promptText).not.toContain('[[SANDORE_ASK]]');
696
+ });
697
+
698
+ it('does not deny ask_user_question via toolFilter (child-scope 同名工具遮蔽全局版)', async () => {
699
+ const { subagents, agents, runner } = harness({
700
+ defaultTimeoutMs: 60000,
701
+ defaultProvider: 'p',
702
+ defaultModel: 'm',
703
+ });
704
+ const child = new FakeAgent('child-1');
705
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
706
+ agents.register(child);
707
+
708
+ await runner.run(request(promptStep()));
709
+
710
+ // toolFilter deny 已删:子代理提问能力由 workflow 层 registerContinuableSetup 注入的
711
+ // child-scope ask_user_question 工具提供(child-scope 同名注册天然遮蔽全局版)。
712
+ expect(subagents.continuableStarts[0]?.request.toolFilter).toBeUndefined();
713
+ });
714
+
715
+ it('applies the no-toolFilter/no-guidance behavior to agent subagents as well (shared runSubagent path)', async () => {
716
+ const { subagents, agents, runner } = harness({
717
+ defaultTimeoutMs: 60000,
718
+ defaultProvider: 'p',
719
+ defaultModel: 'm',
720
+ });
721
+ const child = new FakeAgent('child-1');
722
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'agent output' }]));
723
+ agents.register(child);
724
+
725
+ await runner.run(request(agentStep({ providerId: 'p', modelId: 'm' })));
726
+
727
+ // runSubagent 为 prompt/agent/skill/mcp 四路共用:agent 路同样无 deny、无引导。
728
+ expect(subagents.continuableStarts[0]?.request.toolFilter).toBeUndefined();
729
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
730
+ expect(promptText).not.toContain('[[SANDORE_ASK]]');
731
+ });
732
+ });
733
+
734
+ describe('StepRunner skill/mcp', () => {
735
+ it('prefixes skill prompt with 使用 skill: when name is in skills', async () => {
736
+ const { subagents, agents, runner } = harness({
737
+ defaultTimeoutMs: 60000,
738
+ defaultProvider: 'p',
739
+ defaultModel: 'm',
740
+ skills: ['code-review-skill'],
741
+ });
742
+ const child = new FakeAgent('child-1');
743
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
744
+ agents.register(child);
745
+
746
+ await runner.run(request(skillStep('code-review-skill')));
747
+
748
+ expect(textOf(subagents.continuableStarts[0]?.request.prompt[0])).toContain('使用 skill:code-review-skill');
749
+ });
750
+
751
+ it('falls back to plain prompt without error when skill name is not in skills', async () => {
752
+ const { subagents, agents, runner } = harness({
753
+ defaultTimeoutMs: 60000,
754
+ defaultProvider: 'p',
755
+ defaultModel: 'm',
756
+ skills: ['other-skill'],
757
+ });
758
+ const child = new FakeAgent('child-1');
759
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
760
+ agents.register(child);
761
+
762
+ await expect(runner.run(request(skillStep('unknown-skill')))).resolves.toBeDefined();
763
+
764
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
765
+ expect(promptText).toContain('unknown-skill');
766
+ expect(promptText).not.toContain('使用 skill:');
767
+ });
768
+
769
+ it('prefixes mcp prompt with 使用 MCP: when name is in mcps', async () => {
770
+ const { subagents, agents, runner } = harness({
771
+ defaultTimeoutMs: 60000,
772
+ defaultProvider: 'p',
773
+ defaultModel: 'm',
774
+ mcps: ['filesystem-mcp'],
775
+ });
776
+ const child = new FakeAgent('child-1');
777
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
778
+ agents.register(child);
779
+
780
+ await runner.run(request(mcpStep('filesystem-mcp')));
781
+
782
+ expect(textOf(subagents.continuableStarts[0]?.request.prompt[0])).toContain('使用 MCP:filesystem-mcp');
783
+ });
784
+ });
785
+
786
+ describe('StepRunner ## 任务规则 注入(缺陷 7)', () => {
787
+ /** 组装带 resolvedRules 的请求(模拟 StageExecutor 解析后的 3 级合并结果)。 */
788
+ function requestWithRules(step: WorkflowStep, rules: Record<string, string>): StepRunRequest {
789
+ return {
790
+ step,
791
+ executedId: 'exec-1',
792
+ workdir: '/tmp',
793
+ originalInput: 'do the thing',
794
+ previousOutputs: [],
795
+ stageId: 'stg-1',
796
+ resolvedRules: rules,
797
+ };
798
+ }
799
+
800
+ it('prompt 路注入 ## 任务规则 节(非空 resolvedRules)', async () => {
801
+ const { subagents, agents, runner } = harness({
802
+ defaultTimeoutMs: 60000,
803
+ defaultProvider: 'p',
804
+ defaultModel: 'm',
805
+ });
806
+ const child = new FakeAgent('child-1');
807
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
808
+ agents.register(child);
809
+
810
+ await runner.run(requestWithRules(promptStep(), {
811
+ 'chinese-output': '所有输出必须使用中文',
812
+ 'no-direct-commit': '禁止直接执行 git commit',
813
+ }));
814
+
815
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
816
+ expect(promptText).toContain('## 任务规则');
817
+ expect(promptText).toContain('- **chinese-output**: 所有输出必须使用中文');
818
+ expect(promptText).toContain('- **no-direct-commit**: 禁止直接执行 git commit');
819
+ // 位置对齐 v6:## 任务规则 在 ## 参考信息 之前(用户指令 → 任务规则 → 参考信息)。
820
+ const rulesIdx = promptText.indexOf('## 任务规则');
821
+ const refIdx = promptText.indexOf('## 参考信息');
822
+ expect(rulesIdx).toBeGreaterThan(-1);
823
+ expect(rulesIdx).toBeLessThan(refIdx);
824
+ });
825
+
826
+ it('agent 路注入 ## 任务规则 节(非空 resolvedRules)', async () => {
827
+ const { subagents, agents, runner } = harness({
828
+ defaultTimeoutMs: 60000,
829
+ defaultProvider: 'p',
830
+ defaultModel: 'm',
831
+ });
832
+ const child = new FakeAgent('child-1');
833
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
834
+ agents.register(child);
835
+
836
+ await runner.run(requestWithRules(agentStep(), { 'chinese-output': '所有输出必须使用中文' }));
837
+
838
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
839
+ expect(promptText).toContain('## 任务规则');
840
+ expect(promptText).toContain('- **chinese-output**: 所有输出必须使用中文');
841
+ });
842
+
843
+ it('skill 路注入 ## 任务规则 节(非空 resolvedRules)', async () => {
844
+ const { subagents, agents, runner } = harness({
845
+ defaultTimeoutMs: 60000,
846
+ defaultProvider: 'p',
847
+ defaultModel: 'm',
848
+ skills: ['code-review-skill'],
849
+ });
850
+ const child = new FakeAgent('child-1');
851
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
852
+ agents.register(child);
853
+
854
+ await runner.run(requestWithRules(skillStep('code-review-skill'), { 'chinese-output': '所有输出必须使用中文' }));
855
+
856
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
857
+ expect(promptText).toContain('## 任务规则');
858
+ expect(promptText).toContain('- **chinese-output**: 所有输出必须使用中文');
859
+ });
860
+
861
+ it('mcp 路注入 ## 任务规则 节(非空 resolvedRules)', async () => {
862
+ const { subagents, agents, runner } = harness({
863
+ defaultTimeoutMs: 60000,
864
+ defaultProvider: 'p',
865
+ defaultModel: 'm',
866
+ mcps: ['filesystem-mcp'],
867
+ });
868
+ const child = new FakeAgent('child-1');
869
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
870
+ agents.register(child);
871
+
872
+ await runner.run(requestWithRules(mcpStep('filesystem-mcp'), { 'chinese-output': '所有输出必须使用中文' }));
873
+
874
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
875
+ expect(promptText).toContain('## 任务规则');
876
+ expect(promptText).toContain('- **chinese-output**: 所有输出必须使用中文');
877
+ });
878
+
879
+ it('resolvedRules 缺省时不注入 ## 任务规则 节(向后兼容)', async () => {
880
+ const { subagents, agents, runner } = harness({
881
+ defaultTimeoutMs: 60000,
882
+ defaultProvider: 'p',
883
+ defaultModel: 'm',
884
+ });
885
+ const child = new FakeAgent('child-1');
886
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
887
+ agents.register(child);
888
+
889
+ await runner.run(request(promptStep()));
890
+
891
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
892
+ // 头部三点列表含 `` `## 任务规则` `` 引用(行内反引号),正文节标题为独立行
893
+ // `\n## 任务规则\n`——此处断言正文节不注入。
894
+ expect(promptText).not.toContain('\n## 任务规则\n');
895
+ });
896
+
897
+ it('resolvedRules 为空对象时不注入 ## 任务规则 节', async () => {
898
+ const { subagents, agents, runner } = harness({
899
+ defaultTimeoutMs: 60000,
900
+ defaultProvider: 'p',
901
+ defaultModel: 'm',
902
+ });
903
+ const child = new FakeAgent('child-1');
904
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
905
+ agents.register(child);
906
+
907
+ await runner.run(requestWithRules(promptStep(), {}));
908
+
909
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
910
+ expect(promptText).not.toContain('\n## 任务规则\n');
911
+ });
912
+ });
913
+
914
+ /**
915
+ * settle 测试用控制器:可手动触发 settle 的 `awaitSubagentSettlement` 注入实现。
916
+ * 缓冲 settle(settle 早于 awaiter 注册时存入 buffered,awaiter 注册即消费)——对齐
917
+ * workflow-engine 的缓冲路由语义。
918
+ */
919
+ function settleController(): {
920
+ awaitSubagentSettlement: NonNullable<StepRunnerConfig['awaitSubagentSettlement']>;
921
+ settle: (childId: string, stopReason: SubagentStopReason, output?: readonly ContentBlock[]) => void;
922
+ } {
923
+ const pending = new Map<string, (s: SubagentSettlement) => void>();
924
+ const buffered = new Map<string, SubagentSettlement>();
925
+ const awaitSubagentSettlement: NonNullable<StepRunnerConfig['awaitSubagentSettlement']> = (
926
+ childId,
927
+ _signal,
928
+ ) =>
929
+ new Promise<SubagentSettlement>((resolve) => {
930
+ const buf = buffered.get(childId);
931
+ if (buf !== undefined) {
932
+ buffered.delete(childId);
933
+ resolve(buf);
934
+ return;
935
+ }
936
+ pending.set(childId, resolve);
937
+ });
938
+ const settle = (
939
+ childId: string,
940
+ stopReason: SubagentStopReason,
941
+ output?: readonly ContentBlock[],
942
+ ): void => {
943
+ const settlement: SubagentSettlement = {
944
+ childId,
945
+ stopReason,
946
+ ...(output === undefined ? {} : { output }),
947
+ };
948
+ const resolve = pending.get(childId);
949
+ if (resolve !== undefined) {
950
+ pending.delete(childId);
951
+ resolve(settlement);
952
+ } else {
953
+ buffered.set(childId, settlement);
954
+ }
955
+ };
956
+ return { awaitSubagentSettlement, settle };
957
+ }
958
+
959
+ /** 组装带 settle 注入的 StepRunner harness(agent Step + 真实 childId 占位)。 */
960
+ function settleHarness(ctrl: ReturnType<typeof settleController>): {
961
+ agents: FakeAgentRegistry;
962
+ subagents: FakeSubagents;
963
+ runner: StepRunner;
964
+ } {
965
+ const { agents, subagents, runner } = harness({
966
+ defaultTimeoutMs: 60000,
967
+ defaultProvider: 'p',
968
+ defaultModel: 'm',
969
+ awaitSubagentSettlement: ctrl.awaitSubagentSettlement,
970
+ });
971
+ return { agents, subagents, runner };
972
+ }
973
+
974
+ describe('StepRunner subagent settle completion (DSH subagent/end alignment)', () => {
975
+ // 完成判定对齐 DSH `subagent/end`:collectSubagentOutput 等 settle(非 whenIdle),
976
+ // 按 stopReason 路由:completed→完成 / aborted→STEP_PAUSED / error·max-tokens·refusal→STEP_EXECUTION。
977
+
978
+ it('settle(completed) returns the final assistant output as step output', async () => {
979
+ const child = new FakeAgent('child-1');
980
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'done' }]));
981
+ const ctrl = settleController();
982
+ const { agents, runner } = settleHarness(ctrl);
983
+ agents.register(child);
984
+
985
+ const runP = runner.run(request(agentStep()));
986
+ ctrl.settle('child-1', 'completed');
987
+ const result = await runP;
988
+ expect(result.output).toEqual([{ type: 'text', text: 'done' }]);
989
+ expect(result.sessionMeta).toEqual({ childId: 'child-1', synthetic: false, command: 'agent' });
990
+ });
991
+
992
+ it('settle(completed, output) returns the settlement output verbatim', async () => {
993
+ const child = new FakeAgent('child-1');
994
+ const ctrl = settleController();
995
+ const { agents, runner } = settleHarness(ctrl);
996
+ agents.register(child);
997
+
998
+ const runP = runner.run(request(agentStep()));
999
+ ctrl.settle('child-1', 'completed', [{ type: 'text', text: 'settle output' }]);
1000
+ const result = await runP;
1001
+ expect(result.output).toEqual([{ type: 'text', text: 'settle output' }]);
1002
+ });
1003
+
1004
+ it('settle(aborted) throws STEP_PAUSED (interrupt/pause → not failed)', async () => {
1005
+ const child = new FakeAgent('child-1');
1006
+ const ctrl = settleController();
1007
+ const { agents, runner } = settleHarness(ctrl);
1008
+ agents.register(child);
1009
+
1010
+ const runP = runner.run(request(agentStep()));
1011
+ ctrl.settle('child-1', 'aborted');
1012
+ await expect(runP).rejects.toMatchObject({ code: 'STEP_PAUSED' });
1013
+ });
1014
+
1015
+ it('settle(error) throws STEP_EXECUTION (failed)', async () => {
1016
+ const child = new FakeAgent('child-1');
1017
+ const ctrl = settleController();
1018
+ const { agents, runner } = settleHarness(ctrl);
1019
+ agents.register(child);
1020
+
1021
+ const runP = runner.run(request(agentStep()));
1022
+ ctrl.settle('child-1', 'error');
1023
+ await expect(runP).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
1024
+ });
1025
+
1026
+ it('settle(max-tokens) throws STEP_EXECUTION (token ceiling → failed)', async () => {
1027
+ const child = new FakeAgent('child-1');
1028
+ const ctrl = settleController();
1029
+ const { agents, runner } = settleHarness(ctrl);
1030
+ agents.register(child);
1031
+
1032
+ const runP = runner.run(request(agentStep()));
1033
+ ctrl.settle('child-1', 'max-tokens');
1034
+ await expect(runP).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
1035
+ });
1036
+
1037
+ it('settle(refusal) throws STEP_EXECUTION (declined → failed)', async () => {
1038
+ const child = new FakeAgent('child-1');
1039
+ const ctrl = settleController();
1040
+ const { agents, runner } = settleHarness(ctrl);
1041
+ agents.register(child);
1042
+
1043
+ const runP = runner.run(request(agentStep()));
1044
+ ctrl.settle('child-1', 'refusal');
1045
+ await expect(runP).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
1046
+ });
1047
+
1048
+ it('settle(completed) with no assistant output throws STEP_EXECUTION (no output)', async () => {
1049
+ const child = new FakeAgent('child-1'); // no events
1050
+ const ctrl = settleController();
1051
+ const { agents, runner } = settleHarness(ctrl);
1052
+ agents.register(child);
1053
+
1054
+ const runP = runner.run(request(agentStep()));
1055
+ ctrl.settle('child-1', 'completed');
1056
+ await expect(runP).rejects.toMatchObject({ code: 'STEP_EXECUTION' });
1057
+ });
1058
+ });
1059
+
1060
+ // ── v6:前置输出按 step.inputFrom 过滤(配置只依赖 sce1 却显示所有前置 step) ──
1061
+ describe('StepRunner inputFrom 前置输出过滤(v6)', () => {
1062
+ it('只注入 inputFrom 声明的依赖(path 匹配),未声明的前置不出现', async () => {
1063
+ const { subagents, agents, runner } = harness({
1064
+ defaultTimeoutMs: 60000,
1065
+ defaultProvider: 'p',
1066
+ defaultModel: 'm',
1067
+ });
1068
+ const child = new FakeAgent('child-1');
1069
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
1070
+ agents.register(child);
1071
+ const step: WorkflowStep = { ...promptStep(), inputFrom: [{ path: 'sce1' }] };
1072
+ const previousOutputs = [
1073
+ { name: 'sce1', id: 'sce1', returnPrompt: 'sce1 output', files: [] },
1074
+ { name: 'parallel1', id: 'parallel1', returnPrompt: 'miku', files: [] },
1075
+ { name: 'sce2', id: 'sce2', returnPrompt: 'sce2 old round', files: [] },
1076
+ ];
1077
+
1078
+ await runner.run(request(step, { previousOutputs }));
1079
+
1080
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
1081
+ expect(promptText).toContain('前置Step: sce1');
1082
+ expect(promptText).toContain('sce1 output');
1083
+ expect(promptText).not.toContain('前置Step: parallel1');
1084
+ expect(promptText).not.toContain('前置Step: sce2');
1085
+ expect(promptText).not.toContain('miku');
1086
+ });
1087
+
1088
+ it('同 stepId 多段(循环/重试)去重,只保留最近一段', async () => {
1089
+ const { subagents, agents, runner } = harness({
1090
+ defaultTimeoutMs: 60000,
1091
+ defaultProvider: 'p',
1092
+ defaultModel: 'm',
1093
+ });
1094
+ const child = new FakeAgent('child-1');
1095
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
1096
+ agents.register(child);
1097
+ const step: WorkflowStep = { ...promptStep(), inputFrom: [{ path: 'sce1' }] };
1098
+ const previousOutputs = [
1099
+ { name: 'sce1', id: 'sce1', returnPrompt: 'old round', files: [] },
1100
+ { name: 'sce1', id: 'sce1', returnPrompt: 'nearest round', files: [] },
1101
+ ];
1102
+
1103
+ await runner.run(request(step, { previousOutputs }));
1104
+
1105
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
1106
+ expect(promptText).toContain('nearest round');
1107
+ expect(promptText).not.toContain('old round');
1108
+ expect(promptText).toContain('前置Step: sce1');
1109
+ });
1110
+
1111
+ it('inputFrom 未配置时保留全量前置输出(向后兼容)', async () => {
1112
+ const { subagents, agents, runner } = harness({
1113
+ defaultTimeoutMs: 60000,
1114
+ defaultProvider: 'p',
1115
+ defaultModel: 'm',
1116
+ });
1117
+ const child = new FakeAgent('child-1');
1118
+ child.session.append(assistantMessageEvent([{ type: 'text', text: 'ok' }]));
1119
+ agents.register(child);
1120
+ const previousOutputs = [
1121
+ { name: 'sce1', id: 'sce1', returnPrompt: 'sce1 output', files: [] },
1122
+ { name: 'parallel1', id: 'parallel1', returnPrompt: 'miku', files: [] },
1123
+ ];
1124
+
1125
+ await runner.run(request(promptStep(), { previousOutputs }));
1126
+
1127
+ const promptText = textOf(subagents.continuableStarts[0]?.request.prompt[0]);
1128
+ expect(promptText).toContain('前置Step: sce1');
1129
+ expect(promptText).toContain('前置Step: parallel1');
1130
+ expect(promptText).toContain('miku');
1131
+ });
1132
+ });