@sweny-ai/core 0.1.76 → 0.1.78

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 (123) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +20 -0
  4. package/dist/claude.js +139 -9
  5. package/dist/cli/config.d.ts +22 -0
  6. package/dist/cli/config.js +56 -9
  7. package/dist/cli/main.d.ts +1 -1
  8. package/dist/cli/main.js +56 -57
  9. package/dist/cli/marketplace.js +35 -9
  10. package/dist/cli/publish.d.ts +10 -0
  11. package/dist/cli/publish.js +22 -14
  12. package/dist/executor.js +104 -43
  13. package/dist/loader.d.ts +40 -0
  14. package/dist/loader.js +73 -0
  15. package/dist/mcp-catalog.d.ts +47 -0
  16. package/dist/mcp-catalog.js +304 -0
  17. package/dist/mcp.d.ts +6 -9
  18. package/dist/mcp.js +24 -314
  19. package/dist/requires.d.ts +7 -0
  20. package/dist/requires.js +34 -0
  21. package/dist/retry.d.ts +23 -0
  22. package/dist/retry.js +72 -0
  23. package/dist/schema.d.ts +667 -80
  24. package/dist/schema.js +144 -5
  25. package/dist/skills/custom-loader.d.ts +40 -0
  26. package/dist/skills/custom-loader.js +107 -32
  27. package/dist/skills/index.d.ts +13 -1
  28. package/dist/skills/index.js +12 -3
  29. package/dist/studio.js +4 -1
  30. package/dist/templates.d.ts +29 -8
  31. package/dist/templates.js +30 -20
  32. package/dist/testing.d.ts +7 -0
  33. package/dist/testing.js +7 -0
  34. package/dist/types.d.ts +76 -0
  35. package/dist/verify.d.ts +1 -0
  36. package/dist/verify.js +43 -5
  37. package/package.json +8 -4
  38. package/dist/__tests__/cascade.test.d.ts +0 -11
  39. package/dist/__tests__/cascade.test.js +0 -449
  40. package/dist/__tests__/claude.test.d.ts +0 -1
  41. package/dist/__tests__/claude.test.js +0 -427
  42. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  43. package/dist/__tests__/custom-loader.test.js +0 -116
  44. package/dist/__tests__/executor.test.d.ts +0 -1
  45. package/dist/__tests__/executor.test.js +0 -736
  46. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  48. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  50. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/github.integration.test.js +0 -37
  52. package/dist/__tests__/integration/harness.d.ts +0 -24
  53. package/dist/__tests__/integration/harness.js +0 -34
  54. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  55. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  56. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  57. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  58. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  59. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  60. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  61. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  62. package/dist/__tests__/publish.test.d.ts +0 -1
  63. package/dist/__tests__/publish.test.js +0 -180
  64. package/dist/__tests__/schema.test.d.ts +0 -1
  65. package/dist/__tests__/schema.test.js +0 -526
  66. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  67. package/dist/__tests__/skill-config-integration.test.js +0 -204
  68. package/dist/__tests__/skills-index.test.d.ts +0 -1
  69. package/dist/__tests__/skills-index.test.js +0 -119
  70. package/dist/__tests__/skills.test.d.ts +0 -1
  71. package/dist/__tests__/skills.test.js +0 -565
  72. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  73. package/dist/__tests__/spec-conformance.test.js +0 -1074
  74. package/dist/__tests__/studio.test.d.ts +0 -1
  75. package/dist/__tests__/studio.test.js +0 -403
  76. package/dist/__tests__/templates.test.d.ts +0 -1
  77. package/dist/__tests__/templates.test.js +0 -49
  78. package/dist/__tests__/testing.test.d.ts +0 -1
  79. package/dist/__tests__/testing.test.js +0 -224
  80. package/dist/__tests__/verify.test.d.ts +0 -1
  81. package/dist/__tests__/verify.test.js +0 -517
  82. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  83. package/dist/__tests__/version-consistency.test.js +0 -312
  84. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  85. package/dist/__tests__/workflow-builder.test.js +0 -110
  86. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  87. package/dist/__tests__/workflow-yaml.test.js +0 -201
  88. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  89. package/dist/cli/__tests__/diagram.test.js +0 -225
  90. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  91. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  92. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  93. package/dist/cli/__tests__/upgrade.test.js +0 -317
  94. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  95. package/dist/cli/__tests__/version-check.test.js +0 -209
  96. package/dist/cli/config.test.d.ts +0 -1
  97. package/dist/cli/config.test.js +0 -200
  98. package/dist/cli/credentials.test.d.ts +0 -1
  99. package/dist/cli/credentials.test.js +0 -172
  100. package/dist/cli/e2e.test.d.ts +0 -1
  101. package/dist/cli/e2e.test.js +0 -690
  102. package/dist/cli/marketplace.test.d.ts +0 -1
  103. package/dist/cli/marketplace.test.js +0 -355
  104. package/dist/cli/new.test.d.ts +0 -1
  105. package/dist/cli/new.test.js +0 -1043
  106. package/dist/cli/output.test.d.ts +0 -1
  107. package/dist/cli/output.test.js +0 -180
  108. package/dist/cli/publish.test.d.ts +0 -1
  109. package/dist/cli/publish.test.js +0 -610
  110. package/dist/cli/renderer.test.d.ts +0 -1
  111. package/dist/cli/renderer.test.js +0 -302
  112. package/dist/executor.test.d.ts +0 -1
  113. package/dist/executor.test.js +0 -603
  114. package/dist/mcp.test.d.ts +0 -1
  115. package/dist/mcp.test.js +0 -735
  116. package/dist/mermaid.test.d.ts +0 -1
  117. package/dist/mermaid.test.js +0 -256
  118. package/dist/sources.test.d.ts +0 -1
  119. package/dist/sources.test.js +0 -265
  120. package/dist/sources.validation.test.d.ts +0 -12
  121. package/dist/sources.validation.test.js +0 -380
  122. package/dist/workflow-builder.test.d.ts +0 -1
  123. package/dist/workflow-builder.test.js +0 -117
