autocrew 0.1.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 (165) hide show
  1. package/HAMLETDEER.md +562 -0
  2. package/LICENSE +21 -0
  3. package/README.md +190 -0
  4. package/README_CN.md +190 -0
  5. package/adapters/openclaw/index.ts +68 -0
  6. package/bin/autocrew.mjs +23 -0
  7. package/bin/autocrew.ts +13 -0
  8. package/openclaw.plugin.json +36 -0
  9. package/package.json +74 -0
  10. package/skills/_writing-style/SKILL.md +68 -0
  11. package/skills/audience-profiler/SKILL.md +241 -0
  12. package/skills/content-attribution/SKILL.md +128 -0
  13. package/skills/content-review/SKILL.md +257 -0
  14. package/skills/cover-generator/SKILL.md +93 -0
  15. package/skills/humanizer-zh/SKILL.md +75 -0
  16. package/skills/intel-digest/SKILL.md +57 -0
  17. package/skills/intel-pull/SKILL.md +74 -0
  18. package/skills/manage-pipeline/SKILL.md +63 -0
  19. package/skills/memory-distill/SKILL.md +89 -0
  20. package/skills/onboarding/SKILL.md +117 -0
  21. package/skills/pipeline-status/SKILL.md +51 -0
  22. package/skills/platform-rewrite/SKILL.md +125 -0
  23. package/skills/pre-publish/SKILL.md +142 -0
  24. package/skills/publish-content/SKILL.md +500 -0
  25. package/skills/remix-content/SKILL.md +77 -0
  26. package/skills/research/SKILL.md +127 -0
  27. package/skills/setup/SKILL.md +353 -0
  28. package/skills/spawn-batch-writer/SKILL.md +66 -0
  29. package/skills/spawn-planner/SKILL.md +72 -0
  30. package/skills/spawn-writer/SKILL.md +60 -0
  31. package/skills/teardown/SKILL.md +144 -0
  32. package/skills/title-craft/SKILL.md +234 -0
  33. package/skills/topic-ideas/SKILL.md +105 -0
  34. package/skills/video-timeline/SKILL.md +117 -0
  35. package/skills/write-script/SKILL.md +232 -0
  36. package/skills/xhs-cover-review/SKILL.md +48 -0
  37. package/src/adapters/browser/browser-cdp.ts +260 -0
  38. package/src/adapters/browser/browser-relay.ts +236 -0
  39. package/src/adapters/browser/gateway-client.ts +148 -0
  40. package/src/adapters/browser/types.ts +36 -0
  41. package/src/adapters/image/gemini.ts +219 -0
  42. package/src/adapters/research/tikhub.ts +19 -0
  43. package/src/cli/banner.ts +18 -0
  44. package/src/cli/bootstrap.ts +33 -0
  45. package/src/cli/commands/adapt.ts +28 -0
  46. package/src/cli/commands/advance.ts +28 -0
  47. package/src/cli/commands/assets.ts +24 -0
  48. package/src/cli/commands/audit.ts +18 -0
  49. package/src/cli/commands/contents.ts +18 -0
  50. package/src/cli/commands/cover.ts +58 -0
  51. package/src/cli/commands/events.ts +17 -0
  52. package/src/cli/commands/humanize.ts +27 -0
  53. package/src/cli/commands/index.ts +80 -0
  54. package/src/cli/commands/init.ts +28 -0
  55. package/src/cli/commands/intel.ts +55 -0
  56. package/src/cli/commands/learn.ts +34 -0
  57. package/src/cli/commands/memory.ts +18 -0
  58. package/src/cli/commands/migrate.ts +24 -0
  59. package/src/cli/commands/open.ts +21 -0
  60. package/src/cli/commands/pipelines.ts +18 -0
  61. package/src/cli/commands/pre-publish.ts +27 -0
  62. package/src/cli/commands/profile.ts +31 -0
  63. package/src/cli/commands/research.ts +36 -0
  64. package/src/cli/commands/restore.ts +28 -0
  65. package/src/cli/commands/review.ts +61 -0
  66. package/src/cli/commands/start.ts +28 -0
  67. package/src/cli/commands/status.ts +14 -0
  68. package/src/cli/commands/templates.ts +15 -0
  69. package/src/cli/commands/topics.ts +18 -0
  70. package/src/cli/commands/trash.ts +28 -0
  71. package/src/cli/commands/upgrade.ts +48 -0
  72. package/src/cli/commands/versions.ts +24 -0
  73. package/src/cli/index.ts +40 -0
  74. package/src/data/sensitive-words-builtin.json +114 -0
  75. package/src/data/source-presets.yaml +54 -0
  76. package/src/e2e.test.ts +596 -0
  77. package/src/modules/auth/cookie-manager.ts +113 -0
  78. package/src/modules/cards/template-engine.ts +74 -0
  79. package/src/modules/cards/templates/comparison-table.ts +71 -0
  80. package/src/modules/cards/templates/data-chart.ts +76 -0
  81. package/src/modules/cards/templates/flow-chart.ts +49 -0
  82. package/src/modules/cards/templates/key-points.ts +59 -0
  83. package/src/modules/cover/prompt-builder.test.ts +157 -0
  84. package/src/modules/cover/prompt-builder.ts +212 -0
  85. package/src/modules/cover/ratio-adapter.test.ts +122 -0
  86. package/src/modules/cover/ratio-adapter.ts +104 -0
  87. package/src/modules/filter/sensitive-words.test.ts +72 -0
  88. package/src/modules/filter/sensitive-words.ts +212 -0
  89. package/src/modules/humanizer/zh.test.ts +75 -0
  90. package/src/modules/humanizer/zh.ts +175 -0
  91. package/src/modules/intel/collector.ts +19 -0
  92. package/src/modules/intel/collectors/competitor.test.ts +71 -0
  93. package/src/modules/intel/collectors/competitor.ts +65 -0
  94. package/src/modules/intel/collectors/rss.test.ts +56 -0
  95. package/src/modules/intel/collectors/rss.ts +70 -0
  96. package/src/modules/intel/collectors/trends.test.ts +80 -0
  97. package/src/modules/intel/collectors/trends.ts +107 -0
  98. package/src/modules/intel/collectors/web-search.test.ts +85 -0
  99. package/src/modules/intel/collectors/web-search.ts +81 -0
  100. package/src/modules/intel/integration.test.ts +203 -0
  101. package/src/modules/intel/intel-engine.test.ts +103 -0
  102. package/src/modules/intel/intel-engine.ts +96 -0
  103. package/src/modules/intel/source-config.test.ts +113 -0
  104. package/src/modules/intel/source-config.ts +131 -0
  105. package/src/modules/learnings/diff-tracker.test.ts +144 -0
  106. package/src/modules/learnings/diff-tracker.ts +189 -0
  107. package/src/modules/learnings/rule-distiller.ts +141 -0
  108. package/src/modules/memory/distill.ts +208 -0
  109. package/src/modules/migrate/legacy-migrate.test.ts +169 -0
  110. package/src/modules/migrate/legacy-migrate.ts +229 -0
  111. package/src/modules/pro/api-client.ts +192 -0
  112. package/src/modules/pro/gate.test.ts +110 -0
  113. package/src/modules/pro/gate.ts +104 -0
  114. package/src/modules/profile/creator-profile.test.ts +178 -0
  115. package/src/modules/profile/creator-profile.ts +248 -0
  116. package/src/modules/publish/douyin-api.ts +34 -0
  117. package/src/modules/publish/wechat-mp.ts +320 -0
  118. package/src/modules/publish/xiaohongshu-api.ts +127 -0
  119. package/src/modules/research/free-engine.ts +360 -0
  120. package/src/modules/timeline/markup-generator.ts +63 -0
  121. package/src/modules/timeline/parser.ts +275 -0
  122. package/src/modules/workflow/templates.ts +124 -0
  123. package/src/modules/writing/platform-rewrite.ts +190 -0
  124. package/src/modules/writing/title-hashtag.ts +385 -0
  125. package/src/runtime/context.test.ts +97 -0
  126. package/src/runtime/context.ts +129 -0
  127. package/src/runtime/events.test.ts +83 -0
  128. package/src/runtime/events.ts +104 -0
  129. package/src/runtime/hooks.ts +174 -0
  130. package/src/runtime/tool-runner.test.ts +204 -0
  131. package/src/runtime/tool-runner.ts +282 -0
  132. package/src/runtime/workflow-engine.test.ts +455 -0
  133. package/src/runtime/workflow-engine.ts +391 -0
  134. package/src/server/index.ts +409 -0
  135. package/src/server/start.ts +39 -0
  136. package/src/storage/local-store.test.ts +304 -0
  137. package/src/storage/local-store.ts +704 -0
  138. package/src/storage/pipeline-store.test.ts +363 -0
  139. package/src/storage/pipeline-store.ts +698 -0
  140. package/src/tools/asset.ts +96 -0
  141. package/src/tools/content-save.ts +276 -0
  142. package/src/tools/cover-review.ts +221 -0
  143. package/src/tools/humanize.ts +54 -0
  144. package/src/tools/init.ts +133 -0
  145. package/src/tools/intel.ts +92 -0
  146. package/src/tools/memory.ts +76 -0
  147. package/src/tools/pipeline-ops.ts +109 -0
  148. package/src/tools/pipeline.ts +168 -0
  149. package/src/tools/pre-publish.ts +232 -0
  150. package/src/tools/publish.ts +183 -0
  151. package/src/tools/registry.ts +198 -0
  152. package/src/tools/research.ts +304 -0
  153. package/src/tools/review.ts +305 -0
  154. package/src/tools/rewrite.ts +165 -0
  155. package/src/tools/status.ts +30 -0
  156. package/src/tools/timeline.ts +234 -0
  157. package/src/tools/topic-create.ts +50 -0
  158. package/src/types/providers.ts +69 -0
  159. package/src/types/timeline.test.ts +147 -0
  160. package/src/types/timeline.ts +83 -0
  161. package/src/utils/retry.test.ts +97 -0
  162. package/src/utils/retry.ts +85 -0
  163. package/templates/AGENTS.md +99 -0
  164. package/templates/SOUL.md +31 -0
  165. package/templates/TOOLS.md +76 -0
