@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,526 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { workflowZ, nodeZ, nodeVerifyZ, 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("edgeZ", () => {
|
|
129
|
-
it("parses a simple edge", () => {
|
|
130
|
-
const result = edgeZ.parse({ from: "a", to: "b" });
|
|
131
|
-
expect(result.from).toBe("a");
|
|
132
|
-
expect(result.when).toBeUndefined();
|
|
133
|
-
});
|
|
134
|
-
it("parses an edge with condition", () => {
|
|
135
|
-
const result = edgeZ.parse({ from: "a", to: "b", when: "severity is high" });
|
|
136
|
-
expect(result.when).toBe("severity is high");
|
|
137
|
-
});
|
|
138
|
-
it("rejects empty from", () => {
|
|
139
|
-
expect(() => edgeZ.parse({ from: "", to: "b" })).toThrow();
|
|
140
|
-
});
|
|
141
|
-
it("rejects empty to", () => {
|
|
142
|
-
expect(() => edgeZ.parse({ from: "a", to: "" })).toThrow();
|
|
143
|
-
});
|
|
144
|
-
it("parses an edge with max_iterations", () => {
|
|
145
|
-
const result = edgeZ.parse({ from: "a", to: "b", when: "needs retry", max_iterations: 3 });
|
|
146
|
-
expect(result.max_iterations).toBe(3);
|
|
147
|
-
});
|
|
148
|
-
it("rejects max_iterations less than 1", () => {
|
|
149
|
-
expect(() => edgeZ.parse({ from: "a", to: "b", max_iterations: 0 })).toThrow();
|
|
150
|
-
});
|
|
151
|
-
it("rejects non-integer max_iterations", () => {
|
|
152
|
-
expect(() => edgeZ.parse({ from: "a", to: "b", max_iterations: 1.5 })).toThrow();
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
describe("toolZ", () => {
|
|
156
|
-
it("parses a valid tool", () => {
|
|
157
|
-
const result = toolZ.parse({
|
|
158
|
-
name: "my_tool",
|
|
159
|
-
description: "Does things",
|
|
160
|
-
input_schema: { type: "object", properties: {} },
|
|
161
|
-
});
|
|
162
|
-
expect(result.name).toBe("my_tool");
|
|
163
|
-
});
|
|
164
|
-
it("rejects empty name", () => {
|
|
165
|
-
expect(() => toolZ.parse({ name: "", description: "x", input_schema: {} })).toThrow();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
describe("skillZ", () => {
|
|
169
|
-
it("parses a valid skill", () => {
|
|
170
|
-
const result = skillZ.parse({
|
|
171
|
-
id: "test",
|
|
172
|
-
name: "Test",
|
|
173
|
-
description: "A test skill",
|
|
174
|
-
category: "general",
|
|
175
|
-
config: {
|
|
176
|
-
TOKEN: { description: "API token", required: true, env: "TOKEN" },
|
|
177
|
-
},
|
|
178
|
-
tools: [{ name: "t", description: "d", input_schema: {} }],
|
|
179
|
-
});
|
|
180
|
-
expect(result.id).toBe("test");
|
|
181
|
-
expect(result.tools).toHaveLength(1);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
describe("mcpServerConfigZ", () => {
|
|
185
|
-
it("accepts stdio config", () => {
|
|
186
|
-
const result = mcpServerConfigZ.parse({ command: "npx", args: ["-y", "@company/tool-server"] });
|
|
187
|
-
expect(result.command).toBe("npx");
|
|
188
|
-
});
|
|
189
|
-
it("accepts http config", () => {
|
|
190
|
-
const result = mcpServerConfigZ.parse({
|
|
191
|
-
url: "https://mcp.example.com",
|
|
192
|
-
headers: { Authorization: "Bearer token" },
|
|
193
|
-
});
|
|
194
|
-
expect(result.url).toBe("https://mcp.example.com");
|
|
195
|
-
});
|
|
196
|
-
it("accepts explicit type", () => {
|
|
197
|
-
const result = mcpServerConfigZ.parse({ type: "stdio", command: "node", args: ["server.js"] });
|
|
198
|
-
expect(result.type).toBe("stdio");
|
|
199
|
-
});
|
|
200
|
-
it("accepts env field", () => {
|
|
201
|
-
const result = mcpServerConfigZ.parse({
|
|
202
|
-
command: "npx",
|
|
203
|
-
args: ["-y", "server"],
|
|
204
|
-
env: { API_KEY: "Company API key" },
|
|
205
|
-
});
|
|
206
|
-
expect(result.env).toEqual({ API_KEY: "Company API key" });
|
|
207
|
-
});
|
|
208
|
-
it("rejects config with neither command nor url", () => {
|
|
209
|
-
expect(() => mcpServerConfigZ.parse({ args: ["foo"] })).toThrow("MCP server must have either command (stdio) or url (HTTP)");
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
describe("skillDefinitionZ", () => {
|
|
213
|
-
it("accepts instruction-only skill", () => {
|
|
214
|
-
const result = skillDefinitionZ.parse({
|
|
215
|
-
name: "Code Standards",
|
|
216
|
-
instruction: "Follow our coding conventions...",
|
|
217
|
-
});
|
|
218
|
-
expect(result.instruction).toBe("Follow our coding conventions...");
|
|
219
|
-
});
|
|
220
|
-
it("accepts mcp-only skill", () => {
|
|
221
|
-
const result = skillDefinitionZ.parse({ mcp: { command: "npx", args: ["-y", "server"] } });
|
|
222
|
-
expect(result.mcp?.command).toBe("npx");
|
|
223
|
-
});
|
|
224
|
-
it("accepts both instruction and mcp", () => {
|
|
225
|
-
const result = skillDefinitionZ.parse({
|
|
226
|
-
instruction: "Use this server for...",
|
|
227
|
-
mcp: { url: "https://mcp.example.com" },
|
|
228
|
-
});
|
|
229
|
-
expect(result.instruction).toBeDefined();
|
|
230
|
-
expect(result.mcp).toBeDefined();
|
|
231
|
-
});
|
|
232
|
-
it("rejects skill with neither instruction nor mcp", () => {
|
|
233
|
-
expect(() => skillDefinitionZ.parse({ name: "Empty" })).toThrow("Inline skill must provide instruction, mcp, or both");
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
describe("skillZ with new fields", () => {
|
|
237
|
-
it("accepts skill with instruction and no tools", () => {
|
|
238
|
-
const result = skillZ.parse({
|
|
239
|
-
id: "code-standards",
|
|
240
|
-
name: "Code Standards",
|
|
241
|
-
description: "Team coding conventions",
|
|
242
|
-
category: "general",
|
|
243
|
-
instruction: "Follow camelCase naming...",
|
|
244
|
-
});
|
|
245
|
-
expect(result.tools).toEqual([]);
|
|
246
|
-
expect(result.instruction).toBe("Follow camelCase naming...");
|
|
247
|
-
});
|
|
248
|
-
it("accepts skill with mcp and no tools", () => {
|
|
249
|
-
const result = skillZ.parse({
|
|
250
|
-
id: "our-crm",
|
|
251
|
-
name: "Our CRM",
|
|
252
|
-
description: "CRM integration",
|
|
253
|
-
category: "general",
|
|
254
|
-
mcp: { url: "https://crm.example.com/mcp" },
|
|
255
|
-
});
|
|
256
|
-
expect(result.tools).toEqual([]);
|
|
257
|
-
expect(result.mcp?.url).toBe("https://crm.example.com/mcp");
|
|
258
|
-
});
|
|
259
|
-
it("rejects skill with no tools, no instruction, no mcp", () => {
|
|
260
|
-
expect(() => skillZ.parse({ id: "empty", name: "Empty", description: "Nothing", category: "general" })).toThrow("Skill must provide at least one of: tools, instruction, or mcp");
|
|
261
|
-
});
|
|
262
|
-
it("defaults tools to empty array", () => {
|
|
263
|
-
const result = skillZ.parse({
|
|
264
|
-
id: "x",
|
|
265
|
-
name: "X",
|
|
266
|
-
description: "X",
|
|
267
|
-
category: "general",
|
|
268
|
-
instruction: "do stuff",
|
|
269
|
-
});
|
|
270
|
-
expect(result.tools).toEqual([]);
|
|
271
|
-
});
|
|
272
|
-
it("defaults config to empty object", () => {
|
|
273
|
-
const result = skillZ.parse({
|
|
274
|
-
id: "x",
|
|
275
|
-
name: "X",
|
|
276
|
-
description: "X",
|
|
277
|
-
category: "general",
|
|
278
|
-
instruction: "do stuff",
|
|
279
|
-
});
|
|
280
|
-
expect(result.config).toEqual({});
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
describe("workflowZ with skills", () => {
|
|
284
|
-
it("parses workflow with inline skills", () => {
|
|
285
|
-
const result = workflowZ.parse({
|
|
286
|
-
id: "test",
|
|
287
|
-
name: "Test",
|
|
288
|
-
entry: "a",
|
|
289
|
-
nodes: { a: { name: "A", instruction: "Do A", skills: ["my-skill"] } },
|
|
290
|
-
edges: [],
|
|
291
|
-
skills: { "my-skill": { name: "My Skill", instruction: "Custom guidance here" } },
|
|
292
|
-
});
|
|
293
|
-
expect(result.skills?.["my-skill"]?.instruction).toBe("Custom guidance here");
|
|
294
|
-
});
|
|
295
|
-
it("defaults skills to empty object", () => {
|
|
296
|
-
const result = workflowZ.parse({
|
|
297
|
-
id: "x",
|
|
298
|
-
name: "X",
|
|
299
|
-
entry: "a",
|
|
300
|
-
nodes: { a: { name: "A", instruction: "Do A" } },
|
|
301
|
-
edges: [],
|
|
302
|
-
});
|
|
303
|
-
expect(result.skills).toEqual({});
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
describe("workflowZ", () => {
|
|
307
|
-
it("parses a valid workflow", () => {
|
|
308
|
-
const raw = JSON.parse(JSON.stringify(validWorkflow));
|
|
309
|
-
const result = workflowZ.parse(raw);
|
|
310
|
-
expect(result.id).toBe("test-wf");
|
|
311
|
-
expect(Object.keys(result.nodes)).toHaveLength(3);
|
|
312
|
-
});
|
|
313
|
-
it("defaults description to empty string", () => {
|
|
314
|
-
const raw = {
|
|
315
|
-
id: "x",
|
|
316
|
-
name: "X",
|
|
317
|
-
entry: "a",
|
|
318
|
-
nodes: { a: { name: "A", instruction: "Do A" } },
|
|
319
|
-
edges: [],
|
|
320
|
-
};
|
|
321
|
-
const result = workflowZ.parse(raw);
|
|
322
|
-
expect(result.description).toBe("");
|
|
323
|
-
});
|
|
324
|
-
it("rejects missing id", () => {
|
|
325
|
-
expect(() => workflowZ.parse({ name: "x", entry: "a", nodes: {}, edges: [] })).toThrow();
|
|
326
|
-
});
|
|
327
|
-
it("rejects missing entry", () => {
|
|
328
|
-
expect(() => workflowZ.parse({ id: "x", name: "x", nodes: {}, edges: [] })).toThrow();
|
|
329
|
-
});
|
|
330
|
-
it("rejects empty id", () => {
|
|
331
|
-
expect(() => workflowZ.parse({ id: "", name: "x", entry: "a", nodes: {}, edges: [] })).toThrow();
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
describe("parseWorkflow", () => {
|
|
335
|
-
it("parses a raw JSON workflow", () => {
|
|
336
|
-
const raw = JSON.parse(JSON.stringify(validWorkflow));
|
|
337
|
-
const parsed = parseWorkflow(raw);
|
|
338
|
-
expect(parsed.id).toBe("test-wf");
|
|
339
|
-
expect(parsed.nodes["a"].instruction).toBe("Do A");
|
|
340
|
-
});
|
|
341
|
-
it("rejects invalid input", () => {
|
|
342
|
-
expect(() => parseWorkflow({ id: "" })).toThrow();
|
|
343
|
-
expect(() => parseWorkflow(null)).toThrow();
|
|
344
|
-
expect(() => parseWorkflow("not an object")).toThrow();
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
// ─── Structural validation tests ─────────────────────────────────
|
|
349
|
-
describe("validateWorkflow", () => {
|
|
350
|
-
it("validates a correct workflow", () => {
|
|
351
|
-
expect(validateWorkflow(validWorkflow)).toEqual([]);
|
|
352
|
-
});
|
|
353
|
-
it("detects missing entry node", () => {
|
|
354
|
-
const bad = { ...validWorkflow, entry: "nonexistent" };
|
|
355
|
-
const errors = validateWorkflow(bad);
|
|
356
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "MISSING_ENTRY" }));
|
|
357
|
-
});
|
|
358
|
-
it("detects unknown edge source", () => {
|
|
359
|
-
const bad = {
|
|
360
|
-
...validWorkflow,
|
|
361
|
-
edges: [{ from: "ghost", to: "b" }],
|
|
362
|
-
};
|
|
363
|
-
const errors = validateWorkflow(bad);
|
|
364
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "UNKNOWN_EDGE_SOURCE" }));
|
|
365
|
-
});
|
|
366
|
-
it("detects unknown edge target", () => {
|
|
367
|
-
const bad = {
|
|
368
|
-
...validWorkflow,
|
|
369
|
-
edges: [{ from: "a", to: "ghost" }],
|
|
370
|
-
};
|
|
371
|
-
const errors = validateWorkflow(bad);
|
|
372
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "UNKNOWN_EDGE_TARGET" }));
|
|
373
|
-
});
|
|
374
|
-
it("detects self-loops without max_iterations", () => {
|
|
375
|
-
const loopy = {
|
|
376
|
-
...validWorkflow,
|
|
377
|
-
edges: [
|
|
378
|
-
{ from: "a", to: "a" },
|
|
379
|
-
{ from: "a", to: "b" },
|
|
380
|
-
{ from: "b", to: "c" },
|
|
381
|
-
],
|
|
382
|
-
};
|
|
383
|
-
const errors = validateWorkflow(loopy);
|
|
384
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "SELF_LOOP", nodeId: "a" }));
|
|
385
|
-
});
|
|
386
|
-
it("allows self-loops with max_iterations", () => {
|
|
387
|
-
const loopy = {
|
|
388
|
-
...validWorkflow,
|
|
389
|
-
edges: [
|
|
390
|
-
{ from: "a", to: "a", when: "needs retry", max_iterations: 2 },
|
|
391
|
-
{ from: "a", to: "b" },
|
|
392
|
-
{ from: "b", to: "c" },
|
|
393
|
-
],
|
|
394
|
-
};
|
|
395
|
-
const errors = validateWorkflow(loopy);
|
|
396
|
-
expect(errors).toEqual([]);
|
|
397
|
-
});
|
|
398
|
-
it("detects unbounded multi-node cycles", () => {
|
|
399
|
-
const cyclic = {
|
|
400
|
-
...validWorkflow,
|
|
401
|
-
edges: [
|
|
402
|
-
{ from: "a", to: "b" },
|
|
403
|
-
{ from: "b", to: "a", when: "retry" }, // cycle without max_iterations
|
|
404
|
-
{ from: "b", to: "c" },
|
|
405
|
-
],
|
|
406
|
-
};
|
|
407
|
-
const errors = validateWorkflow(cyclic);
|
|
408
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "UNBOUNDED_CYCLE" }));
|
|
409
|
-
});
|
|
410
|
-
it("allows multi-node cycles with max_iterations", () => {
|
|
411
|
-
const cyclic = {
|
|
412
|
-
...validWorkflow,
|
|
413
|
-
edges: [
|
|
414
|
-
{ from: "a", to: "b" },
|
|
415
|
-
{ from: "b", to: "a", when: "retry", max_iterations: 3 },
|
|
416
|
-
{ from: "b", to: "c" },
|
|
417
|
-
],
|
|
418
|
-
};
|
|
419
|
-
const errors = validateWorkflow(cyclic);
|
|
420
|
-
expect(errors).toEqual([]);
|
|
421
|
-
});
|
|
422
|
-
it("detects unreachable nodes", () => {
|
|
423
|
-
const disconnected = {
|
|
424
|
-
...validWorkflow,
|
|
425
|
-
edges: [{ from: "a", to: "b" }],
|
|
426
|
-
// c has no incoming edge from the reachable set
|
|
427
|
-
};
|
|
428
|
-
const errors = validateWorkflow(disconnected);
|
|
429
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "UNREACHABLE_NODE", nodeId: "c" }));
|
|
430
|
-
});
|
|
431
|
-
it("detects unknown skill references", () => {
|
|
432
|
-
const errors = validateWorkflow(validWorkflow, new Set(["not-github"]));
|
|
433
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "UNKNOWN_SKILL", nodeId: "a" }));
|
|
434
|
-
});
|
|
435
|
-
it("passes when known skills match", () => {
|
|
436
|
-
const errors = validateWorkflow(validWorkflow, new Set(["github"]));
|
|
437
|
-
expect(errors).toEqual([]);
|
|
438
|
-
});
|
|
439
|
-
it("validates triage workflow", () => {
|
|
440
|
-
expect(validateWorkflow(triageWorkflow)).toEqual([]);
|
|
441
|
-
});
|
|
442
|
-
it("validates implement workflow", () => {
|
|
443
|
-
expect(validateWorkflow(implementWorkflow)).toEqual([]);
|
|
444
|
-
});
|
|
445
|
-
it("handles empty nodes object", () => {
|
|
446
|
-
const empty = {
|
|
447
|
-
id: "empty",
|
|
448
|
-
name: "Empty",
|
|
449
|
-
description: "",
|
|
450
|
-
entry: "x",
|
|
451
|
-
nodes: {},
|
|
452
|
-
edges: [],
|
|
453
|
-
};
|
|
454
|
-
const errors = validateWorkflow(empty);
|
|
455
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "MISSING_ENTRY" }));
|
|
456
|
-
});
|
|
457
|
-
it("recognizes inline workflow skills as known", () => {
|
|
458
|
-
const wf = {
|
|
459
|
-
...validWorkflow,
|
|
460
|
-
nodes: { ...validWorkflow.nodes, a: { ...validWorkflow.nodes.a, skills: ["custom-rubric"] } },
|
|
461
|
-
skills: { "custom-rubric": { instruction: "Score things 1-5" } },
|
|
462
|
-
};
|
|
463
|
-
const errors = validateWorkflow(wf, new Set(["github"]));
|
|
464
|
-
expect(errors.filter((e) => e.code === "UNKNOWN_SKILL")).toEqual([]);
|
|
465
|
-
});
|
|
466
|
-
it("rejects inline skills with neither instruction nor mcp", () => {
|
|
467
|
-
const wf = {
|
|
468
|
-
...validWorkflow,
|
|
469
|
-
skills: { "bad-skill": { name: "Bad" } },
|
|
470
|
-
};
|
|
471
|
-
const errors = validateWorkflow(wf);
|
|
472
|
-
expect(errors).toContainEqual(expect.objectContaining({ code: "INVALID_INLINE_SKILL" }));
|
|
473
|
-
});
|
|
474
|
-
it("accepts inline skills with instruction only", () => {
|
|
475
|
-
const wf = {
|
|
476
|
-
...validWorkflow,
|
|
477
|
-
skills: { "ok-skill": { instruction: "Do something" } },
|
|
478
|
-
};
|
|
479
|
-
const errors = validateWorkflow(wf);
|
|
480
|
-
expect(errors.filter((e) => e.code === "INVALID_INLINE_SKILL")).toEqual([]);
|
|
481
|
-
});
|
|
482
|
-
it("accepts inline skills with mcp only", () => {
|
|
483
|
-
const wf = {
|
|
484
|
-
...validWorkflow,
|
|
485
|
-
skills: { "mcp-skill": { mcp: { command: "npx", args: ["-y", "server"] } } },
|
|
486
|
-
};
|
|
487
|
-
const errors = validateWorkflow(wf);
|
|
488
|
-
expect(errors.filter((e) => e.code === "INVALID_INLINE_SKILL")).toEqual([]);
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
// ─── JSON Schema shape tests ─────────────────────────────────────
|
|
492
|
-
describe("workflowJsonSchema", () => {
|
|
493
|
-
it("has the correct $id", () => {
|
|
494
|
-
expect(workflowJsonSchema.$id).toBe("https://spec.sweny.ai/schemas/workflow.json");
|
|
495
|
-
});
|
|
496
|
-
it("requires all top-level fields", () => {
|
|
497
|
-
expect(workflowJsonSchema.required).toContain("id");
|
|
498
|
-
expect(workflowJsonSchema.required).toContain("name");
|
|
499
|
-
expect(workflowJsonSchema.required).toContain("nodes");
|
|
500
|
-
expect(workflowJsonSchema.required).toContain("edges");
|
|
501
|
-
expect(workflowJsonSchema.required).toContain("entry");
|
|
502
|
-
});
|
|
503
|
-
it("has additionalProperties: false at top level", () => {
|
|
504
|
-
expect(workflowJsonSchema.additionalProperties).toBe(false);
|
|
505
|
-
});
|
|
506
|
-
it("has additionalProperties: false on edge items", () => {
|
|
507
|
-
expect(workflowJsonSchema.properties.edges.items.additionalProperties).toBe(false);
|
|
508
|
-
});
|
|
509
|
-
it("has additionalProperties: false on node items", () => {
|
|
510
|
-
expect(workflowJsonSchema.properties.nodes.additionalProperties.additionalProperties).toBe(false);
|
|
511
|
-
});
|
|
512
|
-
it("edge items require from and to", () => {
|
|
513
|
-
expect(workflowJsonSchema.properties.edges.items.required).toContain("from");
|
|
514
|
-
expect(workflowJsonSchema.properties.edges.items.required).toContain("to");
|
|
515
|
-
});
|
|
516
|
-
it("edge items include max_iterations", () => {
|
|
517
|
-
const edgeProps = workflowJsonSchema.properties.edges.items.properties;
|
|
518
|
-
expect(edgeProps.max_iterations).toBeDefined();
|
|
519
|
-
expect(edgeProps.max_iterations.type).toBe("integer");
|
|
520
|
-
expect(edgeProps.max_iterations.minimum).toBe(1);
|
|
521
|
-
});
|
|
522
|
-
it("node items require name and instruction", () => {
|
|
523
|
-
expect(workflowJsonSchema.properties.nodes.additionalProperties.required).toContain("name");
|
|
524
|
-
expect(workflowJsonSchema.properties.nodes.additionalProperties.required).toContain("instruction");
|
|
525
|
-
});
|
|
526
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|