@sweny-ai/core 0.1.49 → 0.1.51
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/__tests__/executor.test.js +66 -5
- package/dist/executor.js +8 -0
- package/dist/executor.test.js +3 -1
- package/package.json +1 -1
|
@@ -28,6 +28,17 @@ const linearWorkflow = {
|
|
|
28
28
|
{ from: "b", to: "c" },
|
|
29
29
|
],
|
|
30
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
|
+
};
|
|
31
42
|
const branchingWorkflow = {
|
|
32
43
|
id: "branching",
|
|
33
44
|
name: "Branching",
|
|
@@ -154,7 +165,7 @@ describe("executor", () => {
|
|
|
154
165
|
c: { data: {} },
|
|
155
166
|
},
|
|
156
167
|
});
|
|
157
|
-
const { results } = await execute(
|
|
168
|
+
const { results } = await execute(skilllessWorkflow, {}, {
|
|
158
169
|
skills: createSkillMap([]),
|
|
159
170
|
claude,
|
|
160
171
|
observer: () => {
|
|
@@ -246,13 +257,13 @@ describe("executor", () => {
|
|
|
246
257
|
const claude = new MockClaude({
|
|
247
258
|
responses: { a: { data: {} }, b: { data: {} }, c: { data: {} } },
|
|
248
259
|
});
|
|
249
|
-
// Should throw without config
|
|
250
|
-
await expect(execute(
|
|
260
|
+
// Should throw without config (uses skilllessWorkflow — no skill requirements on nodes)
|
|
261
|
+
await expect(execute(skilllessWorkflow, {}, {
|
|
251
262
|
skills: createSkillMap([skillWithConfig]),
|
|
252
263
|
claude,
|
|
253
264
|
})).rejects.toThrow("Missing required config");
|
|
254
265
|
// Should succeed with override
|
|
255
|
-
const { results } = await execute(
|
|
266
|
+
const { results } = await execute(skilllessWorkflow, {}, {
|
|
256
267
|
skills: createSkillMap([skillWithConfig]),
|
|
257
268
|
claude,
|
|
258
269
|
config: { MY_KEY: "provided" },
|
|
@@ -267,7 +278,7 @@ describe("executor", () => {
|
|
|
267
278
|
c: { data: {} },
|
|
268
279
|
},
|
|
269
280
|
});
|
|
270
|
-
const { results } = await execute(
|
|
281
|
+
const { results } = await execute(skilllessWorkflow, {}, {
|
|
271
282
|
skills: createSkillMap([]),
|
|
272
283
|
claude,
|
|
273
284
|
config: {},
|
|
@@ -368,6 +379,56 @@ describe("executor", () => {
|
|
|
368
379
|
expect(aCount).toBe(2);
|
|
369
380
|
expect(results.has("end")).toBe(true);
|
|
370
381
|
});
|
|
382
|
+
it("throws when a node with required skills is reached but none are configured", async () => {
|
|
383
|
+
const workflowWithSkills = {
|
|
384
|
+
id: "needs-linear",
|
|
385
|
+
name: "Needs Linear",
|
|
386
|
+
description: "A→B where B needs linear skill",
|
|
387
|
+
entry: "a",
|
|
388
|
+
nodes: {
|
|
389
|
+
a: { name: "A", instruction: "Do A", skills: [] },
|
|
390
|
+
b: { name: "B", instruction: "Create a Linear issue", skills: ["linear"] },
|
|
391
|
+
},
|
|
392
|
+
edges: [{ from: "a", to: "b" }],
|
|
393
|
+
};
|
|
394
|
+
const claude = new MockClaude({
|
|
395
|
+
responses: {
|
|
396
|
+
a: { data: { done: true } },
|
|
397
|
+
b: { data: {} },
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
// No linear skill in the map — should throw when node B is reached
|
|
401
|
+
await expect(execute(workflowWithSkills, {}, { skills: createSkillMap([]), claude, config: {} })).rejects.toThrow('Node "b" requires skills [linear] but none are configured');
|
|
402
|
+
});
|
|
403
|
+
it("does not throw for unreachable nodes with missing skills", async () => {
|
|
404
|
+
const workflowWithUnreachable = {
|
|
405
|
+
id: "unreachable-skill",
|
|
406
|
+
name: "Unreachable Skill",
|
|
407
|
+
description: "A→C, B needs linear but is never reached",
|
|
408
|
+
entry: "a",
|
|
409
|
+
nodes: {
|
|
410
|
+
a: { name: "A", instruction: "Do A", skills: [] },
|
|
411
|
+
b: { name: "B", instruction: "Create issue", skills: ["linear"] },
|
|
412
|
+
c: { name: "C", instruction: "Do C", skills: [] },
|
|
413
|
+
},
|
|
414
|
+
edges: [
|
|
415
|
+
{ from: "a", to: "c" },
|
|
416
|
+
{ from: "a", to: "b", when: "needs escalation" },
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
const claude = new MockClaude({
|
|
420
|
+
responses: {
|
|
421
|
+
a: { data: {} },
|
|
422
|
+
c: { data: {} },
|
|
423
|
+
},
|
|
424
|
+
routes: { a: "c" },
|
|
425
|
+
});
|
|
426
|
+
// Node B is never reached, so missing linear skill should not cause a throw
|
|
427
|
+
const { results } = await execute(workflowWithUnreachable, {}, { skills: createSkillMap([]), claude, config: {} });
|
|
428
|
+
expect(results.has("a")).toBe(true);
|
|
429
|
+
expect(results.has("c")).toBe(true);
|
|
430
|
+
expect(results.has("b")).toBe(false);
|
|
431
|
+
});
|
|
371
432
|
it("validates route choice falls back on invalid Claude response", async () => {
|
|
372
433
|
// Create a claude mock that returns an invalid route
|
|
373
434
|
const badRouteClaude = {
|
package/dist/executor.js
CHANGED
|
@@ -41,6 +41,14 @@ export async function execute(workflow, input, options) {
|
|
|
41
41
|
logger.info(`→ ${node.name}`, { node: currentId });
|
|
42
42
|
// Gather tools from the node's skills
|
|
43
43
|
const tools = resolveTools(node.skills, skills);
|
|
44
|
+
// Runtime guard: if this node declares skills but none resolved, the node
|
|
45
|
+
// cannot do its job (e.g. "create a Linear issue" with no linear skill).
|
|
46
|
+
// The startup validate() warns about this possibility, but only throw when
|
|
47
|
+
// the node is actually reached — unreachable nodes with missing skills are fine.
|
|
48
|
+
if (node.skills.length > 0 && tools.length === 0) {
|
|
49
|
+
throw new Error(`Node "${currentId}" requires skills [${node.skills.join(", ")}] but none are configured. ` +
|
|
50
|
+
`Set the required environment variables and try again.`);
|
|
51
|
+
}
|
|
44
52
|
// Build context: input + all prior node results
|
|
45
53
|
const context = {
|
|
46
54
|
input,
|
package/dist/executor.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { rmSync } from "node:fs";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
7
|
import { execute } from "./executor.js";
|
|
@@ -10,8 +11,9 @@ import { validateWorkflow, parseWorkflow } from "./schema.js";
|
|
|
10
11
|
import { triageWorkflow, implementWorkflow } from "./workflows/index.js";
|
|
11
12
|
// ─── Test fixtures ───────────────────────────────────────────────
|
|
12
13
|
const tmpBase = path.join(tmpdir(), "sweny-core-test");
|
|
14
|
+
let dirCounter = 0;
|
|
13
15
|
function freshDir(name) {
|
|
14
|
-
const dir = path.join(tmpBase, `${name}-${Date.now()}`);
|
|
16
|
+
const dir = path.join(tmpBase, `${name}-${Date.now()}-${++dirCounter}-${randomBytes(4).toString("hex")}`);
|
|
15
17
|
rmSync(dir, { recursive: true, force: true });
|
|
16
18
|
mkdirSync(dir, { recursive: true });
|
|
17
19
|
return dir;
|