@sweny-ai/core 0.1.33 → 0.1.35
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__/studio.test.js +73 -0
- package/dist/studio.js +14 -6
- package/package.json +2 -2
|
@@ -65,6 +65,79 @@ describe("workflowToFlow", () => {
|
|
|
65
65
|
expect(node.data.exec.status).toBe("pending");
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
|
+
// Defensive behavior: streaming LLM YAML (and user-authored
|
|
69
|
+
// workflows) routinely arrive with fields omitted. The viewer
|
|
70
|
+
// must not crash on these — it should render an empty state for
|
|
71
|
+
// the missing pieces.
|
|
72
|
+
describe("defensive handling of partial workflows", () => {
|
|
73
|
+
it("treats a node with undefined skills as having no skills", () => {
|
|
74
|
+
const partial = {
|
|
75
|
+
id: "partial",
|
|
76
|
+
name: "Partial",
|
|
77
|
+
description: "",
|
|
78
|
+
entry: "a",
|
|
79
|
+
nodes: {
|
|
80
|
+
// Cast to unknown→Node: we're deliberately simulating
|
|
81
|
+
// what a partial YAML parse produces at runtime.
|
|
82
|
+
a: { name: "A", instruction: "Do A" },
|
|
83
|
+
},
|
|
84
|
+
edges: [],
|
|
85
|
+
};
|
|
86
|
+
expect(() => workflowToFlow(partial)).not.toThrow();
|
|
87
|
+
const { nodes } = workflowToFlow(partial);
|
|
88
|
+
expect(nodes[0].data.skills).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
it("treats a workflow with undefined edges as having no edges", () => {
|
|
91
|
+
const partial = {
|
|
92
|
+
id: "partial",
|
|
93
|
+
name: "Partial",
|
|
94
|
+
description: "",
|
|
95
|
+
entry: "a",
|
|
96
|
+
nodes: {
|
|
97
|
+
a: { name: "A", instruction: "Do A", skills: [] },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
expect(() => workflowToFlow(partial)).not.toThrow();
|
|
101
|
+
const { nodes, edges } = workflowToFlow(partial);
|
|
102
|
+
expect(edges).toEqual([]);
|
|
103
|
+
// The sole node is still terminal because there are no outgoing edges.
|
|
104
|
+
expect(nodes[0].data.isTerminal).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
it("treats a workflow with undefined nodes as having no nodes", () => {
|
|
107
|
+
const partial = {
|
|
108
|
+
id: "partial",
|
|
109
|
+
name: "Partial",
|
|
110
|
+
description: "",
|
|
111
|
+
entry: "a",
|
|
112
|
+
edges: [],
|
|
113
|
+
};
|
|
114
|
+
expect(() => workflowToFlow(partial)).not.toThrow();
|
|
115
|
+
const { nodes, edges } = workflowToFlow(partial);
|
|
116
|
+
expect(nodes).toEqual([]);
|
|
117
|
+
expect(edges).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
it("treats a skill catalog entry with missing tools as toolCount=0", () => {
|
|
120
|
+
const brokenSkill = {
|
|
121
|
+
id: "broken",
|
|
122
|
+
name: "Broken",
|
|
123
|
+
description: "No tools array",
|
|
124
|
+
category: "general",
|
|
125
|
+
config: {},
|
|
126
|
+
// tools omitted on purpose
|
|
127
|
+
};
|
|
128
|
+
const wf = {
|
|
129
|
+
id: "t",
|
|
130
|
+
name: "T",
|
|
131
|
+
description: "",
|
|
132
|
+
entry: "a",
|
|
133
|
+
nodes: { a: { name: "A", instruction: "Do A", skills: ["broken"] } },
|
|
134
|
+
edges: [],
|
|
135
|
+
};
|
|
136
|
+
expect(() => workflowToFlow(wf, [brokenSkill])).not.toThrow();
|
|
137
|
+
const { nodes } = workflowToFlow(wf, [brokenSkill]);
|
|
138
|
+
expect(nodes[0].data.skills[0].toolCount).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
68
141
|
});
|
|
69
142
|
describe("flowToWorkflow", () => {
|
|
70
143
|
it("round-trips correctly", () => {
|
package/dist/studio.js
CHANGED
|
@@ -49,7 +49,15 @@ import { builtinSkills } from "./skills/index.js";
|
|
|
49
49
|
export function workflowToFlow(workflow, skillCatalog = builtinSkills) {
|
|
50
50
|
const skillMap = new Map(skillCatalog.map((s) => [s.id, s]));
|
|
51
51
|
const terminalIds = findTerminals(workflow);
|
|
52
|
-
|
|
52
|
+
// Defensive: streaming LLM YAML (or user-authored partial workflows)
|
|
53
|
+
// can produce nodes with missing `skills` / workflows with missing
|
|
54
|
+
// `edges`. The types say these are required, but runtime data from
|
|
55
|
+
// untrusted sources frequently violates that. Rather than crash the
|
|
56
|
+
// viewer with "Cannot read properties of undefined (reading 'map')",
|
|
57
|
+
// treat them as empty and let the caller decide what to render.
|
|
58
|
+
const workflowNodes = workflow.nodes ?? {};
|
|
59
|
+
const workflowEdges = workflow.edges ?? [];
|
|
60
|
+
const nodes = Object.entries(workflowNodes).map(([id, node]) => ({
|
|
53
61
|
id,
|
|
54
62
|
type: "skillNode",
|
|
55
63
|
position: { x: 0, y: 0 }, // ELK will compute real positions
|
|
@@ -58,18 +66,18 @@ export function workflowToFlow(workflow, skillCatalog = builtinSkills) {
|
|
|
58
66
|
node,
|
|
59
67
|
isEntry: id === workflow.entry,
|
|
60
68
|
isTerminal: terminalIds.has(id),
|
|
61
|
-
skills: node.skills.map((sid) => {
|
|
69
|
+
skills: (node.skills ?? []).map((sid) => {
|
|
62
70
|
const skill = skillMap.get(sid);
|
|
63
71
|
return {
|
|
64
72
|
id: sid,
|
|
65
73
|
name: skill?.name ?? sid,
|
|
66
|
-
toolCount: skill?.tools
|
|
74
|
+
toolCount: skill?.tools?.length ?? 0,
|
|
67
75
|
};
|
|
68
76
|
}),
|
|
69
77
|
exec: { status: "pending" },
|
|
70
78
|
},
|
|
71
79
|
}));
|
|
72
|
-
const edges =
|
|
80
|
+
const edges = workflowEdges.map((edge) => ({
|
|
73
81
|
id: `${edge.from}--${edge.to}`,
|
|
74
82
|
source: edge.from,
|
|
75
83
|
target: edge.to,
|
|
@@ -143,8 +151,8 @@ export function flowToWorkflow(meta, nodes, edges) {
|
|
|
143
151
|
}
|
|
144
152
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|
145
153
|
function findTerminals(workflow) {
|
|
146
|
-
const hasOutgoing = new Set(workflow.edges.map((e) => e.from));
|
|
147
|
-
return new Set(Object.keys(workflow.nodes).filter((id) => !hasOutgoing.has(id)));
|
|
154
|
+
const hasOutgoing = new Set((workflow.edges ?? []).map((e) => e.from));
|
|
155
|
+
return new Set(Object.keys(workflow.nodes ?? {}).filter((id) => !hasOutgoing.has(id)));
|
|
148
156
|
}
|
|
149
157
|
/**
|
|
150
158
|
* Generate a TypeScript file from a Workflow definition.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweny-ai/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sweny": "./dist/cli/main.js"
|
|
7
7
|
},
|
|
8
|
-
"description": "Skill library
|
|
8
|
+
"description": "Skill library, DAG workflow orchestration, and E2E browser testing powered by Claude",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./dist/index.js",
|