@sweny-ai/core 0.1.77 → 0.1.79

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 (119) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +16 -0
  4. package/dist/claude.js +98 -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/loader.d.ts +40 -0
  13. package/dist/loader.js +73 -0
  14. package/dist/mcp-catalog.d.ts +47 -0
  15. package/dist/mcp-catalog.js +304 -0
  16. package/dist/mcp.d.ts +6 -9
  17. package/dist/mcp.js +24 -314
  18. package/dist/schema.d.ts +183 -151
  19. package/dist/schema.js +49 -7
  20. package/dist/skills/custom-loader.d.ts +40 -0
  21. package/dist/skills/custom-loader.js +107 -32
  22. package/dist/skills/index.d.ts +13 -1
  23. package/dist/skills/index.js +12 -3
  24. package/dist/studio.js +4 -1
  25. package/dist/templates.d.ts +29 -8
  26. package/dist/templates.js +30 -20
  27. package/dist/types.d.ts +12 -0
  28. package/dist/verify.js +96 -7
  29. package/package.json +8 -4
  30. package/dist/__tests__/cascade.test.d.ts +0 -11
  31. package/dist/__tests__/cascade.test.js +0 -452
  32. package/dist/__tests__/claude.test.d.ts +0 -1
  33. package/dist/__tests__/claude.test.js +0 -427
  34. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  35. package/dist/__tests__/custom-loader.test.js +0 -116
  36. package/dist/__tests__/executor.test.d.ts +0 -1
  37. package/dist/__tests__/executor.test.js +0 -1635
  38. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  39. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  40. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  41. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  42. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  43. package/dist/__tests__/integration/github.integration.test.js +0 -37
  44. package/dist/__tests__/integration/harness.d.ts +0 -24
  45. package/dist/__tests__/integration/harness.js +0 -34
  46. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  48. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  50. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  52. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  53. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  54. package/dist/__tests__/publish.test.d.ts +0 -1
  55. package/dist/__tests__/publish.test.js +0 -180
  56. package/dist/__tests__/requires.test.d.ts +0 -1
  57. package/dist/__tests__/requires.test.js +0 -82
  58. package/dist/__tests__/retry.test.d.ts +0 -1
  59. package/dist/__tests__/retry.test.js +0 -213
  60. package/dist/__tests__/schema.test.d.ts +0 -1
  61. package/dist/__tests__/schema.test.js +0 -600
  62. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  63. package/dist/__tests__/skill-config-integration.test.js +0 -204
  64. package/dist/__tests__/skills-index.test.d.ts +0 -1
  65. package/dist/__tests__/skills-index.test.js +0 -119
  66. package/dist/__tests__/skills.test.d.ts +0 -1
  67. package/dist/__tests__/skills.test.js +0 -565
  68. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  69. package/dist/__tests__/spec-conformance.test.js +0 -1084
  70. package/dist/__tests__/studio.test.d.ts +0 -1
  71. package/dist/__tests__/studio.test.js +0 -403
  72. package/dist/__tests__/templates.test.d.ts +0 -1
  73. package/dist/__tests__/templates.test.js +0 -49
  74. package/dist/__tests__/testing.test.d.ts +0 -1
  75. package/dist/__tests__/testing.test.js +0 -242
  76. package/dist/__tests__/verify.test.d.ts +0 -1
  77. package/dist/__tests__/verify.test.js +0 -517
  78. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  79. package/dist/__tests__/version-consistency.test.js +0 -312
  80. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  81. package/dist/__tests__/workflow-builder.test.js +0 -110
  82. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  83. package/dist/__tests__/workflow-yaml.test.js +0 -201
  84. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  85. package/dist/cli/__tests__/diagram.test.js +0 -225
  86. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  87. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  88. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  89. package/dist/cli/__tests__/upgrade.test.js +0 -317
  90. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  91. package/dist/cli/__tests__/version-check.test.js +0 -209
  92. package/dist/cli/config.test.d.ts +0 -1
  93. package/dist/cli/config.test.js +0 -200
  94. package/dist/cli/credentials.test.d.ts +0 -1
  95. package/dist/cli/credentials.test.js +0 -172
  96. package/dist/cli/e2e.test.d.ts +0 -1
  97. package/dist/cli/e2e.test.js +0 -690
  98. package/dist/cli/marketplace.test.d.ts +0 -1
  99. package/dist/cli/marketplace.test.js +0 -355
  100. package/dist/cli/new.test.d.ts +0 -1
  101. package/dist/cli/new.test.js +0 -1043
  102. package/dist/cli/output.test.d.ts +0 -1
  103. package/dist/cli/output.test.js +0 -180
  104. package/dist/cli/publish.test.d.ts +0 -1
  105. package/dist/cli/publish.test.js +0 -610
  106. package/dist/cli/renderer.test.d.ts +0 -1
  107. package/dist/cli/renderer.test.js +0 -302
  108. package/dist/executor.test.d.ts +0 -1
  109. package/dist/executor.test.js +0 -618
  110. package/dist/mcp.test.d.ts +0 -1
  111. package/dist/mcp.test.js +0 -735
  112. package/dist/mermaid.test.d.ts +0 -1
  113. package/dist/mermaid.test.js +0 -256
  114. package/dist/sources.test.d.ts +0 -1
  115. package/dist/sources.test.js +0 -265
  116. package/dist/sources.validation.test.d.ts +0 -12
  117. package/dist/sources.validation.test.js +0 -380
  118. package/dist/workflow-builder.test.d.ts +0 -1
  119. package/dist/workflow-builder.test.js +0 -123
