@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.
- package/dist/browser.d.ts +11 -5
- package/dist/browser.js +11 -11
- package/dist/claude.d.ts +20 -0
- package/dist/claude.js +139 -9
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.js +56 -9
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +56 -57
- package/dist/cli/marketplace.js +35 -9
- package/dist/cli/publish.d.ts +10 -0
- package/dist/cli/publish.js +22 -14
- package/dist/executor.js +104 -43
- package/dist/loader.d.ts +40 -0
- package/dist/loader.js +73 -0
- package/dist/mcp-catalog.d.ts +47 -0
- package/dist/mcp-catalog.js +304 -0
- package/dist/mcp.d.ts +6 -9
- package/dist/mcp.js +24 -314
- package/dist/requires.d.ts +7 -0
- package/dist/requires.js +34 -0
- package/dist/retry.d.ts +23 -0
- package/dist/retry.js +72 -0
- package/dist/schema.d.ts +667 -80
- package/dist/schema.js +144 -5
- package/dist/skills/custom-loader.d.ts +40 -0
- package/dist/skills/custom-loader.js +107 -32
- package/dist/skills/index.d.ts +13 -1
- package/dist/skills/index.js +12 -3
- package/dist/studio.js +4 -1
- package/dist/templates.d.ts +29 -8
- package/dist/templates.js +30 -20
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +7 -0
- package/dist/types.d.ts +76 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +43 -5
- package/package.json +8 -4
- package/dist/__tests__/cascade.test.d.ts +0 -11
- package/dist/__tests__/cascade.test.js +0 -449
- package/dist/__tests__/claude.test.d.ts +0 -1
- package/dist/__tests__/claude.test.js +0 -427
- package/dist/__tests__/custom-loader.test.d.ts +0 -1
- package/dist/__tests__/custom-loader.test.js +0 -116
- package/dist/__tests__/executor.test.d.ts +0 -1
- package/dist/__tests__/executor.test.js +0 -736
- package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/datadog.integration.test.js +0 -23
- package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
- package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/github.integration.test.js +0 -37
- package/dist/__tests__/integration/harness.d.ts +0 -24
- package/dist/__tests__/integration/harness.js +0 -34
- package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/linear.integration.test.js +0 -15
- package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sentry.integration.test.js +0 -20
- package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/slack.integration.test.js +0 -22
- package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sources.integration.test.js +0 -123
- package/dist/__tests__/publish.test.d.ts +0 -1
- package/dist/__tests__/publish.test.js +0 -180
- package/dist/__tests__/schema.test.d.ts +0 -1
- package/dist/__tests__/schema.test.js +0 -526
- package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
- package/dist/__tests__/skill-config-integration.test.js +0 -204
- package/dist/__tests__/skills-index.test.d.ts +0 -1
- package/dist/__tests__/skills-index.test.js +0 -119
- package/dist/__tests__/skills.test.d.ts +0 -1
- package/dist/__tests__/skills.test.js +0 -565
- package/dist/__tests__/spec-conformance.test.d.ts +0 -18
- package/dist/__tests__/spec-conformance.test.js +0 -1074
- package/dist/__tests__/studio.test.d.ts +0 -1
- package/dist/__tests__/studio.test.js +0 -403
- package/dist/__tests__/templates.test.d.ts +0 -1
- package/dist/__tests__/templates.test.js +0 -49
- package/dist/__tests__/testing.test.d.ts +0 -1
- package/dist/__tests__/testing.test.js +0 -224
- package/dist/__tests__/verify.test.d.ts +0 -1
- package/dist/__tests__/verify.test.js +0 -517
- package/dist/__tests__/version-consistency.test.d.ts +0 -1
- package/dist/__tests__/version-consistency.test.js +0 -312
- package/dist/__tests__/workflow-builder.test.d.ts +0 -1
- package/dist/__tests__/workflow-builder.test.js +0 -110
- package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
- package/dist/__tests__/workflow-yaml.test.js +0 -201
- package/dist/cli/__tests__/diagram.test.d.ts +0 -1
- package/dist/cli/__tests__/diagram.test.js +0 -225
- package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
- package/dist/cli/__tests__/report-cloud.test.js +0 -165
- package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
- package/dist/cli/__tests__/upgrade.test.js +0 -317
- package/dist/cli/__tests__/version-check.test.d.ts +0 -1
- package/dist/cli/__tests__/version-check.test.js +0 -209
- package/dist/cli/config.test.d.ts +0 -1
- package/dist/cli/config.test.js +0 -200
- package/dist/cli/credentials.test.d.ts +0 -1
- package/dist/cli/credentials.test.js +0 -172
- package/dist/cli/e2e.test.d.ts +0 -1
- package/dist/cli/e2e.test.js +0 -690
- package/dist/cli/marketplace.test.d.ts +0 -1
- package/dist/cli/marketplace.test.js +0 -355
- package/dist/cli/new.test.d.ts +0 -1
- package/dist/cli/new.test.js +0 -1043
- package/dist/cli/output.test.d.ts +0 -1
- package/dist/cli/output.test.js +0 -180
- package/dist/cli/publish.test.d.ts +0 -1
- package/dist/cli/publish.test.js +0 -610
- package/dist/cli/renderer.test.d.ts +0 -1
- package/dist/cli/renderer.test.js +0 -302
- package/dist/executor.test.d.ts +0 -1
- package/dist/executor.test.js +0 -603
- package/dist/mcp.test.d.ts +0 -1
- package/dist/mcp.test.js +0 -735
- package/dist/mermaid.test.d.ts +0 -1
- package/dist/mermaid.test.js +0 -256
- package/dist/sources.test.d.ts +0 -1
- package/dist/sources.test.js +0 -265
- package/dist/sources.validation.test.d.ts +0 -12
- package/dist/sources.validation.test.js +0 -380
- package/dist/workflow-builder.test.d.ts +0 -1
- 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
|
-
});
|