@@ -0,0 +1,455 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { WorkflowEngine, type WorkflowDefinition } from "./workflow-engine.js";
3
+ import { ToolRunner } from "./tool-runner.js";
4
+ import { createContext } from "./context.js";
5
+ import { EventBus } from "./events.js";
6
+ import fs from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+
10
+ let testDir: string;
11
+
12
+ /** Create a complete profile so onboarding gate doesn't block */
13
+ async function seedProfile(dir: string) {
14
+ await fs.mkdir(path.join(dir, "profiles"), { recursive: true });
15
+ const profile = {
16
+ industry: "tech",
17
+ platforms: ["xhs"],
18
+ audiencePersona: { name: "test", age: "25-35", job: "dev" },
19
+ styleCalibrated: true,
20
+ writingRules: [],
21
+ competitorAccounts: [],
22
+ performanceHistory: [],
23
+ };
24
+ await fs.writeFile(path.join(dir, "creator-profile.json"), JSON.stringify(profile));
25
+ }
26
+
27
+ function makeToolRunner(dir: string): ToolRunner {
28
+ const ctx = createContext({ data_dir: dir });
29
+ return new ToolRunner({ ctx, eventBus: new EventBus() });
30
+ }
31
+
32
+ /** A simple 3-step workflow: step_a → step_b (approval) → step_c */
33
+ function simpleDefinition(): WorkflowDefinition {
34
+ return {
35
+ id: "test_workflow",
36
+ name: "Test Workflow",
37
+ description: "A test workflow",
38
+ steps: [
39
+ { id: "a", name: "Step A", tool: "tool_a", params: {} },
40
+ { id: "b", name: "Step B", tool: "tool_b", params: {}, requiresApproval: true },
41
+ { id: "c", name: "Step C", tool: "tool_c", params: {} },
42
+ ],
43
+ };
44
+ }
45
+
46
+ describe("WorkflowEngine", () => {
47
+ let runner: ToolRunner;
48
+ let engine: WorkflowEngine;
49
+
50
+ beforeEach(async () => {
51
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), "autocrew-wf-test-"));
52
+ await seedProfile(testDir);
53
+ runner = makeToolRunner(testDir);
54
+ engine = new WorkflowEngine(runner, testDir);
55
+ });
56
+
57
+ afterEach(async () => {
58
+ await fs.rm(testDir, { recursive: true, force: true });
59
+ });
60
+
61
+ it("creates and starts a workflow that runs to completion", async () => {
62
+ // Register mock tools
63
+ runner.register({
64
+ name: "tool_a",
65
+ label: "A",
66
+ description: "a",
67
+ parameters: {},
68
+ execute: vi.fn(async () => ({ ok: true, value: "a_result" })),
69
+ });
70
+ runner.register({
71
+ name: "tool_c",
72
+ label: "C",
73
+ description: "c",
74
+ parameters: {},
75
+ execute: vi.fn(async () => ({ ok: true, value: "c_result" })),
76
+ });
77
+
78
+ // No approval step — use a definition without approval
79
+ const def: WorkflowDefinition = {
80
+ id: "simple_no_approval",
81
+ name: "Simple",
82
+ description: "No approvals",
83
+ steps: [
84
+ { id: "a", name: "Step A", tool: "tool_a", params: {} },
85
+ { id: "c", name: "Step C", tool: "tool_c", params: {} },
86
+ ],
87
+ };
88
+ engine.registerDefinition(def);
89
+
90
+ const instance = await engine.create("simple_no_approval");
91
+ expect(instance.status).toBe("pending");
92
+
93
+ const result = await engine.start(instance.id);
94
+ expect(result.status).toBe("completed");
95
+ expect(result.currentStepIndex).toBe(2);
96
+ expect(result.stepResults).toHaveProperty("a");
97
+ expect(result.stepResults).toHaveProperty("c");
98
+ });
99
+
100
+ it("pauses at an approval step", async () => {
101
+ runner.register({
102
+ name: "tool_a",
103
+ label: "A",
104
+ description: "a",
105
+ parameters: {},
106
+ execute: vi.fn(async () => ({ ok: true })),
107
+ });
108
+ runner.register({
109
+ name: "tool_b",
110
+ label: "B",
111
+ description: "b",
112
+ parameters: {},
113
+ execute: vi.fn(async () => ({ ok: true })),
114
+ });
115
+ runner.register({
116
+ name: "tool_c",
117
+ label: "C",
118
+ description: "c",
119
+ parameters: {},
120
+ execute: vi.fn(async () => ({ ok: true })),
121
+ });
122
+
123
+ engine.registerDefinition(simpleDefinition());
124
+
125
+ const instance = await engine.create("test_workflow");
126
+ const result = await engine.start(instance.id);
127
+
128
+ // Should pause at step_b (index 1) which requires approval
129
+ expect(result.status).toBe("paused");
130
+ expect(result.currentStepIndex).toBe(1);
131
+ // step_a should have executed
132
+ expect(result.stepResults).toHaveProperty("a");
133
+ // step_b should NOT have executed yet
134
+ expect(result.stepResults).not.toHaveProperty("b");
135
+ });
136
+
137
+ it("resumes after approval", async () => {
138
+ runner.register({
139
+ name: "tool_a",
140
+ label: "A",
141
+ description: "a",
142
+ parameters: {},
143
+ execute: vi.fn(async () => ({ ok: true })),
144
+ });
145
+ runner.register({
146
+ name: "tool_b",
147
+ label: "B",
148
+ description: "b",
149
+ parameters: {},
150
+ execute: vi.fn(async () => ({ ok: true })),
151
+ });
152
+ runner.register({
153
+ name: "tool_c",
154
+ label: "C",
155
+ description: "c",
156
+ parameters: {},
157
+ execute: vi.fn(async () => ({ ok: true })),
158
+ });
159
+
160
+ engine.registerDefinition(simpleDefinition());
161
+
162
+ const instance = await engine.create("test_workflow");
163
+ await engine.start(instance.id);
164
+
165
+ // Approve the paused step
166
+ const result = await engine.approve(instance.id);
167
+
168
+ // Should run step_c and complete
169
+ expect(result.status).toBe("completed");
170
+ expect(result.stepResults).toHaveProperty("c");
171
+ });
172
+
173
+ it("cancels a workflow", async () => {
174
+ runner.register({
175
+ name: "tool_a",
176
+ label: "A",
177
+ description: "a",
178
+ parameters: {},
179
+ execute: vi.fn(async () => ({ ok: true })),
180
+ });
181
+ runner.register({
182
+ name: "tool_b",
183
+ label: "B",
184
+ description: "b",
185
+ parameters: {},
186
+ execute: vi.fn(async () => ({ ok: true })),
187
+ });
188
+ runner.register({
189
+ name: "tool_c",
190
+ label: "C",
191
+ description: "c",
192
+ parameters: {},
193
+ execute: vi.fn(async () => ({ ok: true })),
194
+ });
195
+
196
+ engine.registerDefinition(simpleDefinition());
197
+
198
+ const instance = await engine.create("test_workflow");
199
+ await engine.start(instance.id);
200
+ // Workflow is paused at step_b
201
+
202
+ const cancelled = await engine.cancel(instance.id);
203
+ expect(cancelled.status).toBe("cancelled");
204
+ });
205
+
206
+ it("sets status to failed when a step fails", async () => {
207
+ runner.register({
208
+ name: "tool_a",
209
+ label: "A",
210
+ description: "a",
211
+ parameters: {},
212
+ execute: vi.fn(async () => ({ ok: false, error: "tool_a broke" })),
213
+ });
214
+
215
+ const def: WorkflowDefinition = {
216
+ id: "fail_workflow",
217
+ name: "Fail",
218
+ description: "Fails at step a",
219
+ steps: [{ id: "a", name: "Step A", tool: "tool_a", params: {} }],
220
+ };
221
+ engine.registerDefinition(def);
222
+
223
+ const instance = await engine.create("fail_workflow");
224
+ const result = await engine.start(instance.id);
225
+
226
+ expect(result.status).toBe("failed");
227
+ expect(result.error).toContain("Step A");
228
+ expect(result.error).toContain("tool_a broke");
229
+ });
230
+
231
+ it("persists state to disk after each step", async () => {
232
+ runner.register({
233
+ name: "tool_a",
234
+ label: "A",
235
+ description: "a",
236
+ parameters: {},
237
+ execute: vi.fn(async () => ({ ok: true, data: 42 })),
238
+ });
239
+ runner.register({
240
+ name: "tool_b",
241
+ label: "B",
242
+ description: "b",
243
+ parameters: {},
244
+ execute: vi.fn(async () => ({ ok: true })),
245
+ });
246
+ runner.register({
247
+ name: "tool_c",
248
+ label: "C",
249
+ description: "c",
250
+ parameters: {},
251
+ execute: vi.fn(async () => ({ ok: true })),
252
+ });
253
+
254
+ engine.registerDefinition(simpleDefinition());
255
+ const instance = await engine.create("test_workflow");
256
+ await engine.start(instance.id);
257
+ // Paused at step_b
258
+
259
+ // Read directly from disk
260
+ const filePath = path.join(testDir, "workflows", `${instance.id}.json`);
261
+ const raw = await fs.readFile(filePath, "utf-8");
262
+ const persisted = JSON.parse(raw);
263
+
264
+ expect(persisted.id).toBe(instance.id);
265
+ expect(persisted.status).toBe("paused");
266
+ expect(persisted.stepResults).toHaveProperty("a");
267
+ });
268
+
269
+ it("lists all workflow instances", async () => {
270
+ runner.register({
271
+ name: "tool_a",
272
+ label: "A",
273
+ description: "a",
274
+ parameters: {},
275
+ execute: vi.fn(async () => ({ ok: true })),
276
+ });
277
+
278
+ const def: WorkflowDefinition = {
279
+ id: "list_test",
280
+ name: "List Test",
281
+ description: "For listing",
282
+ steps: [{ id: "a", name: "Step A", tool: "tool_a", params: {} }],
283
+ };
284
+ engine.registerDefinition(def);
285
+
286
+ await engine.create("list_test");
287
+ await engine.create("list_test");
288
+
289
+ const instances = await engine.list();
290
+ expect(instances.length).toBe(2);
291
+ });
292
+
293
+ it("retries a failed step when retry config is set", async () => {
294
+ let callCount = 0;
295
+ runner.register({
296
+ name: "flaky_tool",
297
+ label: "Flaky",
298
+ description: "Fails first, succeeds second",
299
+ parameters: {},
300
+ execute: vi.fn(async () => {
301
+ callCount++;
302
+ if (callCount < 2) return { ok: false, error: "transient" };
303
+ return { ok: true, value: "recovered" };
304
+ }),
305
+ });
306
+
307
+ const def: WorkflowDefinition = {
308
+ id: "retry_workflow",
309
+ name: "Retry",
310
+ description: "Has retry",
311
+ steps: [
312
+ {
313
+ id: "flaky",
314
+ name: "Flaky Step",
315
+ tool: "flaky_tool",
316
+ params: {},
317
+ retry: { maxAttempts: 3, delayMs: 0 },
318
+ },
319
+ ],
320
+ };
321
+ engine.registerDefinition(def);
322
+
323
+ const instance = await engine.create("retry_workflow");
324
+ const result = await engine.start(instance.id);
325
+
326
+ expect(result.status).toBe("completed");
327
+ expect(callCount).toBe(2);
328
+ });
329
+
330
+ it("resolves parameter references from previous step outputs", async () => {
331
+ const capturedParams: Record<string, unknown>[] = [];
332
+
333
+ runner.register({
334
+ name: "producer",
335
+ label: "Producer",
336
+ description: "produces",
337
+ parameters: {},
338
+ execute: vi.fn(async () => ({ ok: true, id: "content-123", title: "Hello" })),
339
+ });
340
+ runner.register({
341
+ name: "consumer",
342
+ label: "Consumer",
343
+ description: "consumes",
344
+ parameters: {},
345
+ execute: vi.fn(async (params: Record<string, unknown>) => {
346
+ capturedParams.push({ ...params });
347
+ return { ok: true };
348
+ }),
349
+ });
350
+
351
+ const def: WorkflowDefinition = {
352
+ id: "ref_workflow",
353
+ name: "Ref",
354
+ description: "Has param references",
355
+ steps: [
356
+ { id: "step1", name: "Produce", tool: "producer", params: {} },
357
+ {
358
+ id: "step2",
359
+ name: "Consume",
360
+ tool: "consumer",
361
+ params: { content_id: "${step1.id}", label: "${step1.title}" },
362
+ },
363
+ ],
364
+ };
365
+ engine.registerDefinition(def);
366
+
367
+ const instance = await engine.create("ref_workflow");
368
+ await engine.start(instance.id);
369
+
370
+ // The consumer should have received resolved params
371
+ expect(capturedParams.length).toBe(1);
372
+ expect(capturedParams[0].content_id).toBe("content-123");
373
+ expect(capturedParams[0].label).toBe("Hello");
374
+ });
375
+
376
+ it("skips a step when condition evaluates to false", async () => {
377
+ const executedTools: string[] = [];
378
+
379
+ runner.register({
380
+ name: "tool_a",
381
+ label: "A",
382
+ description: "a",
383
+ parameters: {},
384
+ execute: vi.fn(async () => {
385
+ executedTools.push("tool_a");
386
+ return { ok: true, skip_next: false };
387
+ }),
388
+ });
389
+ runner.register({
390
+ name: "tool_b",
391
+ label: "B",
392
+ description: "b",
393
+ parameters: {},
394
+ execute: vi.fn(async () => {
395
+ executedTools.push("tool_b");
396
+ return { ok: true };
397
+ }),
398
+ });
399
+ runner.register({
400
+ name: "tool_c",
401
+ label: "C",
402
+ description: "c",
403
+ parameters: {},
404
+ execute: vi.fn(async () => {
405
+ executedTools.push("tool_c");
406
+ return { ok: true };
407
+ }),
408
+ });
409
+
410
+ const def: WorkflowDefinition = {
411
+ id: "cond_workflow",
412
+ name: "Conditional",
413
+ description: "Has conditions",
414
+ steps: [
415
+ { id: "a", name: "Step A", tool: "tool_a", params: {} },
416
+ { id: "b", name: "Step B", tool: "tool_b", params: {}, condition: "a.skip_next" },
417
+ { id: "c", name: "Step C", tool: "tool_c", params: {} },
418
+ ],
419
+ };
420
+ engine.registerDefinition(def);
421
+
422
+ const instance = await engine.create("cond_workflow");
423
+ const result = await engine.start(instance.id);
424
+
425
+ expect(result.status).toBe("completed");
426
+ expect(executedTools).toEqual(["tool_a", "tool_c"]);
427
+ });
428
+
429
+ it("throws when approving a non-paused workflow", async () => {
430
+ runner.register({
431
+ name: "tool_a",
432
+ label: "A",
433
+ description: "a",
434
+ parameters: {},
435
+ execute: vi.fn(async () => ({ ok: true })),
436
+ });
437
+
438
+ const def: WorkflowDefinition = {
439
+ id: "no_pause",
440
+ name: "No Pause",
441
+ description: "Completes immediately",
442
+ steps: [{ id: "a", name: "Step A", tool: "tool_a", params: {} }],
443
+ };
444
+ engine.registerDefinition(def);
445
+
446
+ const instance = await engine.create("no_pause");
447
+ await engine.start(instance.id);
448
+
449
+ await expect(engine.approve(instance.id)).rejects.toThrow("not paused");
450
+ });
451
+
452
+ it("throws when starting a nonexistent workflow", async () => {
453
+ await expect(engine.start("nonexistent")).rejects.toThrow("not found");
454
+ });
455
+ });