minimal-agent 0.2.0 → 0.3.0

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 (107) hide show
  1. package/README.md +50 -72
  2. package/package.json +18 -13
  3. package/plugins/ralph-wiggum/plugin.js +205 -0
  4. package/plugins/ralph-wiggum/src/goalState.js +260 -0
  5. package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
  6. package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
  7. package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
  8. package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
  9. package/plugins/workflow-runner/src/expressions.js +369 -0
  10. package/plugins/workflow-runner/src/index.js +174 -0
  11. package/plugins/workflow-runner/src/loader.js +183 -0
  12. package/plugins/workflow-runner/src/runner.js +290 -0
  13. package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
  14. package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
  15. package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
  16. package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
  17. package/plugins/workflow-runner/src/types.js +59 -0
  18. package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
  19. package/src/bootstrap/cwdArg.js +22 -0
  20. package/src/bootstrap/workingDir.js +31 -0
  21. package/src/cli/configWizard.js +272 -0
  22. package/src/cli/print.js +192 -0
  23. package/src/config/configFile.js +78 -0
  24. package/src/config.js +118 -0
  25. package/src/context/compact.js +357 -0
  26. package/src/context/microCompactLite.js +151 -0
  27. package/src/context/persistContext.js +109 -0
  28. package/src/context/reactiveCompact.js +121 -0
  29. package/src/context/sessionPath.js +58 -0
  30. package/src/context/snipCompact.js +112 -0
  31. package/src/context/tokenCounter.js +66 -0
  32. package/src/llm/client.js +182 -0
  33. package/src/loop.js +230 -0
  34. package/src/main.js +116 -0
  35. package/src/plugin-sdk.js +24 -0
  36. package/src/plugins/commandRouter.js +169 -0
  37. package/src/plugins/hookEngine.js +258 -0
  38. package/src/plugins/pluginApi.js +23 -0
  39. package/src/plugins/pluginLoader.js +71 -0
  40. package/src/plugins/pluginRunner.js +65 -0
  41. package/src/plugins/transcript.js +171 -0
  42. package/src/prompts/projectInstructions.js +48 -0
  43. package/src/prompts/skillList.js +126 -0
  44. package/src/prompts/system.js +155 -0
  45. package/src/session/runTurn.js +41 -0
  46. package/src/session/sessionState.js +19 -0
  47. package/src/tools/bash/bash.js +352 -0
  48. package/src/tools/bash/semantics.js +85 -0
  49. package/src/tools/bash/warnings.js +98 -0
  50. package/src/tools/edit/edit.js +253 -0
  51. package/src/tools/edit/multi-edit.js +155 -0
  52. package/src/tools/glob/glob.js +97 -0
  53. package/src/tools/grep/grep.js +185 -0
  54. package/src/tools/grep/rgPath.js +173 -0
  55. package/src/tools/index.js +94 -0
  56. package/src/tools/read/read.js +209 -0
  57. package/src/tools/shared/fileState.js +61 -0
  58. package/src/tools/shared/fileUtils.js +281 -0
  59. package/src/tools/shared/schemas.js +16 -0
  60. package/src/tools/types.js +21 -0
  61. package/src/tools/webbrowser/browser.js +55 -0
  62. package/src/tools/webbrowser/webbrowser.js +194 -0
  63. package/src/tools/webfetch/preapproved.js +267 -0
  64. package/src/tools/webfetch/webfetch.js +317 -0
  65. package/src/tools/websearch/websearch.js +161 -0
  66. package/src/tools/write/write.js +125 -0
  67. package/src/types/turndown.d.ts +23 -0
  68. package/src/types.js +16 -0
  69. package/src/ui/App.js +37 -0
  70. package/src/ui/InputBox.js +240 -0
  71. package/src/ui/MessageList.js +28 -0
  72. package/src/ui/Root.js +70 -0
  73. package/src/ui/StatusLine.js +41 -0
  74. package/src/ui/ToolStatus.js +11 -0
  75. package/src/ui/hooks/useChat.js +234 -0
  76. package/src/ui/hooks/usePasteHandler.js +137 -0
  77. package/src/ui/hooks/useTextBuffer.js +55 -0
  78. package/src/ui/hooks/useTokenUsage.js +30 -0
  79. package/src/ui/textBuffer.js +217 -0
  80. package/src/utils/packageRoot.js +37 -0
  81. package/src/utils/resourcePaths.js +49 -0
  82. package/src/utils/zodToJson.js +29 -0
  83. package/dist/main.js +0 -5315
  84. package/plugins/ralph-wiggum/plugin.ts +0 -275
  85. package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
  86. package/plugins/ralph-wiggum/src/goalState.ts +0 -310
  87. package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
  88. package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
  89. package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
  90. package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
  91. package/plugins/workflow-runner/src/expressions.ts +0 -371
  92. package/plugins/workflow-runner/src/index.ts +0 -194
  93. package/plugins/workflow-runner/src/loader.ts +0 -193
  94. package/plugins/workflow-runner/src/runner.ts +0 -313
  95. package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
  96. package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
  97. package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
  98. package/plugins/workflow-runner/src/types.ts +0 -183
  99. package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
  100. package/plugins/workflow-runner/test/e2e.test.ts +0 -268
  101. package/plugins/workflow-runner/test/expressions.test.ts +0 -140
  102. package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
  103. package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
  104. package/plugins/workflow-runner/test/graceful.test.ts +0 -139
  105. package/plugins/workflow-runner/test/loader.test.ts +0 -216
  106. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
  107. package/plugins/workflow-runner/test/runner.test.ts +0 -511