@@ -1,736 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
3
- import * as path from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { execute } from "../executor.js";
6
- import { MockClaude, createFileSkill } from "../testing.js";
7
- import { createSkillMap } from "../skills/index.js";
8
- // ─── Fixtures ────────────────────────────────────────────────────
9
- const tmpBase = path.join(tmpdir(), "sweny-executor-test");
10
- function freshDir(name) {
11
- const dir = path.join(tmpBase, `${name}-${Date.now()}`);
12
- rmSync(dir, { recursive: true, force: true });
13
- mkdirSync(dir, { recursive: true });
14
- return dir;
15
- }
16
- const linearWorkflow = {
17
- id: "linear",
18
- name: "Linear",
19
- description: "A→B→C",
20
- entry: "a",
21
- nodes: {
22
- a: { name: "A", instruction: "Do A", skills: ["filesystem"] },
23
- b: { name: "B", instruction: "Do B", skills: ["filesystem"] },
24
- c: { name: "C", instruction: "Do C", skills: ["filesystem"] },
25
- },
26
- edges: [
27
- { from: "a", to: "b" },
28
- { from: "b", to: "c" },
29
- ],
30
- };
31
- /** Same shape as linearWorkflow but with no skill requirements — for tests that
32
- * don't exercise tool usage and just need a 3-node DAG. */
33
- const skilllessWorkflow = {
34
- ...linearWorkflow,
35
- id: "linear-no-skills",
36
- nodes: {
37
- a: { name: "A", instruction: "Do A", skills: [] },
38
- b: { name: "B", instruction: "Do B", skills: [] },
39
- c: { name: "C", instruction: "Do C", skills: [] },
40
- },
41
- };
42
- const branchingWorkflow = {
43
- id: "branching",
44
- name: "Branching",
45
- description: "A → B or C",
46
- entry: "check",
47
- nodes: {
48
- check: { name: "Check", instruction: "Check severity", skills: [] },
49
- high: { name: "High", instruction: "Handle high", skills: [] },
50
- low: { name: "Low", instruction: "Handle low", skills: [] },
51
- },
52
- edges: [
53
- { from: "check", to: "high", when: "severity is high" },
54
- { from: "check", to: "low", when: "severity is low" },
55
- ],
56
- };
57
- // ─── Tests ───────────────────────────────────────────────────────
58
- describe("executor", () => {
59
- let outputDir;
60
- beforeEach(() => {
61
- outputDir = freshDir("exec");
62
- });
63
- it("executes a linear workflow end-to-end", async () => {
64
- const fileSkill = createFileSkill(outputDir);
65
- writeFileSync(path.join(outputDir, "input.json"), JSON.stringify({ alert: "test" }));
66
- const claude = new MockClaude({
67
- responses: {
68
- a: {
69
- toolCalls: [{ tool: "fs_read_json", input: { path: "input.json" } }],
70
- data: { context: "loaded" },
71
- },
72
- b: { data: { processed: true } },
73
- c: {
74
- toolCalls: [{ tool: "fs_write_json", input: { path: "out.json", data: { done: true } } }],
75
- data: { written: true },
76
- },
77
- },
78
- });
79
- const events = [];
80
- const { results } = await execute(linearWorkflow, { alert: "test" }, {
81
- skills: createSkillMap([fileSkill]),
82
- claude,
83
- observer: (e) => events.push(e),
84
- config: {},
85
- });
86
- expect(results.size).toBe(3);
87
- expect(results.get("a")?.status).toBe("success");
88
- expect(results.get("b")?.status).toBe("success");
89
- expect(results.get("c")?.status).toBe("success");
90
- // Verify file was written
91
- expect(existsSync(path.join(outputDir, "out.json"))).toBe(true);
92
- // Events — workflow:start fires first, then sources:resolved (per spec)
93
- expect(events[0]).toEqual({ type: "workflow:start", workflow: "linear" });
94
- expect(events[1].type).toBe("sources:resolved");
95
- expect(events.filter((e) => e.type === "node:enter")).toHaveLength(3);
96
- expect(events[events.length - 1].type).toBe("workflow:end");
97
- });
98
- it("executes conditional branches", async () => {
99
- const claude = new MockClaude({
100
- workflow: branchingWorkflow,
101
- responses: {
102
- check: { data: { severity: "high" } },
103
- high: { data: { handled: true } },
104
- },
105
- routes: { check: "high" },
106
- });
107
- const { results } = await execute(branchingWorkflow, { alert: "cpu" }, {
108
- skills: createSkillMap([]),
109
- claude,
110
- config: {},
111
- });
112
- expect(results.size).toBe(2);
113
- expect(results.has("check")).toBe(true);
114
- expect(results.has("high")).toBe(true);
115
- expect(results.has("low")).toBe(false);
116
- });
117
- it("routes to alternative branch", async () => {
118
- const claude = new MockClaude({
119
- workflow: branchingWorkflow,
120
- responses: {
121
- check: { data: { severity: "low" } },
122
- low: { data: { skipped: true } },
123
- },
124
- routes: { check: "low" },
125
- });
126
- const { results } = await execute(branchingWorkflow, { alert: "minor" }, {
127
- skills: createSkillMap([]),
128
- claude,
129
- config: {},
130
- });
131
- expect(results.size).toBe(2);
132
- expect(results.has("low")).toBe(true);
133
- expect(results.has("high")).toBe(false);
134
- });
135
- it("emits tool:call and tool:result events", async () => {
136
- const fileSkill = createFileSkill(outputDir);
137
- writeFileSync(path.join(outputDir, "data.json"), '{"key": "value"}');
138
- const claude = new MockClaude({
139
- responses: {
140
- a: {
141
- toolCalls: [{ tool: "fs_read_json", input: { path: "data.json" } }],
142
- data: { read: true },
143
- },
144
- b: { data: {} },
145
- c: { data: {} },
146
- },
147
- });
148
- const events = [];
149
- await execute(linearWorkflow, {}, {
150
- skills: createSkillMap([fileSkill]),
151
- claude,
152
- observer: (e) => events.push(e),
153
- config: {},
154
- });
155
- const toolCalls = events.filter((e) => e.type === "tool:call");
156
- const toolResults = events.filter((e) => e.type === "tool:result");
157
- expect(toolCalls).toHaveLength(1);
158
- expect(toolResults).toHaveLength(1);
159
- expect(toolCalls[0]).toMatchObject({ type: "tool:call", node: "a", tool: "fs_read_json" });
160
- });
161
- it("observer errors do not crash the workflow", async () => {
162
- const claude = new MockClaude({
163
- responses: {
164
- a: { data: {} },
165
- b: { data: {} },
166
- c: { data: {} },
167
- },
168
- });
169
- const { results } = await execute(skilllessWorkflow, {}, {
170
- skills: createSkillMap([]),
171
- claude,
172
- observer: () => {
173
- throw new Error("observer boom");
174
- },
175
- config: {},
176
- });
177
- // Workflow completed despite observer throwing
178
- expect(results.size).toBe(3);
179
- });
180
- it("throws on missing entry node", async () => {
181
- const bad = { ...linearWorkflow, entry: "nonexistent" };
182
- const claude = new MockClaude({ responses: {} });
183
- await expect(execute(bad, {}, { skills: createSkillMap([]), claude, config: {} })).rejects.toThrow("Entry node");
184
- });
185
- it("throws on invalid edge reference", async () => {
186
- const bad = {
187
- ...linearWorkflow,
188
- edges: [
189
- { from: "a", to: "ghost" },
190
- { from: "a", to: "b" },
191
- ],
192
- };
193
- const claude = new MockClaude({ responses: {} });
194
- await expect(execute(bad, {}, { skills: createSkillMap([]), claude, config: {} })).rejects.toThrow("unknown node");
195
- });
196
- it("handles single-node workflow (entry = terminal)", async () => {
197
- const singleNode = {
198
- id: "single",
199
- name: "Single",
200
- description: "",
201
- entry: "only",
202
- nodes: { only: { name: "Only", instruction: "Do it", skills: [] } },
203
- edges: [],
204
- };
205
- const claude = new MockClaude({
206
- responses: { only: { data: { result: "done" } } },
207
- });
208
- const { results } = await execute(singleNode, {}, {
209
- skills: createSkillMap([]),
210
- claude,
211
- config: {},
212
- });
213
- expect(results.size).toBe(1);
214
- expect(results.get("only")?.data.result).toBe("done");
215
- });
216
- it("passes prior node results as context", async () => {
217
- let capturedContext = {};
218
- const mockClaude = {
219
- async run(opts) {
220
- capturedContext = opts.context;
221
- return { status: "success", data: { step: "done" }, toolCalls: [] };
222
- },
223
- async evaluate(opts) {
224
- return opts.choices[0].id;
225
- },
226
- };
227
- const twoStep = {
228
- id: "two",
229
- name: "Two",
230
- description: "",
231
- entry: "first",
232
- nodes: {
233
- first: { name: "First", instruction: "First step", skills: [] },
234
- second: { name: "Second", instruction: "Second step", skills: [] },
235
- },
236
- edges: [{ from: "first", to: "second" }],
237
- };
238
- await execute(twoStep, { original: true }, {
239
- skills: createSkillMap([]),
240
- claude: mockClaude,
241
- config: {},
242
- });
243
- // Second node should have first node's result in context
244
- expect(capturedContext).toHaveProperty("first");
245
- expect(capturedContext).toHaveProperty("input");
246
- });
247
- it("resolves config from env vars and overrides", async () => {
248
- // This tests that config resolution works (skill with required config)
249
- const skillWithConfig = {
250
- id: "needs-config",
251
- name: "Needs Config",
252
- description: "test",
253
- config: {
254
- MY_KEY: { description: "A key", required: true, env: "MY_KEY_ENV" },
255
- },
256
- tools: [],
257
- };
258
- const claude = new MockClaude({
259
- responses: { a: { data: {} }, b: { data: {} }, c: { data: {} } },
260
- });
261
- // Should throw without config (uses skilllessWorkflow — no skill requirements on nodes)
262
- await expect(execute(skilllessWorkflow, {}, {
263
- skills: createSkillMap([skillWithConfig]),
264
- claude,
265
- })).rejects.toThrow("Missing required config");
266
- // Should succeed with override
267
- const { results } = await execute(skilllessWorkflow, {}, {
268
- skills: createSkillMap([skillWithConfig]),
269
- claude,
270
- config: { MY_KEY: "provided" },
271
- });
272
- expect(results.size).toBe(3);
273
- });
274
- it("handles failed node results", async () => {
275
- const claude = new MockClaude({
276
- responses: {
277
- a: { status: "failed", data: { error: "something broke" } },
278
- b: { data: {} },
279
- c: { data: {} },
280
- },
281
- });
282
- const { results } = await execute(skilllessWorkflow, {}, {
283
- skills: createSkillMap([]),
284
- claude,
285
- config: {},
286
- });
287
- // All nodes still execute (executor doesn't short-circuit on failure)
288
- expect(results.get("a")?.status).toBe("failed");
289
- });
290
- it("executes a retry loop with max_iterations", async () => {
291
- const retryWorkflow = {
292
- id: "retry",
293
- name: "Retry",
294
- description: "A→B→A (retry once)→B→C",
295
- entry: "extract",
296
- nodes: {
297
- extract: { name: "Extract", instruction: "Extract data", skills: [] },
298
- judge: { name: "Judge", instruction: "Judge quality", skills: [] },
299
- done: { name: "Done", instruction: "Finish", skills: [] },
300
- },
301
- edges: [
302
- { from: "extract", to: "judge" },
303
- { from: "judge", to: "extract", when: "quality is low", max_iterations: 1 },
304
- { from: "judge", to: "done", when: "quality is high or retries exhausted" },
305
- ],
306
- };
307
- // Track how many times each node runs
308
- let extractCount = 0;
309
- let judgeCount = 0;
310
- const mockClaude = {
311
- async run(opts) {
312
- if (opts.instruction.includes("Extract")) {
313
- extractCount++;
314
- return { status: "success", data: { extracted: true, attempt: extractCount }, toolCalls: [] };
315
- }
316
- if (opts.instruction.includes("Judge")) {
317
- judgeCount++;
318
- // First judge: low quality (triggers retry). Second judge: still low but edge exhausted.
319
- return { status: "success", data: { quality: "low" }, toolCalls: [] };
320
- }
321
- return { status: "success", data: {}, toolCalls: [] };
322
- },
323
- async evaluate(opts) {
324
- // Always try to route to extract (retry), but max_iterations will block it on 2nd attempt
325
- const retryChoice = opts.choices.find((c) => c.id === "extract");
326
- if (retryChoice)
327
- return "extract";
328
- return opts.choices[0].id;
329
- },
330
- };
331
- const { results, trace } = await execute(retryWorkflow, {}, { skills: createSkillMap([]), claude: mockClaude, config: {} });
332
- // extract ran twice (initial + 1 retry), judge ran twice, done ran once
333
- expect(extractCount).toBe(2);
334
- expect(judgeCount).toBe(2);
335
- expect(results.has("done")).toBe(true);
336
- // The second extract result overwrites the first
337
- expect(results.get("extract")?.data.attempt).toBe(2);
338
- // Trace captures the full execution path including loops
339
- expect(trace.steps).toHaveLength(5); // extract, judge, extract(retry), judge, done
340
- expect(trace.steps[0]).toEqual({ node: "extract", status: "success", iteration: 1 });
341
- expect(trace.steps[2]).toEqual({ node: "extract", status: "success", iteration: 2 });
342
- // Trace edges include the retry loop
343
- const retryEdge = trace.edges.find((e) => e.from === "judge" && e.to === "extract");
344
- expect(retryEdge).toBeDefined();
345
- });
346
- it("respects max_iterations=1 as single retry", async () => {
347
- const singleRetry = {
348
- id: "single-retry",
349
- name: "Single Retry",
350
- description: "A↔B with max 1 loop",
351
- entry: "a",
352
- nodes: {
353
- a: { name: "A", instruction: "Step A", skills: [] },
354
- b: { name: "B", instruction: "Step B", skills: [] },
355
- end: { name: "End", instruction: "End", skills: [] },
356
- },
357
- edges: [
358
- { from: "a", to: "b" },
359
- { from: "b", to: "a", when: "retry", max_iterations: 1 },
360
- { from: "b", to: "end", when: "done" },
361
- ],
362
- };
363
- let aCount = 0;
364
- const mockClaude = {
365
- async run(opts) {
366
- if (opts.instruction.includes("Step A"))
367
- aCount++;
368
- return { status: "success", data: {}, toolCalls: [] };
369
- },
370
- async evaluate(opts) {
371
- // Always try to loop back
372
- const retry = opts.choices.find((c) => c.id === "a");
373
- if (retry)
374
- return "a";
375
- return opts.choices[0].id;
376
- },
377
- };
378
- const { results } = await execute(singleRetry, {}, { skills: createSkillMap([]), claude: mockClaude, config: {} });
379
- // A runs twice (once initially, once via retry), then edge is exhausted → falls to "end"
380
- expect(aCount).toBe(2);
381
- expect(results.has("end")).toBe(true);
382
- });
383
- it("throws when a node with required skills is reached but none are configured", async () => {
384
- const workflowWithSkills = {
385
- id: "needs-linear",
386
- name: "Needs Linear",
387
- description: "A→B where B needs linear skill",
388
- entry: "a",
389
- nodes: {
390
- a: { name: "A", instruction: "Do A", skills: [] },
391
- b: { name: "B", instruction: "Create a Linear issue", skills: ["linear"] },
392
- },
393
- edges: [{ from: "a", to: "b" }],
394
- };
395
- const claude = new MockClaude({
396
- responses: {
397
- a: { data: { done: true } },
398
- b: { data: {} },
399
- },
400
- });
401
- // No linear skill in the map — should throw when node B is reached
402
- await expect(execute(workflowWithSkills, {}, { skills: createSkillMap([]), claude, config: {} })).rejects.toThrow('Node "b" requires skills [linear] but none are configured');
403
- });
404
- it("does not throw for unreachable nodes with missing skills", async () => {
405
- const workflowWithUnreachable = {
406
- id: "unreachable-skill",
407
- name: "Unreachable Skill",
408
- description: "A→C, B needs linear but is never reached",
409
- entry: "a",
410
- nodes: {
411
- a: { name: "A", instruction: "Do A", skills: [] },
412
- b: { name: "B", instruction: "Create issue", skills: ["linear"] },
413
- c: { name: "C", instruction: "Do C", skills: [] },
414
- },
415
- edges: [
416
- { from: "a", to: "c" },
417
- { from: "a", to: "b", when: "needs escalation" },
418
- ],
419
- };
420
- const claude = new MockClaude({
421
- responses: {
422
- a: { data: {} },
423
- c: { data: {} },
424
- },
425
- routes: { a: "c" },
426
- });
427
- // Node B is never reached, so missing linear skill should not cause a throw
428
- const { results } = await execute(workflowWithUnreachable, {}, { skills: createSkillMap([]), claude, config: {} });
429
- expect(results.has("a")).toBe(true);
430
- expect(results.has("c")).toBe(true);
431
- expect(results.has("b")).toBe(false);
432
- });
433
- it("validates route choice falls back on invalid Claude response", async () => {
434
- // Create a claude mock that returns an invalid route
435
- const badRouteClaude = {
436
- async run() {
437
- return { status: "success", data: {}, toolCalls: [] };
438
- },
439
- async evaluate() {
440
- return "nonexistent_node"; // invalid target
441
- },
442
- };
443
- const { results } = await execute(branchingWorkflow, {}, {
444
- skills: createSkillMap([]),
445
- claude: badRouteClaude,
446
- config: {},
447
- });
448
- // Should still complete — falls back to first valid edge target
449
- expect(results.size).toBe(2);
450
- });
451
- describe("node.verify", () => {
452
- // Context: a create_issue node can satisfy its output schema (issueIdentifier
453
- // present) while never actually calling the Linear/GitHub create-issue tool.
454
- // The verify block catches this by requiring a real, successful tool call.
455
- const verifyWorkflow = {
456
- id: "verify-create-issue",
457
- name: "Verify Create Issue",
458
- description: "Single node that must call linear_create_issue",
459
- entry: "create",
460
- nodes: {
461
- create: {
462
- name: "Create",
463
- instruction: "Create an issue",
464
- skills: [],
465
- verify: { any_tool_called: ["linear_create_issue", "github_create_issue"] },
466
- },
467
- },
468
- edges: [],
469
- };
470
- it("marks node failed when required tool was never called", async () => {
471
- const claude = {
472
- async run() {
473
- return {
474
- status: "success",
475
- data: { issueIdentifier: "OFF-9999", issueTitle: "hallucinated", issueUrl: "https://" },
476
- toolCalls: [{ tool: "linear_search_issues", input: {}, output: [] }],
477
- };
478
- },
479
- async evaluate(opts) {
480
- return opts.choices[0]?.id;
481
- },
482
- };
483
- const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
484
- const r = results.get("create");
485
- expect(r.status).toBe("failed");
486
- expect(r.data.error).toMatch(/verify failed:.*any_tool_called/s);
487
- expect(r.data.error).toMatch(/linear_create_issue/);
488
- });
489
- it("marks node failed when required tool erred (output.error set)", async () => {
490
- const claude = {
491
- async run() {
492
- return {
493
- status: "success",
494
- data: { issueIdentifier: "OFF-9999" },
495
- toolCalls: [{ tool: "linear_create_issue", input: {}, output: { error: "Linear API 403" } }],
496
- };
497
- },
498
- async evaluate(opts) {
499
- return opts.choices[0]?.id;
500
- },
501
- };
502
- const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
503
- expect(results.get("create").status).toBe("failed");
504
- });
505
- it("passes when a required tool was invoked successfully", async () => {
506
- const claude = {
507
- async run() {
508
- return {
509
- status: "success",
510
- data: { issueIdentifier: "OFF-1234" },
511
- toolCalls: [
512
- {
513
- tool: "linear_create_issue",
514
- input: { title: "t" },
515
- output: { issueCreate: { success: true, issue: { identifier: "OFF-1234" } } },
516
- },
517
- ],
518
- };
519
- },
520
- async evaluate(opts) {
521
- return opts.choices[0]?.id;
522
- },
523
- };
524
- const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
525
- expect(results.get("create").status).toBe("success");
526
- });
527
- it("leaves already-failed nodes alone (does not overwrite Claude errors)", async () => {
528
- const claude = {
529
- async run() {
530
- return { status: "failed", data: { error: "max_turns" }, toolCalls: [] };
531
- },
532
- async evaluate(opts) {
533
- return opts.choices[0]?.id;
534
- },
535
- };
536
- const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
537
- expect(results.get("create").status).toBe("failed");
538
- expect(results.get("create").data.error).toBe("max_turns");
539
- });
540
- it("marks node failed when all_tools_called missing a required tool", async () => {
541
- const wf = {
542
- id: "wf",
543
- name: "wf",
544
- description: "",
545
- entry: "n",
546
- nodes: {
547
- n: {
548
- name: "N",
549
- instruction: "Do",
550
- skills: [],
551
- verify: { all_tools_called: ["a", "b"] },
552
- },
553
- },
554
- edges: [],
555
- };
556
- const claude = {
557
- async run() {
558
- return {
559
- status: "success",
560
- data: {},
561
- toolCalls: [{ tool: "a", input: {}, output: { ok: true } }],
562
- };
563
- },
564
- async evaluate(opts) {
565
- return opts.choices[0]?.id;
566
- },
567
- };
568
- const { results } = await execute(wf, {}, { skills: createSkillMap([]), claude, config: {} });
569
- const r = results.get("n");
570
- expect(r.status).toBe("failed");
571
- expect(r.data.error).toMatch(/verify failed.*all_tools_called.*\[b\]/s);
572
- });
573
- it("marks node failed when no_tool_called was violated", async () => {
574
- const wf = {
575
- id: "wf",
576
- name: "wf",
577
- description: "",
578
- entry: "n",
579
- nodes: {
580
- n: {
581
- name: "N",
582
- instruction: "Do",
583
- skills: [],
584
- verify: { no_tool_called: ["force_push"] },
585
- },
586
- },
587
- edges: [],
588
- };
589
- const claude = {
590
- async run() {
591
- return {
592
- status: "success",
593
- data: {},
594
- toolCalls: [{ tool: "force_push", input: {}, output: { ok: true } }],
595
- };
596
- },
597
- async evaluate(opts) {
598
- return opts.choices[0]?.id;
599
- },
600
- };
601
- const { results } = await execute(wf, {}, { skills: createSkillMap([]), claude, config: {} });
602
- const r = results.get("n");
603
- expect(r.status).toBe("failed");
604
- expect(r.data.error).toMatch(/verify failed.*no_tool_called.*force_push/s);
605
- });
606
- it("marks node failed when output_required is missing", async () => {
607
- const wf = {
608
- id: "wf",
609
- name: "wf",
610
- description: "",
611
- entry: "n",
612
- nodes: {
613
- n: {
614
- name: "N",
615
- instruction: "Do",
616
- skills: [],
617
- verify: { output_required: ["prUrl"] },
618
- },
619
- },
620
- edges: [],
621
- };
622
- const claude = {
623
- async run() {
624
- return { status: "success", data: { branch: "main" }, toolCalls: [] };
625
- },
626
- async evaluate(opts) {
627
- return opts.choices[0]?.id;
628
- },
629
- };
630
- const { results } = await execute(wf, {}, { skills: createSkillMap([]), claude, config: {} });
631
- const r = results.get("n");
632
- expect(r.status).toBe("failed");
633
- expect(r.data.error).toMatch(/verify failed.*output_required.*'prUrl'/s);
634
- });
635
- it("marks node failed when output_matches assertion fails", async () => {
636
- const wf = {
637
- id: "wf",
638
- name: "wf",
639
- description: "",
640
- entry: "n",
641
- nodes: {
642
- n: {
643
- name: "N",
644
- instruction: "Do",
645
- skills: [],
646
- verify: { output_matches: [{ path: "branch", matches: "^sweny/" }] },
647
- },
648
- },
649
- edges: [],
650
- };
651
- const claude = {
652
- async run() {
653
- return { status: "success", data: { branch: "main" }, toolCalls: [] };
654
- },
655
- async evaluate(opts) {
656
- return opts.choices[0]?.id;
657
- },
658
- };
659
- const { results } = await execute(wf, {}, { skills: createSkillMap([]), claude, config: {} });
660
- const r = results.get("n");
661
- expect(r.status).toBe("failed");
662
- expect(r.data.error).toMatch(/verify failed.*output_matches.*'branch'/s);
663
- });
664
- it("aggregates multiple verify failures into one error string", async () => {
665
- const wf = {
666
- id: "wf",
667
- name: "wf",
668
- description: "",
669
- entry: "n",
670
- nodes: {
671
- n: {
672
- name: "N",
673
- instruction: "Do",
674
- skills: [],
675
- verify: {
676
- all_tools_called: ["create_pr"],
677
- output_required: ["prUrl"],
678
- },
679
- },
680
- },
681
- edges: [],
682
- };
683
- const claude = {
684
- async run() {
685
- return { status: "success", data: {}, toolCalls: [] };
686
- },
687
- async evaluate(opts) {
688
- return opts.choices[0]?.id;
689
- },
690
- };
691
- const { results } = await execute(wf, {}, { skills: createSkillMap([]), claude, config: {} });
692
- const r = results.get("n");
693
- expect(r.status).toBe("failed");
694
- expect(r.data.error).toMatch(/all_tools_called/);
695
- expect(r.data.error).toMatch(/output_required/);
696
- });
697
- it("passes a node when every declared check passes", async () => {
698
- const wf = {
699
- id: "wf",
700
- name: "wf",
701
- description: "",
702
- entry: "n",
703
- nodes: {
704
- n: {
705
- name: "N",
706
- instruction: "Do",
707
- skills: [],
708
- verify: {
709
- all_tools_called: ["create_pr"],
710
- no_tool_called: ["force_push"],
711
- output_required: ["prUrl"],
712
- output_matches: [{ path: "prUrl", matches: "^https://" }],
713
- },
714
- },
715
- },
716
- edges: [],
717
- };
718
- const claude = {
719
- async run() {
720
- return {
721
- status: "success",
722
- data: { prUrl: "https://github.com/x/y/pull/1" },
723
- toolCalls: [{ tool: "create_pr", input: {}, output: { ok: true } }],
724
- };
725
- },
726
- async evaluate(opts) {
727
- return opts.choices[0]?.id;
728
- },
729
- };
730
- const { results } = await execute(wf, {}, { skills: createSkillMap([]), claude, config: {} });
731
- const r = results.get("n");
732
- expect(r.status).toBe("success");
733
- expect(r.data.error).toBeUndefined();
734
- });
735
- });
736
- });