@@ -1,600 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { workflowZ, nodeZ, nodeVerifyZ, nodeRequiresZ, nodeRetryZ, edgeZ, skillZ, toolZ, mcpServerConfigZ, skillDefinitionZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "../schema.js";
3
- import { triageWorkflow, implementWorkflow } from "../workflows/index.js";
4
- // ─── Test fixtures ───────────────────────────────────────────────
5
- const validWorkflow = {
6
- id: "test-wf",
7
- name: "Test Workflow",
8
- description: "A→B→C",
9
- entry: "a",
10
- nodes: {
11
- a: { name: "A", instruction: "Do A", skills: ["github"] },
12
- b: { name: "B", instruction: "Do B", skills: [] },
13
- c: { name: "C", instruction: "Do C", skills: [] },
14
- },
15
- edges: [
16
- { from: "a", to: "b" },
17
- { from: "b", to: "c" },
18
- ],
19
- };
20
- // ─── Zod schema tests ────────────────────────────────────────────
21
- describe("Zod schemas", () => {
22
- describe("nodeZ", () => {
23
- it("parses a valid node", () => {
24
- const result = nodeZ.parse({ name: "Step", instruction: "Do it", skills: ["github"] });
25
- expect(result.name).toBe("Step");
26
- expect(result.skills).toEqual(["github"]);
27
- });
28
- it("defaults skills to empty array", () => {
29
- const result = nodeZ.parse({ name: "Step", instruction: "Do it" });
30
- expect(result.skills).toEqual([]);
31
- });
32
- it("rejects empty name", () => {
33
- expect(() => nodeZ.parse({ name: "", instruction: "x" })).toThrow();
34
- });
35
- it("rejects empty instruction", () => {
36
- expect(() => nodeZ.parse({ name: "x", instruction: "" })).toThrow();
37
- });
38
- it("accepts optional output schema", () => {
39
- const result = nodeZ.parse({
40
- name: "S",
41
- instruction: "I",
42
- output: { type: "object", properties: { severity: { type: "string" } } },
43
- });
44
- expect(result.output).toBeDefined();
45
- });
46
- it("accepts a verify block with any_tool_called", () => {
47
- const result = nodeZ.parse({
48
- name: "S",
49
- instruction: "I",
50
- verify: { any_tool_called: ["linear_create_issue", "github_create_issue"] },
51
- });
52
- expect(result.verify?.any_tool_called).toEqual(["linear_create_issue", "github_create_issue"]);
53
- });
54
- it("rejects verify with no check declared", () => {
55
- expect(() => nodeZ.parse({ name: "S", instruction: "I", verify: {} })).toThrow();
56
- });
57
- it("rejects verify with empty any_tool_called", () => {
58
- expect(() => nodeZ.parse({ name: "S", instruction: "I", verify: { any_tool_called: [] } })).toThrow();
59
- });
60
- it("accepts all_tools_called", () => {
61
- const v = nodeVerifyZ.parse({ all_tools_called: ["a", "b"] });
62
- expect(v.all_tools_called).toEqual(["a", "b"]);
63
- });
64
- it("accepts no_tool_called", () => {
65
- const v = nodeVerifyZ.parse({ no_tool_called: ["force_push"] });
66
- expect(v.no_tool_called).toEqual(["force_push"]);
67
- });
68
- it("accepts output_required", () => {
69
- const v = nodeVerifyZ.parse({ output_required: ["prUrl", "branch"] });
70
- expect(v.output_required).toEqual(["prUrl", "branch"]);
71
- });
72
- it("accepts output_matches with each operator", () => {
73
- const v = nodeVerifyZ.parse({
74
- output_matches: [
75
- { path: "a", equals: 1 },
76
- { path: "b", in: ["x", "y"] },
77
- { path: "c", matches: "^foo" },
78
- ],
79
- });
80
- expect(v.output_matches).toHaveLength(3);
81
- });
82
- it("accepts a combination of multiple checks", () => {
83
- const v = nodeVerifyZ.parse({
84
- any_tool_called: ["a"],
85
- output_required: ["x"],
86
- output_matches: [{ path: "x", equals: 1 }],
87
- });
88
- expect(v.any_tool_called).toBeDefined();
89
- expect(v.output_required).toBeDefined();
90
- expect(v.output_matches).toBeDefined();
91
- });
92
- it("rejects an output_matches entry with zero operators", () => {
93
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "a" }] })).toThrow();
94
- });
95
- it("rejects an output_matches entry with two operators", () => {
96
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "a", equals: 1, in: [1, 2] }] })).toThrow();
97
- });
98
- it("rejects an output_matches entry with equals + matches", () => {
99
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "a", equals: 1, matches: "^x$" }] })).toThrow();
100
- });
101
- it("rejects an output_matches entry with in + matches", () => {
102
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "a", in: [1, 2], matches: "^x$" }] })).toThrow();
103
- });
104
- it("rejects an output_matches entry with all three operators", () => {
105
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "a", equals: 1, in: [1], matches: "^x$" }] })).toThrow();
106
- });
107
- it("accepts equals: null as a valid operator value", () => {
108
- // null IS a valid value to assert equality against; only `undefined` means
109
- // "not set". Schema must distinguish.
110
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "a", equals: null }] })).not.toThrow();
111
- });
112
- it("rejects an output_matches entry with empty path", () => {
113
- expect(() => nodeVerifyZ.parse({ output_matches: [{ path: "", equals: 1 }] })).toThrow();
114
- });
115
- it("rejects empty output_required", () => {
116
- expect(() => nodeVerifyZ.parse({ output_required: [] })).toThrow();
117
- });
118
- it("rejects empty all_tools_called", () => {
119
- expect(() => nodeVerifyZ.parse({ all_tools_called: [] })).toThrow();
120
- });
121
- it("rejects empty no_tool_called", () => {
122
- expect(() => nodeVerifyZ.parse({ no_tool_called: [] })).toThrow();
123
- });
124
- it("rejects empty output_matches array", () => {
125
- expect(() => nodeVerifyZ.parse({ output_matches: [] })).toThrow();
126
- });
127
- });
128
- describe("nodeRequiresZ", () => {
129
- it("accepts output_required only", () => {
130
- expect(() => nodeRequiresZ.parse({ output_required: ["input.x"] })).not.toThrow();
131
- });
132
- it("accepts output_matches only", () => {
133
- expect(() => nodeRequiresZ.parse({ output_matches: [{ path: "input.x", equals: 1 }] })).not.toThrow();
134
- });
135
- it("accepts on_fail: 'fail'", () => {
136
- expect(() => nodeRequiresZ.parse({ output_required: ["input.x"], on_fail: "fail" })).not.toThrow();
137
- });
138
- it("accepts on_fail: 'skip'", () => {
139
- expect(() => nodeRequiresZ.parse({ output_required: ["input.x"], on_fail: "skip" })).not.toThrow();
140
- });
141
- it("rejects empty requires (no checks declared)", () => {
142
- expect(() => nodeRequiresZ.parse({})).toThrow();
143
- });
144
- it("rejects on_fail other than 'fail' or 'skip'", () => {
145
- expect(() => nodeRequiresZ.parse({ output_required: ["input.x"], on_fail: "throw" })).toThrow();
146
- });
147
- it("rejects empty output_required array", () => {
148
- expect(() => nodeRequiresZ.parse({ output_required: [] })).toThrow();
149
- });
150
- it("nodeZ accepts a node with requires", () => {
151
- expect(() => nodeZ.parse({
152
- name: "Test",
153
- instruction: "Do thing",
154
- skills: [],
155
- requires: { output_required: ["input.x"] },
156
- })).not.toThrow();
157
- });
158
- });
159
- describe("nodeRetryZ", () => {
160
- it("accepts max alone", () => {
161
- expect(() => nodeRetryZ.parse({ max: 2 })).not.toThrow();
162
- });
163
- it("accepts max + string instruction", () => {
164
- expect(() => nodeRetryZ.parse({ max: 1, instruction: "Try harder" })).not.toThrow();
165
- });
166
- it("accepts max + { auto: true }", () => {
167
- expect(() => nodeRetryZ.parse({ max: 1, instruction: { auto: true } })).not.toThrow();
168
- });
169
- it("accepts max + { reflect: '...' }", () => {
170
- expect(() => nodeRetryZ.parse({ max: 1, instruction: { reflect: "Focus on tool calls" } })).not.toThrow();
171
- });
172
- it("rejects max: 0", () => {
173
- expect(() => nodeRetryZ.parse({ max: 0 })).toThrow();
174
- });
175
- it("rejects negative max", () => {
176
- expect(() => nodeRetryZ.parse({ max: -1 })).toThrow();
177
- });
178
- it("rejects non-integer max", () => {
179
- expect(() => nodeRetryZ.parse({ max: 1.5 })).toThrow();
180
- });
181
- it("rejects { auto: false }", () => {
182
- expect(() => nodeRetryZ.parse({ max: 1, instruction: { auto: false } })).toThrow();
183
- });
184
- it("rejects empty reflect string", () => {
185
- expect(() => nodeRetryZ.parse({ max: 1, instruction: { reflect: "" } })).toThrow();
186
- });
187
- it("rejects instruction with both auto and reflect", () => {
188
- expect(() => nodeRetryZ.parse({ max: 1, instruction: { auto: true, reflect: "x" } })).toThrow();
189
- });
190
- it("requires max field", () => {
191
- expect(() => nodeRetryZ.parse({ instruction: "x" })).toThrow();
192
- });
193
- it("nodeZ accepts a node with retry", () => {
194
- expect(() => nodeZ.parse({
195
- name: "Test",
196
- instruction: "Do thing",
197
- skills: [],
198
- retry: { max: 2, instruction: { auto: true } },
199
- })).not.toThrow();
200
- });
201
- });
202
- describe("edgeZ", () => {
203
- it("parses a simple edge", () => {
204
- const result = edgeZ.parse({ from: "a", to: "b" });
205
- expect(result.from).toBe("a");
206
- expect(result.when).toBeUndefined();
207
- });
208
- it("parses an edge with condition", () => {
209
- const result = edgeZ.parse({ from: "a", to: "b", when: "severity is high" });
210
- expect(result.when).toBe("severity is high");
211
- });
212
- it("rejects empty from", () => {
213
- expect(() => edgeZ.parse({ from: "", to: "b" })).toThrow();
214
- });
215
- it("rejects empty to", () => {
216
- expect(() => edgeZ.parse({ from: "a", to: "" })).toThrow();
217
- });
218
- it("parses an edge with max_iterations", () => {
219
- const result = edgeZ.parse({ from: "a", to: "b", when: "needs retry", max_iterations: 3 });
220
- expect(result.max_iterations).toBe(3);
221
- });
222
- it("rejects max_iterations less than 1", () => {
223
- expect(() => edgeZ.parse({ from: "a", to: "b", max_iterations: 0 })).toThrow();
224
- });
225
- it("rejects non-integer max_iterations", () => {
226
- expect(() => edgeZ.parse({ from: "a", to: "b", max_iterations: 1.5 })).toThrow();
227
- });
228
- });
229
- describe("toolZ", () => {
230
- it("parses a valid tool", () => {
231
- const result = toolZ.parse({
232
- name: "my_tool",
233
- description: "Does things",
234
- input_schema: { type: "object", properties: {} },
235
- });
236
- expect(result.name).toBe("my_tool");
237
- });
238
- it("rejects empty name", () => {
239
- expect(() => toolZ.parse({ name: "", description: "x", input_schema: {} })).toThrow();
240
- });
241
- });
242
- describe("skillZ", () => {
243
- it("parses a valid skill", () => {
244
- const result = skillZ.parse({
245
- id: "test",
246
- name: "Test",
247
- description: "A test skill",
248
- category: "general",
249
- config: {
250
- TOKEN: { description: "API token", required: true, env: "TOKEN" },
251
- },
252
- tools: [{ name: "t", description: "d", input_schema: {} }],
253
- });
254
- expect(result.id).toBe("test");
255
- expect(result.tools).toHaveLength(1);
256
- });
257
- });
258
- describe("mcpServerConfigZ", () => {
259
- it("accepts stdio config", () => {
260
- const result = mcpServerConfigZ.parse({ command: "npx", args: ["-y", "@company/tool-server"] });
261
- expect(result.command).toBe("npx");
262
- });
263
- it("accepts http config", () => {
264
- const result = mcpServerConfigZ.parse({
265
- url: "https://mcp.example.com",
266
- headers: { Authorization: "Bearer token" },
267
- });
268
- expect(result.url).toBe("https://mcp.example.com");
269
- });
270
- it("accepts explicit type", () => {
271
- const result = mcpServerConfigZ.parse({ type: "stdio", command: "node", args: ["server.js"] });
272
- expect(result.type).toBe("stdio");
273
- });
274
- it("accepts env field", () => {
275
- const result = mcpServerConfigZ.parse({
276
- command: "npx",
277
- args: ["-y", "server"],
278
- env: { API_KEY: "Company API key" },
279
- });
280
- expect(result.env).toEqual({ API_KEY: "Company API key" });
281
- });
282
- it("rejects config with neither command nor url", () => {
283
- expect(() => mcpServerConfigZ.parse({ args: ["foo"] })).toThrow("MCP server must have either command (stdio) or url (HTTP)");
284
- });
285
- });
286
- describe("skillDefinitionZ", () => {
287
- it("accepts instruction-only skill", () => {
288
- const result = skillDefinitionZ.parse({
289
- name: "Code Standards",
290
- instruction: "Follow our coding conventions...",
291
- });
292
- expect(result.instruction).toBe("Follow our coding conventions...");
293
- });
294
- it("accepts mcp-only skill", () => {
295
- const result = skillDefinitionZ.parse({ mcp: { command: "npx", args: ["-y", "server"] } });
296
- expect(result.mcp?.command).toBe("npx");
297
- });
298
- it("accepts both instruction and mcp", () => {
299
- const result = skillDefinitionZ.parse({
300
- instruction: "Use this server for...",
301
- mcp: { url: "https://mcp.example.com" },
302
- });
303
- expect(result.instruction).toBeDefined();
304
- expect(result.mcp).toBeDefined();
305
- });
306
- it("rejects skill with neither instruction nor mcp", () => {
307
- expect(() => skillDefinitionZ.parse({ name: "Empty" })).toThrow("Inline skill must provide instruction, mcp, or both");
308
- });
309
- });
310
- describe("skillZ with new fields", () => {
311
- it("accepts skill with instruction and no tools", () => {
312
- const result = skillZ.parse({
313
- id: "code-standards",
314
- name: "Code Standards",
315
- description: "Team coding conventions",
316
- category: "general",
317
- instruction: "Follow camelCase naming...",
318
- });
319
- expect(result.tools).toEqual([]);
320
- expect(result.instruction).toBe("Follow camelCase naming...");
321
- });
322
- it("accepts skill with mcp and no tools", () => {
323
- const result = skillZ.parse({
324
- id: "our-crm",
325
- name: "Our CRM",
326
- description: "CRM integration",
327
- category: "general",
328
- mcp: { url: "https://crm.example.com/mcp" },
329
- });
330
- expect(result.tools).toEqual([]);
331
- expect(result.mcp?.url).toBe("https://crm.example.com/mcp");
332
- });
333
- it("rejects skill with no tools, no instruction, no mcp", () => {
334
- expect(() => skillZ.parse({ id: "empty", name: "Empty", description: "Nothing", category: "general" })).toThrow("Skill must provide at least one of: tools, instruction, or mcp");
335
- });
336
- it("defaults tools to empty array", () => {
337
- const result = skillZ.parse({
338
- id: "x",
339
- name: "X",
340
- description: "X",
341
- category: "general",
342
- instruction: "do stuff",
343
- });
344
- expect(result.tools).toEqual([]);
345
- });
346
- it("defaults config to empty object", () => {
347
- const result = skillZ.parse({
348
- id: "x",
349
- name: "X",
350
- description: "X",
351
- category: "general",
352
- instruction: "do stuff",
353
- });
354
- expect(result.config).toEqual({});
355
- });
356
- });
357
- describe("workflowZ with skills", () => {
358
- it("parses workflow with inline skills", () => {
359
- const result = workflowZ.parse({
360
- id: "test",
361
- name: "Test",
362
- entry: "a",
363
- nodes: { a: { name: "A", instruction: "Do A", skills: ["my-skill"] } },
364
- edges: [],
365
- skills: { "my-skill": { name: "My Skill", instruction: "Custom guidance here" } },
366
- });
367
- expect(result.skills?.["my-skill"]?.instruction).toBe("Custom guidance here");
368
- });
369
- it("defaults skills to empty object", () => {
370
- const result = workflowZ.parse({
371
- id: "x",
372
- name: "X",
373
- entry: "a",
374
- nodes: { a: { name: "A", instruction: "Do A" } },
375
- edges: [],
376
- });
377
- expect(result.skills).toEqual({});
378
- });
379
- });
380
- describe("workflowZ", () => {
381
- it("parses a valid workflow", () => {
382
- const raw = JSON.parse(JSON.stringify(validWorkflow));
383
- const result = workflowZ.parse(raw);
384
- expect(result.id).toBe("test-wf");
385
- expect(Object.keys(result.nodes)).toHaveLength(3);
386
- });
387
- it("defaults description to empty string", () => {
388
- const raw = {
389
- id: "x",
390
- name: "X",
391
- entry: "a",
392
- nodes: { a: { name: "A", instruction: "Do A" } },
393
- edges: [],
394
- };
395
- const result = workflowZ.parse(raw);
396
- expect(result.description).toBe("");
397
- });
398
- it("rejects missing id", () => {
399
- expect(() => workflowZ.parse({ name: "x", entry: "a", nodes: {}, edges: [] })).toThrow();
400
- });
401
- it("rejects missing entry", () => {
402
- expect(() => workflowZ.parse({ id: "x", name: "x", nodes: {}, edges: [] })).toThrow();
403
- });
404
- it("rejects empty id", () => {
405
- expect(() => workflowZ.parse({ id: "", name: "x", entry: "a", nodes: {}, edges: [] })).toThrow();
406
- });
407
- });
408
- describe("parseWorkflow", () => {
409
- it("parses a raw JSON workflow", () => {
410
- const raw = JSON.parse(JSON.stringify(validWorkflow));
411
- const parsed = parseWorkflow(raw);
412
- expect(parsed.id).toBe("test-wf");
413
- expect(parsed.nodes["a"].instruction).toBe("Do A");
414
- });
415
- it("rejects invalid input", () => {
416
- expect(() => parseWorkflow({ id: "" })).toThrow();
417
- expect(() => parseWorkflow(null)).toThrow();
418
- expect(() => parseWorkflow("not an object")).toThrow();
419
- });
420
- });
421
- });
422
- // ─── Structural validation tests ─────────────────────────────────
423
- describe("validateWorkflow", () => {
424
- it("validates a correct workflow", () => {
425
- expect(validateWorkflow(validWorkflow)).toEqual([]);
426
- });
427
- it("detects missing entry node", () => {
428
- const bad = { ...validWorkflow, entry: "nonexistent" };
429
- const errors = validateWorkflow(bad);
430
- expect(errors).toContainEqual(expect.objectContaining({ code: "MISSING_ENTRY" }));
431
- });
432
- it("detects unknown edge source", () => {
433
- const bad = {
434
- ...validWorkflow,
435
- edges: [{ from: "ghost", to: "b" }],
436
- };
437
- const errors = validateWorkflow(bad);
438
- expect(errors).toContainEqual(expect.objectContaining({ code: "UNKNOWN_EDGE_SOURCE" }));
439
- });
440
- it("detects unknown edge target", () => {
441
- const bad = {
442
- ...validWorkflow,
443
- edges: [{ from: "a", to: "ghost" }],
444
- };
445
- const errors = validateWorkflow(bad);
446
- expect(errors).toContainEqual(expect.objectContaining({ code: "UNKNOWN_EDGE_TARGET" }));
447
- });
448
- it("detects self-loops without max_iterations", () => {
449
- const loopy = {
450
- ...validWorkflow,
451
- edges: [
452
- { from: "a", to: "a" },
453
- { from: "a", to: "b" },
454
- { from: "b", to: "c" },
455
- ],
456
- };
457
- const errors = validateWorkflow(loopy);
458
- expect(errors).toContainEqual(expect.objectContaining({ code: "SELF_LOOP", nodeId: "a" }));
459
- });
460
- it("allows self-loops with max_iterations", () => {
461
- const loopy = {
462
- ...validWorkflow,
463
- edges: [
464
- { from: "a", to: "a", when: "needs retry", max_iterations: 2 },
465
- { from: "a", to: "b" },
466
- { from: "b", to: "c" },
467
- ],
468
- };
469
- const errors = validateWorkflow(loopy);
470
- expect(errors).toEqual([]);
471
- });
472
- it("detects unbounded multi-node cycles", () => {
473
- const cyclic = {
474
- ...validWorkflow,
475
- edges: [
476
- { from: "a", to: "b" },
477
- { from: "b", to: "a", when: "retry" }, // cycle without max_iterations
478
- { from: "b", to: "c" },
479
- ],
480
- };
481
- const errors = validateWorkflow(cyclic);
482
- expect(errors).toContainEqual(expect.objectContaining({ code: "UNBOUNDED_CYCLE" }));
483
- });
484
- it("allows multi-node cycles with max_iterations", () => {
485
- const cyclic = {
486
- ...validWorkflow,
487
- edges: [
488
- { from: "a", to: "b" },
489
- { from: "b", to: "a", when: "retry", max_iterations: 3 },
490
- { from: "b", to: "c" },
491
- ],
492
- };
493
- const errors = validateWorkflow(cyclic);
494
- expect(errors).toEqual([]);
495
- });
496
- it("detects unreachable nodes", () => {
497
- const disconnected = {
498
- ...validWorkflow,
499
- edges: [{ from: "a", to: "b" }],
500
- // c has no incoming edge from the reachable set
501
- };
502
- const errors = validateWorkflow(disconnected);
503
- expect(errors).toContainEqual(expect.objectContaining({ code: "UNREACHABLE_NODE", nodeId: "c" }));
504
- });
505
- it("detects unknown skill references", () => {
506
- const errors = validateWorkflow(validWorkflow, new Set(["not-github"]));
507
- expect(errors).toContainEqual(expect.objectContaining({ code: "UNKNOWN_SKILL", nodeId: "a" }));
508
- });
509
- it("passes when known skills match", () => {
510
- const errors = validateWorkflow(validWorkflow, new Set(["github"]));
511
- expect(errors).toEqual([]);
512
- });
513
- it("validates triage workflow", () => {
514
- expect(validateWorkflow(triageWorkflow)).toEqual([]);
515
- });
516
- it("validates implement workflow", () => {
517
- expect(validateWorkflow(implementWorkflow)).toEqual([]);
518
- });
519
- it("handles empty nodes object", () => {
520
- const empty = {
521
- id: "empty",
522
- name: "Empty",
523
- description: "",
524
- entry: "x",
525
- nodes: {},
526
- edges: [],
527
- };
528
- const errors = validateWorkflow(empty);
529
- expect(errors).toContainEqual(expect.objectContaining({ code: "MISSING_ENTRY" }));
530
- });
531
- it("recognizes inline workflow skills as known", () => {
532
- const wf = {
533
- ...validWorkflow,
534
- nodes: { ...validWorkflow.nodes, a: { ...validWorkflow.nodes.a, skills: ["custom-rubric"] } },
535
- skills: { "custom-rubric": { instruction: "Score things 1-5" } },
536
- };
537
- const errors = validateWorkflow(wf, new Set(["github"]));
538
- expect(errors.filter((e) => e.code === "UNKNOWN_SKILL")).toEqual([]);
539
- });
540
- it("rejects inline skills with neither instruction nor mcp", () => {
541
- const wf = {
542
- ...validWorkflow,
543
- skills: { "bad-skill": { name: "Bad" } },
544
- };
545
- const errors = validateWorkflow(wf);
546
- expect(errors).toContainEqual(expect.objectContaining({ code: "INVALID_INLINE_SKILL" }));
547
- });
548
- it("accepts inline skills with instruction only", () => {
549
- const wf = {
550
- ...validWorkflow,
551
- skills: { "ok-skill": { instruction: "Do something" } },
552
- };
553
- const errors = validateWorkflow(wf);
554
- expect(errors.filter((e) => e.code === "INVALID_INLINE_SKILL")).toEqual([]);
555
- });
556
- it("accepts inline skills with mcp only", () => {
557
- const wf = {
558
- ...validWorkflow,
559
- skills: { "mcp-skill": { mcp: { command: "npx", args: ["-y", "server"] } } },
560
- };
561
- const errors = validateWorkflow(wf);
562
- expect(errors.filter((e) => e.code === "INVALID_INLINE_SKILL")).toEqual([]);
563
- });
564
- });
565
- // ─── JSON Schema shape tests ─────────────────────────────────────
566
- describe("workflowJsonSchema", () => {
567
- it("has the correct $id", () => {
568
- expect(workflowJsonSchema.$id).toBe("https://spec.sweny.ai/schemas/workflow.json");
569
- });
570
- it("requires all top-level fields", () => {
571
- expect(workflowJsonSchema.required).toContain("id");
572
- expect(workflowJsonSchema.required).toContain("name");
573
- expect(workflowJsonSchema.required).toContain("nodes");
574
- expect(workflowJsonSchema.required).toContain("edges");
575
- expect(workflowJsonSchema.required).toContain("entry");
576
- });
577
- it("has additionalProperties: false at top level", () => {
578
- expect(workflowJsonSchema.additionalProperties).toBe(false);
579
- });
580
- it("has additionalProperties: false on edge items", () => {
581
- expect(workflowJsonSchema.properties.edges.items.additionalProperties).toBe(false);
582
- });
583
- it("has additionalProperties: false on node items", () => {
584
- expect(workflowJsonSchema.properties.nodes.additionalProperties.additionalProperties).toBe(false);
585
- });
586
- it("edge items require from and to", () => {
587
- expect(workflowJsonSchema.properties.edges.items.required).toContain("from");
588
- expect(workflowJsonSchema.properties.edges.items.required).toContain("to");
589
- });
590
- it("edge items include max_iterations", () => {
591
- const edgeProps = workflowJsonSchema.properties.edges.items.properties;
592
- expect(edgeProps.max_iterations).toBeDefined();
593
- expect(edgeProps.max_iterations.type).toBe("integer");
594
- expect(edgeProps.max_iterations.minimum).toBe(1);
595
- });
596
- it("node items require name and instruction", () => {
597
- expect(workflowJsonSchema.properties.nodes.additionalProperties.required).toContain("name");
598
- expect(workflowJsonSchema.properties.nodes.additionalProperties.required).toContain("instruction");
599
- });
600
- });
@@ -1 +0,0 @@
1
- export {};