@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.
@@ -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
- const nodes = Object.entries(workflow.nodes).map(([id, node]) => ({
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.length ?? 0,
74
+ toolCount: skill?.tools?.length ?? 0,
67
75
  };
68
76
  }),
69
77
  exec: { status: "pending" },
70
78
  },
71
79
  }));
72
- const edges = workflow.edges.map((edge) => ({
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.33",
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 + DAG workflow orchestration powered by Claude",
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",