@@ -1,511 +0,0 @@
1
- /**
2
- * ============================================================
3
- * test/workflows/runner.test.ts
4
- * ------------------------------------------------------------
5
- * runner P0 行为:
6
- * - tool / llm / assert step 成功路径
7
- * - capture 绑变量到 VarStack
8
- * - when=false → workflow_step_skipped
9
- * - onError=halt(默认)→ workflow halt + error
10
- * - onError=continue → 继续下一步
11
- * - 工作流完成 yield workflow_done
12
- *
13
- * 策略:
14
- * - tool 步骤用真实 Write 工具(写到 tmpdir,避免 mock executeTool)
15
- * - llm 步骤 mock.module('../../../src/llm/client.ts')
16
- * - assert / when 走纯表达式引擎,无需 mock
17
- * ============================================================
18
- */
19
-
20
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
21
- import {
22
- existsSync,
23
- mkdirSync,
24
- mkdtempSync,
25
- readFileSync,
26
- rmSync,
27
- writeFileSync,
28
- } from 'node:fs';
29
- import { tmpdir } from 'node:os';
30
- import { join } from 'node:path';
31
-
32
- import { _resetWorkingDir } from '../../../src/bootstrap/workingDir.ts';
33
- import { runWorkflow } from '../src/runner.ts';
34
- import type {
35
- LoopEvent,
36
- Provider,
37
- } from '../../../src/types.ts';
38
- import type {
39
- RunContext,
40
- WorkflowDef,
41
- WorkflowEvent,
42
- } from '../src/types.ts';
43
-
44
- type WfOrLoop = WorkflowEvent | LoopEvent;
45
-
46
- const fakeProvider: Provider = {
47
- name: 'test',
48
- baseURL: '',
49
- apiKey: '',
50
- model: 'm',
51
- contextWindow: 128000,
52
- };
53
-
54
- async function collect(
55
- gen: AsyncGenerator<WfOrLoop, void, void>,
56
- ): Promise<WfOrLoop[]> {
57
- const out: WfOrLoop[] = [];
58
- for await (const ev of gen) out.push(ev);
59
- return out;
60
- }
61
-
62
- describe('workflows/runner', () => {
63
- let tmp: string;
64
- let ctx: RunContext;
65
-
66
- beforeEach(() => {
67
- tmp = mkdtempSync(join(tmpdir(), 'wf-runner-'));
68
- process.env.MINIMAL_AGENT_CWD = tmp;
69
- _resetWorkingDir();
70
- // 抵抗 pluginRunner.test.ts 等遗留的 workingDir.ts mock 全局污染
71
- mock.module('../../../src/bootstrap/workingDir.ts', () => ({
72
- getWorkingDir: () => tmp,
73
- initWorkingDir: () => tmp,
74
- _resetWorkingDir: () => {},
75
- }));
76
- ctx = { provider: fakeProvider, history: [] };
77
- });
78
-
79
- afterEach(() => {
80
- delete process.env.MINIMAL_AGENT_CWD;
81
- _resetWorkingDir();
82
- rmSync(tmp, { recursive: true, force: true });
83
- });
84
-
85
- it('线性 assert step 全通过 → workflow_done', async () => {
86
- const def: WorkflowDef = {
87
- name: 'pure-assert',
88
- description: 'd',
89
- steps: [
90
- { id: 's1', type: 'assert', condition: '1 == 1' },
91
- { id: 's2', type: 'assert', condition: '"a" != "b"' },
92
- ],
93
- };
94
- const events = await collect(runWorkflow(def, {}, ctx));
95
- const types = events.map((e) => e.type);
96
- expect(types[0]).toBe('workflow_start');
97
- expect(types.filter((t) => t === 'workflow_step_end').length).toBe(2);
98
- expect(types[types.length - 1]).toBe('workflow_done');
99
- });
100
-
101
- it('assert 失败 → halt + error 事件', async () => {
102
- const def: WorkflowDef = {
103
- name: 'assert-fail',
104
- description: 'd',
105
- steps: [
106
- { id: 's1', type: 'assert', condition: '1 == 2', onFail: 'nope' },
107
- { id: 's2', type: 'assert', condition: 'true' }, // 不应执行
108
- ],
109
- };
110
- const events = await collect(runWorkflow(def, {}, ctx));
111
- expect(events.some((e) => e.type === 'error')).toBe(true);
112
- // s2 不应有 step_start
113
- const startedIds = events
114
- .filter((e): e is Extract<WorkflowEvent, { type: 'workflow_step_start' }> => e.type === 'workflow_step_start')
115
- .map((e) => e.id);
116
- expect(startedIds).toEqual(['s1']);
117
- // 没 workflow_done
118
- expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
119
- });
120
-
121
- it('onError=continue → 失败后继续下一步', async () => {
122
- const def: WorkflowDef = {
123
- name: 'continue',
124
- description: 'd',
125
- steps: [
126
- { id: 's1', type: 'assert', condition: 'false', onError: 'continue' },
127
- { id: 's2', type: 'assert', condition: 'true' },
128
- ],
129
- };
130
- const events = await collect(runWorkflow(def, {}, ctx));
131
- const ends = events.filter(
132
- (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> => e.type === 'workflow_step_end',
133
- );
134
- expect(ends.length).toBe(2);
135
- expect(ends[0].ok).toBe(false);
136
- expect(ends[1].ok).toBe(true);
137
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
138
- });
139
-
140
- it('when=false → workflow_step_skipped 而不是 step_start', async () => {
141
- const def: WorkflowDef = {
142
- name: 'skip',
143
- description: 'd',
144
- inputs: [{ name: 'flag', type: 'string' }],
145
- steps: [
146
- {
147
- id: 's1',
148
- type: 'assert',
149
- condition: 'true',
150
- when: 'inputs.flag == "yes"',
151
- },
152
- { id: 's2', type: 'assert', condition: 'true' },
153
- ],
154
- };
155
- const events = await collect(runWorkflow(def, { flag: 'no' }, ctx));
156
- const skipped = events.find((e) => e.type === 'workflow_step_skipped');
157
- expect(skipped).toBeDefined();
158
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
159
- });
160
-
161
- it('tool step 写文件 + ${var} 插值(用真实 Write 工具)', async () => {
162
- const def: WorkflowDef = {
163
- name: 'wr',
164
- description: 'd',
165
- inputs: [{ name: 'book', type: 'string' }],
166
- steps: [
167
- {
168
- id: 'w',
169
- tool: 'Write',
170
- args: {
171
- file_path: 'videos/${inputs.book}/x.txt',
172
- content: 'hello ${inputs.book}',
173
- },
174
- },
175
- ],
176
- };
177
- const events = await collect(runWorkflow(def, { book: '原则' }, ctx));
178
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
179
-
180
- const filePath = join(tmp, 'videos', '原则', 'x.txt');
181
- expect(existsSync(filePath)).toBe(true);
182
- expect(readFileSync(filePath, 'utf8')).toBe('hello 原则');
183
- });
184
-
185
- it('capture 把 tool 结果绑到变量,下一步 assert 能用', async () => {
186
- const def: WorkflowDef = {
187
- name: 'cap',
188
- description: 'd',
189
- steps: [
190
- {
191
- id: 'w',
192
- tool: 'Write',
193
- args: { file_path: 'a.txt', content: 'data' },
194
- capture: { content: 'last_write_content' },
195
- },
196
- {
197
- id: 'check',
198
- type: 'assert',
199
- condition: 'length(last_write_content) > 0',
200
- },
201
- ],
202
- };
203
- const events = await collect(runWorkflow(def, {}, ctx));
204
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
205
- });
206
-
207
- it('未知 tool → halt + error', async () => {
208
- const def: WorkflowDef = {
209
- name: 'badtool',
210
- description: 'd',
211
- steps: [{ id: 'x', tool: 'NoSuchTool9999' }],
212
- };
213
- const events = await collect(runWorkflow(def, {}, ctx));
214
- expect(events.some((e) => e.type === 'error')).toBe(true);
215
- });
216
-
217
- it('llm step 拿 chat 累积文本 + capture text', async () => {
218
- mock.module('../../../src/llm/client.ts', () => ({
219
- chat: async function* () {
220
- yield { type: 'text_delta', delta: 'Hello ' };
221
- yield { type: 'text_delta', delta: 'world' };
222
- yield { type: 'done', stopReason: 'stop' };
223
- },
224
- }));
225
-
226
- const def: WorkflowDef = {
227
- name: 'llm-cap',
228
- description: 'd',
229
- steps: [
230
- {
231
- id: 'g',
232
- llm: 'irrelevant',
233
- capture: { text: 'msg' },
234
- },
235
- {
236
- id: 'check',
237
- type: 'assert',
238
- condition: 'msg == "Hello world"',
239
- },
240
- ],
241
- };
242
- const events = await collect(runWorkflow(def, {}, ctx));
243
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
244
- });
245
-
246
- it('workflow_step_start 的 index 与 total 与 steps 长度一致', async () => {
247
- const def: WorkflowDef = {
248
- name: 'count',
249
- description: 'd',
250
- steps: [
251
- { id: 'a', type: 'assert', condition: 'true' },
252
- { id: 'b', type: 'assert', condition: 'true' },
253
- { id: 'c', type: 'assert', condition: 'true' },
254
- ],
255
- };
256
- const events = await collect(runWorkflow(def, {}, ctx));
257
- const starts = events.filter(
258
- (e): e is Extract<WorkflowEvent, { type: 'workflow_step_start' }> => e.type === 'workflow_step_start',
259
- );
260
- expect(starts.length).toBe(3);
261
- expect(starts.map((s) => s.index)).toEqual([0, 1, 2]);
262
- expect(starts.every((s) => s.total === 3)).toBe(true);
263
- });
264
-
265
- it('signal.aborted → 立即 interrupted', async () => {
266
- const ctrl = new AbortController();
267
- ctrl.abort();
268
- const def: WorkflowDef = {
269
- name: 'abort',
270
- description: 'd',
271
- steps: [{ id: 'a', type: 'assert', condition: 'true' }],
272
- };
273
- const events = await collect(
274
- runWorkflow(def, {}, { provider: fakeProvider, history: [], signal: ctrl.signal }),
275
- );
276
- expect(events.some((e) => e.type === 'interrupted')).toBe(true);
277
- expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
278
- });
279
-
280
- it('P1 branch step:condition=true → 走 then 分支,输出 branch=true', async () => {
281
- const def: WorkflowDef = {
282
- name: 'branch-true',
283
- description: 'd',
284
- inputs: [{ name: 'x', type: 'number' }],
285
- steps: [
286
- {
287
- id: 'br',
288
- type: 'branch',
289
- condition: 'inputs.x > 0',
290
- then: [
291
- {
292
- id: 'then_w',
293
- tool: 'Write',
294
- args: { file_path: 'pos.txt', content: 'positive' },
295
- },
296
- ],
297
- else: [
298
- {
299
- id: 'else_w',
300
- tool: 'Write',
301
- args: { file_path: 'neg.txt', content: 'negative' },
302
- },
303
- ],
304
- },
305
- ],
306
- };
307
- const events = await collect(runWorkflow(def, { x: 5 }, ctx));
308
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
309
- expect(existsSync(join(tmp, 'pos.txt'))).toBe(true);
310
- expect(existsSync(join(tmp, 'neg.txt'))).toBe(false);
311
- const ends = events.filter(
312
- (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> => e.type === 'workflow_step_end',
313
- );
314
- // 至少 then_w (1) + br (1) = 2 个 step_end,且都 ok
315
- expect(ends.length).toBe(2);
316
- expect(ends.every((e) => e.ok)).toBe(true);
317
- const brEnd = ends.find((e) => e.id === 'br');
318
- expect(brEnd?.output).toBe('branch=true');
319
- });
320
-
321
- it('P1 branch step:condition=false → 走 else 分支', async () => {
322
- const def: WorkflowDef = {
323
- name: 'branch-false',
324
- description: 'd',
325
- inputs: [{ name: 'x', type: 'number' }],
326
- steps: [
327
- {
328
- id: 'br',
329
- type: 'branch',
330
- condition: 'inputs.x > 0',
331
- then: [{ id: 'then_a', type: 'assert', condition: 'true' }],
332
- else: [
333
- {
334
- id: 'else_w',
335
- tool: 'Write',
336
- args: { file_path: 'neg.txt', content: 'negative' },
337
- },
338
- ],
339
- },
340
- ],
341
- };
342
- const events = await collect(runWorkflow(def, { x: -3 }, ctx));
343
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
344
- expect(existsSync(join(tmp, 'neg.txt'))).toBe(true);
345
- const brEnd = events.find(
346
- (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
347
- e.type === 'workflow_step_end' && e.id === 'br',
348
- );
349
- expect(brEnd?.output).toBe('branch=false');
350
- });
351
-
352
- it('P1 branch step:缺 then/else 也合法(cond 求值后空跳过)', async () => {
353
- const def: WorkflowDef = {
354
- name: 'branch-empty',
355
- description: 'd',
356
- steps: [
357
- { id: 'br', type: 'branch', condition: '1 == 1' },
358
- { id: 's2', type: 'assert', condition: 'true' },
359
- ],
360
- };
361
- const events = await collect(runWorkflow(def, {}, ctx));
362
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
363
- const ids = events
364
- .filter((e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> => e.type === 'workflow_step_end')
365
- .map((e) => e.id);
366
- expect(ids).toEqual(['br', 's2']);
367
- });
368
-
369
- it('P1 loop step:over=数组字面量,子步骤用 ${as} 和 ${as}_idx', async () => {
370
- const def: WorkflowDef = {
371
- name: 'loop-lit',
372
- description: 'd',
373
- steps: [
374
- {
375
- id: 'lp',
376
- type: 'loop',
377
- over: '["a", "b", "c"]',
378
- as: 'item',
379
- do: [
380
- {
381
- id: 'w',
382
- tool: 'Write',
383
- args: {
384
- file_path: 'items/${item_idx}-${item}.txt',
385
- content: 'idx=${item_idx} val=${item}',
386
- },
387
- },
388
- ],
389
- },
390
- ],
391
- };
392
- const events = await collect(runWorkflow(def, {}, ctx));
393
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
394
- for (let i = 0; i < 3; i++) {
395
- const val = ['a', 'b', 'c'][i];
396
- const filePath = join(tmp, 'items', `${i}-${val}.txt`);
397
- expect(existsSync(filePath)).toBe(true);
398
- expect(readFileSync(filePath, 'utf8')).toBe(`idx=${i} val=${val}`);
399
- }
400
- const lpEnd = events.find(
401
- (e): e is Extract<WorkflowEvent, { type: 'workflow_step_end' }> =>
402
- e.type === 'workflow_step_end' && e.id === 'lp',
403
- );
404
- expect(lpEnd?.output).toBe('loop 3 次');
405
- });
406
-
407
- it('P1 loop step:迭代变量在循环外不再可见(push/pop 帧)', async () => {
408
- const def: WorkflowDef = {
409
- name: 'loop-scope',
410
- description: 'd',
411
- steps: [
412
- {
413
- id: 'lp',
414
- type: 'loop',
415
- over: '[1, 2]',
416
- as: 'i',
417
- do: [{ id: 'noop', type: 'assert', condition: 'i > 0' }],
418
- },
419
- // 循环外的 ${i} 已 pop —— 这里把它当字符串拿不到,length()=0
420
- { id: 'check', type: 'assert', condition: 'length(i) == 0' },
421
- ],
422
- };
423
- const events = await collect(runWorkflow(def, {}, ctx));
424
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
425
- });
426
-
427
- it('P1 loop step:over 求值不是数组 → halt + error', async () => {
428
- const def: WorkflowDef = {
429
- name: 'loop-bad-over',
430
- description: 'd',
431
- steps: [
432
- {
433
- id: 'lp',
434
- type: 'loop',
435
- over: '"not an array"',
436
- as: 'i',
437
- do: [{ id: 'inner', type: 'assert', condition: 'true' }],
438
- },
439
- ],
440
- };
441
- const events = await collect(runWorkflow(def, {}, ctx));
442
- expect(events.some((e) => e.type === 'error')).toBe(true);
443
- expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
444
- });
445
-
446
- it('P1 pause step:非交互模式直接 skipped + step_end ok', async () => {
447
- const def: WorkflowDef = {
448
- name: 'pause-skip',
449
- description: 'd',
450
- steps: [
451
- { id: 'p1', type: 'pause', prompt: '等用户确认' },
452
- { id: 's2', type: 'assert', condition: 'true' },
453
- ],
454
- };
455
- const events = await collect(runWorkflow(def, {}, ctx));
456
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
457
- const skipped = events.find(
458
- (e): e is Extract<WorkflowEvent, { type: 'workflow_step_skipped' }> =>
459
- e.type === 'workflow_step_skipped' && e.id === 'p1',
460
- );
461
- expect(skipped?.reason).toContain('等用户确认');
462
- });
463
-
464
- it('P1 skill step:加载 cwd/skills/<name>/SKILL.md,跑 runQuery 累计 text', async () => {
465
- // 在 tmp 准备一个最小 SKILL.md
466
- const skillDir = join(tmp, 'skills', 'echo-bot');
467
- mkdirSync(skillDir, { recursive: true });
468
- writeFileSync(
469
- join(skillDir, 'SKILL.md'),
470
- '---\nname: echo-bot\ndescription: echo\n---\n\n按用户指示原样输出',
471
- 'utf8',
472
- );
473
-
474
- // skill executor 通过 plugin-sdk.runQuery 调子循环;直接 mock loop.ts
475
- // 避开 chat 层(也免疫其它测试文件遗留的 loop.ts mock 污染)。
476
- mock.module('../../../src/loop.ts', () => ({
477
- runQuery: async function* () {
478
- yield { type: 'text', delta: 'echoed-' };
479
- yield { type: 'text', delta: 'OK' };
480
- yield { type: 'turn_done' };
481
- },
482
- }));
483
-
484
- const def: WorkflowDef = {
485
- name: 'skill-test',
486
- description: 'd',
487
- steps: [
488
- {
489
- id: 'sk',
490
- skill: 'echo-bot',
491
- input: 'say hi',
492
- capture: { text: 'msg' },
493
- },
494
- { id: 'check', type: 'assert', condition: 'msg == "echoed-OK"' },
495
- ],
496
- };
497
- const events = await collect(runWorkflow(def, {}, ctx));
498
- expect(events.some((e) => e.type === 'workflow_done')).toBe(true);
499
- });
500
-
501
- it('P1 skill step:未找到 skill → halt + error', async () => {
502
- const def: WorkflowDef = {
503
- name: 'skill-missing',
504
- description: 'd',
505
- steps: [{ id: 'sk', skill: 'definitely-not-exists' }],
506
- };
507
- const events = await collect(runWorkflow(def, {}, ctx));
508
- expect(events.some((e) => e.type === 'error')).toBe(true);
509
- expect(events.some((e) => e.type === 'workflow_done')).toBe(false);
510
- });
511
- });