@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.
Files changed (123) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +20 -0
  4. package/dist/claude.js +139 -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/executor.js +104 -43
  13. package/dist/loader.d.ts +40 -0
  14. package/dist/loader.js +73 -0
  15. package/dist/mcp-catalog.d.ts +47 -0
  16. package/dist/mcp-catalog.js +304 -0
  17. package/dist/mcp.d.ts +6 -9
  18. package/dist/mcp.js +24 -314
  19. package/dist/requires.d.ts +7 -0
  20. package/dist/requires.js +34 -0
  21. package/dist/retry.d.ts +23 -0
  22. package/dist/retry.js +72 -0
  23. package/dist/schema.d.ts +667 -80
  24. package/dist/schema.js +144 -5
  25. package/dist/skills/custom-loader.d.ts +40 -0
  26. package/dist/skills/custom-loader.js +107 -32
  27. package/dist/skills/index.d.ts +13 -1
  28. package/dist/skills/index.js +12 -3
  29. package/dist/studio.js +4 -1
  30. package/dist/templates.d.ts +29 -8
  31. package/dist/templates.js +30 -20
  32. package/dist/testing.d.ts +7 -0
  33. package/dist/testing.js +7 -0
  34. package/dist/types.d.ts +76 -0
  35. package/dist/verify.d.ts +1 -0
  36. package/dist/verify.js +43 -5
  37. package/package.json +8 -4
  38. package/dist/__tests__/cascade.test.d.ts +0 -11
  39. package/dist/__tests__/cascade.test.js +0 -449
  40. package/dist/__tests__/claude.test.d.ts +0 -1
  41. package/dist/__tests__/claude.test.js +0 -427
  42. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  43. package/dist/__tests__/custom-loader.test.js +0 -116
  44. package/dist/__tests__/executor.test.d.ts +0 -1
  45. package/dist/__tests__/executor.test.js +0 -736
  46. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  48. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  50. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/github.integration.test.js +0 -37
  52. package/dist/__tests__/integration/harness.d.ts +0 -24
  53. package/dist/__tests__/integration/harness.js +0 -34
  54. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  55. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  56. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  57. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  58. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  59. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  60. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  61. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  62. package/dist/__tests__/publish.test.d.ts +0 -1
  63. package/dist/__tests__/publish.test.js +0 -180
  64. package/dist/__tests__/schema.test.d.ts +0 -1
  65. package/dist/__tests__/schema.test.js +0 -526
  66. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  67. package/dist/__tests__/skill-config-integration.test.js +0 -204
  68. package/dist/__tests__/skills-index.test.d.ts +0 -1
  69. package/dist/__tests__/skills-index.test.js +0 -119
  70. package/dist/__tests__/skills.test.d.ts +0 -1
  71. package/dist/__tests__/skills.test.js +0 -565
  72. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  73. package/dist/__tests__/spec-conformance.test.js +0 -1074
  74. package/dist/__tests__/studio.test.d.ts +0 -1
  75. package/dist/__tests__/studio.test.js +0 -403
  76. package/dist/__tests__/templates.test.d.ts +0 -1
  77. package/dist/__tests__/templates.test.js +0 -49
  78. package/dist/__tests__/testing.test.d.ts +0 -1
  79. package/dist/__tests__/testing.test.js +0 -224
  80. package/dist/__tests__/verify.test.d.ts +0 -1
  81. package/dist/__tests__/verify.test.js +0 -517
  82. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  83. package/dist/__tests__/version-consistency.test.js +0 -312
  84. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  85. package/dist/__tests__/workflow-builder.test.js +0 -110
  86. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  87. package/dist/__tests__/workflow-yaml.test.js +0 -201
  88. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  89. package/dist/cli/__tests__/diagram.test.js +0 -225
  90. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  91. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  92. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  93. package/dist/cli/__tests__/upgrade.test.js +0 -317
  94. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  95. package/dist/cli/__tests__/version-check.test.js +0 -209
  96. package/dist/cli/config.test.d.ts +0 -1
  97. package/dist/cli/config.test.js +0 -200
  98. package/dist/cli/credentials.test.d.ts +0 -1
  99. package/dist/cli/credentials.test.js +0 -172
  100. package/dist/cli/e2e.test.d.ts +0 -1
  101. package/dist/cli/e2e.test.js +0 -690
  102. package/dist/cli/marketplace.test.d.ts +0 -1
  103. package/dist/cli/marketplace.test.js +0 -355
  104. package/dist/cli/new.test.d.ts +0 -1
  105. package/dist/cli/new.test.js +0 -1043
  106. package/dist/cli/output.test.d.ts +0 -1
  107. package/dist/cli/output.test.js +0 -180
  108. package/dist/cli/publish.test.d.ts +0 -1
  109. package/dist/cli/publish.test.js +0 -610
  110. package/dist/cli/renderer.test.d.ts +0 -1
  111. package/dist/cli/renderer.test.js +0 -302
  112. package/dist/executor.test.d.ts +0 -1
  113. package/dist/executor.test.js +0 -603
  114. package/dist/mcp.test.d.ts +0 -1
  115. package/dist/mcp.test.js +0 -735
  116. package/dist/mermaid.test.d.ts +0 -1
  117. package/dist/mermaid.test.js +0 -256
  118. package/dist/sources.test.d.ts +0 -1
  119. package/dist/sources.test.js +0 -265
  120. package/dist/sources.validation.test.d.ts +0 -12
  121. package/dist/sources.validation.test.js +0 -380
  122. package/dist/workflow-builder.test.d.ts +0 -1
  123. package/dist/workflow-builder.test.js +0 -117
@@ -1,565 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { github } from "../skills/github.js";
6
- import { linear } from "../skills/linear.js";
7
- import { slack } from "../skills/slack.js";
8
- import { sentry } from "../skills/sentry.js";
9
- import { datadog } from "../skills/datadog.js";
10
- import { notification } from "../skills/notification.js";
11
- import { createSkillMap } from "../skills/index.js";
12
- import { loadCustomSkills, discoverSkills, configuredSkills } from "../skills/custom-loader.js";
13
- // ─── Helpers ─────────────────────────────────────────────────────
14
- const mockCtx = (config = {}) => ({
15
- config,
16
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
17
- });
18
- let fetchMock;
19
- beforeEach(() => {
20
- fetchMock = vi.fn();
21
- vi.stubGlobal("fetch", fetchMock);
22
- });
23
- afterEach(() => {
24
- vi.restoreAllMocks();
25
- });
26
- function mockResponse(body, ok = true, status = 200) {
27
- return {
28
- ok,
29
- status,
30
- json: async () => body,
31
- text: async () => JSON.stringify(body),
32
- };
33
- }
34
- function findTool(skillTools, name) {
35
- const tool = skillTools.find((t) => t.name === name);
36
- if (!tool)
37
- throw new Error(`Tool ${name} not found`);
38
- return tool;
39
- }
40
- // ─── GitHub skill tests ──────────────────────────────────────────
41
- describe("github skill", () => {
42
- const ctx = () => mockCtx({ GITHUB_TOKEN: "ghp_test" });
43
- it("search_code sends correct request", async () => {
44
- fetchMock.mockResolvedValueOnce(mockResponse({ items: [] }));
45
- const tool = findTool(github.tools, "github_search_code");
46
- await tool.handler({ query: "foo", repo: "owner/repo" }, ctx());
47
- expect(fetchMock).toHaveBeenCalledOnce();
48
- const [url, opts] = fetchMock.mock.calls[0];
49
- expect(url).toContain("/search/code");
50
- expect(url).toContain("repo%3Aowner%2Frepo");
51
- expect(opts.headers.Authorization).toBe("token ghp_test");
52
- expect(opts.signal).toBeDefined(); // timeout signal
53
- });
54
- it("get_issue sends correct request", async () => {
55
- fetchMock.mockResolvedValueOnce(mockResponse({ id: 1, title: "Bug" }));
56
- const tool = findTool(github.tools, "github_get_issue");
57
- const result = await tool.handler({ repo: "owner/repo", number: 42 }, ctx());
58
- expect(fetchMock.mock.calls[0][0]).toContain("/repos/owner/repo/issues/42");
59
- expect(result.title).toBe("Bug");
60
- });
61
- it("create_issue sends POST with body", async () => {
62
- fetchMock.mockResolvedValueOnce(mockResponse({ number: 1 }));
63
- const tool = findTool(github.tools, "github_create_issue");
64
- await tool.handler({ repo: "o/r", title: "New Issue", body: "Details", labels: ["bug"] }, ctx());
65
- const [, opts] = fetchMock.mock.calls[0];
66
- expect(opts.method).toBe("POST");
67
- const body = JSON.parse(opts.body);
68
- expect(body.title).toBe("New Issue");
69
- expect(body.labels).toContain("bug");
70
- });
71
- it("create_pr sends POST with correct fields", async () => {
72
- fetchMock.mockResolvedValueOnce(mockResponse({ number: 10 }));
73
- const tool = findTool(github.tools, "github_create_pr");
74
- await tool.handler({ repo: "o/r", title: "PR", head: "feature", base: "develop" }, ctx());
75
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
76
- expect(body.head).toBe("feature");
77
- expect(body.base).toBe("develop");
78
- });
79
- it("get_file decodes base64 content", async () => {
80
- const encoded = Buffer.from("hello world").toString("base64");
81
- fetchMock.mockResolvedValueOnce(mockResponse({ content: encoded, encoding: "base64" }));
82
- const tool = findTool(github.tools, "github_get_file");
83
- const result = await tool.handler({ repo: "o/r", path: "README.md" }, ctx());
84
- expect(result.decoded_content).toBe("hello world");
85
- });
86
- it("throws on HTTP error", async () => {
87
- fetchMock.mockResolvedValueOnce(mockResponse("Not Found", false, 404));
88
- const tool = findTool(github.tools, "github_search_code");
89
- await expect(tool.handler({ query: "x", repo: "o/r" }, ctx())).rejects.toThrow("[GitHub]");
90
- });
91
- });
92
- // ─── Linear skill tests ─────────────────────────────────────────
93
- describe("linear skill", () => {
94
- const ctx = () => mockCtx({ LINEAR_API_KEY: "lin_test" });
95
- it("create_issue sends correct GraphQL mutation", async () => {
96
- fetchMock.mockResolvedValueOnce(mockResponse({
97
- data: {
98
- issueCreate: {
99
- success: true,
100
- issue: { id: "1", identifier: "TEST-1", url: "https://linear.app/test/issue/TEST-1", title: "Bug" },
101
- },
102
- },
103
- }));
104
- const tool = findTool(linear.tools, "linear_create_issue");
105
- await tool.handler({ teamId: "team1", title: "Bug" }, ctx());
106
- const [url, opts] = fetchMock.mock.calls[0];
107
- expect(url).toBe("https://api.linear.app/graphql");
108
- expect(opts.headers.Authorization).toBe("lin_test");
109
- const body = JSON.parse(opts.body);
110
- expect(body.query).toContain("issueCreate");
111
- });
112
- it("throws when issueCreate returns no identifier (silent failure guard)", async () => {
113
- fetchMock.mockResolvedValueOnce(mockResponse({ data: { issueCreate: { success: true, issue: { id: "1" } } } }));
114
- const tool = findTool(linear.tools, "linear_create_issue");
115
- await expect(tool.handler({ teamId: "team1", title: "Bug" }, ctx())).rejects.toThrow(/linear_create_issue returned no identifier/);
116
- });
117
- it("throws when issueCreate returns success: false", async () => {
118
- fetchMock.mockResolvedValueOnce(mockResponse({ data: { issueCreate: { success: false } } }));
119
- const tool = findTool(linear.tools, "linear_create_issue");
120
- await expect(tool.handler({ teamId: "team1", title: "Bug" }, ctx())).rejects.toThrow(/linear_create_issue returned no identifier/);
121
- });
122
- it("search_issues sends correct query", async () => {
123
- fetchMock.mockResolvedValueOnce(mockResponse({ data: { searchIssues: { nodes: [] } } }));
124
- const tool = findTool(linear.tools, "linear_search_issues");
125
- await tool.handler({ query: "memory leak", limit: 5 }, ctx());
126
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
127
- expect(body.variables.query).toBe("memory leak");
128
- expect(body.variables.first).toBe(5);
129
- });
130
- it("update_issue sends correct mutation", async () => {
131
- fetchMock.mockResolvedValueOnce(mockResponse({ data: { issueUpdate: { success: true } } }));
132
- const tool = findTool(linear.tools, "linear_update_issue");
133
- await tool.handler({ issueId: "id1", title: "Updated" }, ctx());
134
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
135
- expect(body.variables.id).toBe("id1");
136
- expect(body.variables.input.title).toBe("Updated");
137
- });
138
- it("throws on GraphQL errors", async () => {
139
- fetchMock.mockResolvedValueOnce(mockResponse({ errors: [{ message: "Not found" }] }));
140
- const tool = findTool(linear.tools, "linear_search_issues");
141
- await expect(tool.handler({ query: "x" }, ctx())).rejects.toThrow("[Linear] GraphQL error");
142
- });
143
- it("throws on HTTP error", async () => {
144
- fetchMock.mockResolvedValueOnce(mockResponse("Unauthorized", false, 401));
145
- const tool = findTool(linear.tools, "linear_create_issue");
146
- await expect(tool.handler({ teamId: "t", title: "x" }, ctx())).rejects.toThrow("[Linear]");
147
- });
148
- });
149
- // ─── Slack skill tests ──────────────────────────────────────────
150
- describe("slack skill", () => {
151
- it("sends via webhook when only webhook is configured", async () => {
152
- fetchMock.mockResolvedValueOnce(mockResponse("ok", true, 200));
153
- const tool = findTool(slack.tools, "slack_send_message");
154
- const ctx = mockCtx({ SLACK_WEBHOOK_URL: "https://hooks.slack.com/test" });
155
- await tool.handler({ text: "Hello" }, ctx);
156
- expect(fetchMock.mock.calls[0][0]).toBe("https://hooks.slack.com/test");
157
- });
158
- it("sends via bot token when bot token and channel are provided", async () => {
159
- fetchMock.mockResolvedValueOnce(mockResponse({ ok: true, ts: "123" }));
160
- const tool = findTool(slack.tools, "slack_send_message");
161
- const ctx = mockCtx({ SLACK_BOT_TOKEN: "xoxb-test" });
162
- const result = await tool.handler({ text: "Hello", channel: "C123" }, ctx);
163
- expect(fetchMock.mock.calls[0][0]).toBe("https://slack.com/api/chat.postMessage");
164
- expect(result.ok).toBe(true);
165
- });
166
- it("throws when Slack API returns ok:false", async () => {
167
- fetchMock.mockResolvedValueOnce(mockResponse({ ok: false, error: "channel_not_found" }));
168
- const tool = findTool(slack.tools, "slack_send_message");
169
- const ctx = mockCtx({ SLACK_BOT_TOKEN: "xoxb-test" });
170
- await expect(tool.handler({ text: "Hello", channel: "C123" }, ctx)).rejects.toThrow("channel_not_found");
171
- });
172
- it("throws when no credentials configured", async () => {
173
- const tool = findTool(slack.tools, "slack_send_message");
174
- const ctx = mockCtx({});
175
- await expect(tool.handler({ text: "Hello" }, ctx)).rejects.toThrow("No Slack credentials");
176
- });
177
- it("thread_reply requires bot token", async () => {
178
- const tool = findTool(slack.tools, "slack_send_thread_reply");
179
- const ctx = mockCtx({});
180
- await expect(tool.handler({ channel: "C1", thread_ts: "123", text: "Reply" }, ctx)).rejects.toThrow("SLACK_BOT_TOKEN");
181
- });
182
- it("thread_reply validates response", async () => {
183
- fetchMock.mockResolvedValueOnce(mockResponse({ ok: false, error: "thread_not_found" }));
184
- const tool = findTool(slack.tools, "slack_send_thread_reply");
185
- const ctx = mockCtx({ SLACK_BOT_TOKEN: "xoxb-test" });
186
- await expect(tool.handler({ channel: "C1", thread_ts: "123", text: "Reply" }, ctx)).rejects.toThrow("thread_not_found");
187
- });
188
- });
189
- // ─── Sentry skill tests ─────────────────────────────────────────
190
- describe("sentry skill", () => {
191
- const ctx = () => mockCtx({ SENTRY_AUTH_TOKEN: "sntrys_test", SENTRY_ORG: "my-org" });
192
- it("list_issues sends correct request", async () => {
193
- fetchMock.mockResolvedValueOnce(mockResponse([]));
194
- const tool = findTool(sentry.tools, "sentry_list_issues");
195
- await tool.handler({ project: "my-project" }, ctx());
196
- const [url, opts] = fetchMock.mock.calls[0];
197
- expect(url).toContain("/projects/my-org/my-project/issues/");
198
- expect(opts.headers.Authorization).toBe("Bearer sntrys_test");
199
- });
200
- it("get_issue sends correct request", async () => {
201
- fetchMock.mockResolvedValueOnce(mockResponse({ id: "123" }));
202
- const tool = findTool(sentry.tools, "sentry_get_issue");
203
- await tool.handler({ issueId: "123" }, ctx());
204
- expect(fetchMock.mock.calls[0][0]).toContain("/issues/123/");
205
- });
206
- it("uses SENTRY_BASE_URL when configured", async () => {
207
- fetchMock.mockResolvedValueOnce(mockResponse([]));
208
- const tool = findTool(sentry.tools, "sentry_list_issues");
209
- const customCtx = mockCtx({
210
- SENTRY_AUTH_TOKEN: "x",
211
- SENTRY_ORG: "org",
212
- SENTRY_BASE_URL: "https://sentry.example.com",
213
- });
214
- await tool.handler({ project: "p" }, customCtx);
215
- expect(fetchMock.mock.calls[0][0]).toContain("https://sentry.example.com");
216
- });
217
- it("throws on HTTP error", async () => {
218
- fetchMock.mockResolvedValueOnce(mockResponse("Forbidden", false, 403));
219
- const tool = findTool(sentry.tools, "sentry_list_issues");
220
- await expect(tool.handler({ project: "p" }, ctx())).rejects.toThrow("[Sentry]");
221
- });
222
- });
223
- // ─── Datadog skill tests ────────────────────────────────────────
224
- describe("datadog skill", () => {
225
- const ctx = () => mockCtx({ DD_API_KEY: "dd_key", DD_APP_KEY: "dd_app" });
226
- it("search_logs sends correct POST to v2 API", async () => {
227
- fetchMock.mockResolvedValueOnce(mockResponse({ data: [] }));
228
- const tool = findTool(datadog.tools, "datadog_search_logs");
229
- await tool.handler({ query: "service:web status:error" }, ctx());
230
- const [url, opts] = fetchMock.mock.calls[0];
231
- expect(url).toContain("/api/v2/logs/events/search");
232
- expect(opts.method).toBe("POST");
233
- expect(opts.headers["DD-API-KEY"]).toBe("dd_key");
234
- expect(opts.headers["DD-APPLICATION-KEY"]).toBe("dd_app");
235
- });
236
- it("query_metrics sends correct GET to v1 API", async () => {
237
- fetchMock.mockResolvedValueOnce(mockResponse({ series: [] }));
238
- const tool = findTool(datadog.tools, "datadog_query_metrics");
239
- await tool.handler({ query: "avg:system.cpu.user{*}", from: 1000, to: 2000 }, ctx());
240
- const url = fetchMock.mock.calls[0][0];
241
- expect(url).toContain("/api/v1/query");
242
- expect(url).toContain("from=1000");
243
- expect(url).toContain("to=2000");
244
- });
245
- it("list_monitors sends correct GET", async () => {
246
- fetchMock.mockResolvedValueOnce(mockResponse([]));
247
- const tool = findTool(datadog.tools, "datadog_list_monitors");
248
- await tool.handler({ name: "CPU" }, ctx());
249
- const url = fetchMock.mock.calls[0][0];
250
- expect(url).toContain("/api/v1/monitor");
251
- expect(url).toContain("name=CPU");
252
- });
253
- it("uses DD_SITE for custom domain", async () => {
254
- fetchMock.mockResolvedValueOnce(mockResponse([]));
255
- const tool = findTool(datadog.tools, "datadog_list_monitors");
256
- const euCtx = mockCtx({ DD_API_KEY: "k", DD_APP_KEY: "a", DD_SITE: "datadoghq.eu" });
257
- await tool.handler({}, euCtx);
258
- expect(fetchMock.mock.calls[0][0]).toContain("api.datadoghq.eu");
259
- });
260
- it("throws on HTTP error", async () => {
261
- fetchMock.mockResolvedValueOnce(mockResponse("Unauthorized", false, 401));
262
- const tool = findTool(datadog.tools, "datadog_search_logs");
263
- await expect(tool.handler({ query: "x" }, ctx())).rejects.toThrow("[Datadog]");
264
- });
265
- });
266
- // ─── Notification skill tests ───────────────────────────────────
267
- describe("notification skill", () => {
268
- it("notify_webhook sends POST to configured URL", async () => {
269
- fetchMock.mockResolvedValueOnce(mockResponse("ok", true, 200));
270
- const tool = findTool(notification.tools, "notify_webhook");
271
- const ctx = mockCtx({ NOTIFICATION_WEBHOOK_URL: "https://example.com/hook" });
272
- await tool.handler({ payload: { event: "test" } }, ctx);
273
- expect(fetchMock.mock.calls[0][0]).toBe("https://example.com/hook");
274
- });
275
- it("notify_webhook allows URL override", async () => {
276
- fetchMock.mockResolvedValueOnce(mockResponse("ok", true, 200));
277
- const tool = findTool(notification.tools, "notify_webhook");
278
- const ctx = mockCtx({});
279
- await tool.handler({ url: "https://custom.com/hook", payload: { x: 1 } }, ctx);
280
- expect(fetchMock.mock.calls[0][0]).toBe("https://custom.com/hook");
281
- });
282
- it("notify_webhook throws without URL", async () => {
283
- const tool = findTool(notification.tools, "notify_webhook");
284
- await expect(tool.handler({ payload: {} }, mockCtx({}))).rejects.toThrow("No webhook URL");
285
- });
286
- it("notify_discord sends to Discord webhook", async () => {
287
- fetchMock.mockResolvedValueOnce(mockResponse(null, true, 204));
288
- const tool = findTool(notification.tools, "notify_discord");
289
- const ctx = mockCtx({ DISCORD_WEBHOOK_URL: "https://discord.com/api/webhooks/test" });
290
- await tool.handler({ content: "Hello Discord" }, ctx);
291
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
292
- expect(body.content).toBe("Hello Discord");
293
- });
294
- it("notify_discord throws without URL", async () => {
295
- const tool = findTool(notification.tools, "notify_discord");
296
- await expect(tool.handler({ content: "x" }, mockCtx({}))).rejects.toThrow("DISCORD_WEBHOOK_URL");
297
- });
298
- it("notify_teams sends MessageCard", async () => {
299
- fetchMock.mockResolvedValueOnce(mockResponse("1", true, 200));
300
- const tool = findTool(notification.tools, "notify_teams");
301
- const ctx = mockCtx({ TEAMS_WEBHOOK_URL: "https://outlook.webhook.office.com/test" });
302
- await tool.handler({ text: "Alert!", title: "SWEny" }, ctx);
303
- const body = JSON.parse(fetchMock.mock.calls[0][1].body);
304
- expect(body["@type"]).toBe("MessageCard");
305
- expect(body.title).toBe("SWEny");
306
- });
307
- it("notify_teams throws without URL", async () => {
308
- const tool = findTool(notification.tools, "notify_teams");
309
- await expect(tool.handler({ text: "x" }, mockCtx({}))).rejects.toThrow("TEAMS_WEBHOOK_URL");
310
- });
311
- it("all notification fetches have timeout signal", async () => {
312
- fetchMock.mockResolvedValueOnce(mockResponse("ok", true, 200));
313
- const tool = findTool(notification.tools, "notify_webhook");
314
- const ctx = mockCtx({ NOTIFICATION_WEBHOOK_URL: "https://example.com" });
315
- await tool.handler({ payload: {} }, ctx);
316
- expect(fetchMock.mock.calls[0][1].signal).toBeDefined();
317
- });
318
- });
319
- // ─── Custom skill discovery tests ──────────────────────────────
320
- describe("loadCustomSkills", () => {
321
- let tmpDir;
322
- beforeEach(() => {
323
- tmpDir = mkdtempSync(join(tmpdir(), "sweny-skills-test-"));
324
- });
325
- afterEach(() => {
326
- rmSync(tmpDir, { recursive: true, force: true });
327
- });
328
- function writeSkill(dirName, content) {
329
- const dir = join(tmpDir, ".claude", "skills", dirName);
330
- mkdirSync(dir, { recursive: true });
331
- writeFileSync(join(dir, "SKILL.md"), content);
332
- }
333
- it("returns empty array when .claude/skills/ does not exist", () => {
334
- const skills = loadCustomSkills(tmpDir);
335
- expect(skills).toEqual([]);
336
- });
337
- it("returns empty array when .claude/skills/ is empty", () => {
338
- mkdirSync(join(tmpDir, ".claude", "skills"), { recursive: true });
339
- const skills = loadCustomSkills(tmpDir);
340
- expect(skills).toEqual([]);
341
- });
342
- it("discovers a valid custom skill from SKILL.md frontmatter", () => {
343
- writeSkill("my-skill", `---
344
- name: my-skill
345
- description: A test skill
346
- ---
347
-
348
- Instructions for Claude go here.
349
- `);
350
- const skills = loadCustomSkills(tmpDir);
351
- expect(skills).toHaveLength(1);
352
- expect(skills[0]).toMatchObject({
353
- id: "my-skill",
354
- name: "my-skill",
355
- description: "A test skill",
356
- category: "general",
357
- });
358
- expect(skills[0].tools).toEqual([]);
359
- expect(skills[0].config).toEqual({});
360
- });
361
- it("discovers multiple skills", () => {
362
- writeSkill("skill-a", `---\nname: skill-a\ndescription: First\n---\nBody`);
363
- writeSkill("skill-b", `---\nname: skill-b\ndescription: Second\n---\nBody`);
364
- const skills = loadCustomSkills(tmpDir);
365
- expect(skills).toHaveLength(2);
366
- const ids = skills.map((s) => s.id).sort();
367
- expect(ids).toEqual(["skill-a", "skill-b"]);
368
- });
369
- it("skips directories without SKILL.md", () => {
370
- mkdirSync(join(tmpDir, ".claude", "skills", "no-skill-file"), { recursive: true });
371
- writeFileSync(join(tmpDir, ".claude", "skills", "no-skill-file", "README.md"), "not a skill");
372
- const skills = loadCustomSkills(tmpDir);
373
- expect(skills).toEqual([]);
374
- });
375
- it("skips SKILL.md without frontmatter", () => {
376
- writeSkill("no-frontmatter", "Just plain text, no YAML frontmatter.");
377
- const skills = loadCustomSkills(tmpDir);
378
- expect(skills).toEqual([]);
379
- });
380
- it("skips SKILL.md with frontmatter missing name field", () => {
381
- writeSkill("no-name", `---\ndescription: Missing name\n---\nBody`);
382
- const skills = loadCustomSkills(tmpDir);
383
- expect(skills).toEqual([]);
384
- });
385
- it("uses fallback description when description is missing", () => {
386
- writeSkill("name-only", `---\nname: name-only\n---\nBody`);
387
- const skills = loadCustomSkills(tmpDir);
388
- expect(skills).toHaveLength(1);
389
- expect(skills[0].description).toBe("Custom skill: name-only");
390
- });
391
- it("skips malformed YAML in frontmatter", () => {
392
- writeSkill("bad-yaml", `---\n: [invalid yaml\n---\nBody`);
393
- const skills = loadCustomSkills(tmpDir);
394
- expect(skills).toEqual([]);
395
- });
396
- });
397
- describe("configuredSkills with custom skills", () => {
398
- let tmpDir;
399
- beforeEach(() => {
400
- tmpDir = mkdtempSync(join(tmpdir(), "sweny-configured-test-"));
401
- });
402
- afterEach(() => {
403
- rmSync(tmpDir, { recursive: true, force: true });
404
- });
405
- function writeSkill(dirName, content) {
406
- const dir = join(tmpDir, ".claude", "skills", dirName);
407
- mkdirSync(dir, { recursive: true });
408
- writeFileSync(join(dir, "SKILL.md"), content);
409
- }
410
- it("includes custom skills alongside configured built-ins", () => {
411
- writeSkill("my-custom", `---\nname: my-custom\ndescription: Custom\n---\nBody`);
412
- const skills = configuredSkills({}, tmpDir);
413
- const ids = skills.map((s) => s.id);
414
- expect(ids).toContain("my-custom");
415
- // Skills with only optional config are always configured
416
- expect(ids).toContain("notification");
417
- expect(ids).toContain("slack");
418
- });
419
- it("custom skills override built-in ID but merge tools", () => {
420
- writeSkill("notification", `---\nname: notification\ndescription: Custom override\n---\nCustom notification guidance.`);
421
- const skills = configuredSkills({}, tmpDir);
422
- const notif = skills.filter((s) => s.id === "notification");
423
- expect(notif).toHaveLength(1);
424
- // Built-in tools are preserved
425
- expect(notif[0].tools.length).toBeGreaterThan(0);
426
- // Custom instruction is added
427
- expect(notif[0].instruction).toBe("Custom notification guidance.");
428
- });
429
- it("custom skills are usable in createSkillMap", () => {
430
- writeSkill("extract-colors", `---\nname: extract-colors\ndescription: Route color extraction\n---\nBody`);
431
- const skills = configuredSkills({}, tmpDir);
432
- const map = createSkillMap(skills);
433
- expect(map.has("extract-colors")).toBe(true);
434
- expect(map.get("extract-colors").tools).toEqual([]);
435
- });
436
- });
437
- // ─── Multi-directory discovery tests ─────────────────────────────
438
- describe("discoverSkills (multi-directory)", () => {
439
- let tmpDir;
440
- beforeEach(() => {
441
- tmpDir = mkdtempSync(join(tmpdir(), "sweny-discover-test-"));
442
- });
443
- afterEach(() => {
444
- rmSync(tmpDir, { recursive: true, force: true });
445
- });
446
- function writeSkillAt(base, dirName, content) {
447
- const dir = join(tmpDir, base, "skills", dirName);
448
- mkdirSync(dir, { recursive: true });
449
- writeFileSync(join(dir, "SKILL.md"), content);
450
- }
451
- it("discovers skills from .sweny/skills/", () => {
452
- writeSkillAt(".sweny", "my-skill", `---\nname: my-skill\ndescription: A skill\n---\nDo things well.`);
453
- const skills = discoverSkills(tmpDir);
454
- expect(skills).toHaveLength(1);
455
- expect(skills[0].id).toBe("my-skill");
456
- });
457
- it("discovers skills from .claude/skills/", () => {
458
- writeSkillAt(".claude", "claude-skill", `---\nname: claude-skill\ndescription: Claude\n---\nInstructions.`);
459
- const skills = discoverSkills(tmpDir);
460
- expect(skills).toHaveLength(1);
461
- expect(skills[0].id).toBe("claude-skill");
462
- });
463
- it("discovers skills from .agents/skills/", () => {
464
- writeSkillAt(".agents", "agent-skill", `---\nname: agent-skill\ndescription: Agent\n---\nBody.`);
465
- const skills = discoverSkills(tmpDir);
466
- expect(skills).toHaveLength(1);
467
- });
468
- it("discovers skills from .gemini/skills/", () => {
469
- writeSkillAt(".gemini", "gemini-skill", `---\nname: gemini-skill\ndescription: Gemini\n---\nBody.`);
470
- const skills = discoverSkills(tmpDir);
471
- expect(skills).toHaveLength(1);
472
- });
473
- it("higher-priority directory wins on ID collision", () => {
474
- writeSkillAt(".sweny", "shared", `---\nname: shared\ndescription: From sweny\n---\nSweny instructions.`);
475
- writeSkillAt(".claude", "shared", `---\nname: shared\ndescription: From claude\n---\nClaude instructions.`);
476
- const skills = discoverSkills(tmpDir);
477
- const shared = skills.find((s) => s.id === "shared");
478
- expect(shared.description).toBe("From sweny");
479
- expect(shared.instruction).toBe("Sweny instructions.");
480
- });
481
- it("captures markdown body as instruction", () => {
482
- writeSkillAt(".sweny", "code-standards", `---
483
- name: code-standards
484
- description: Coding conventions
485
- ---
486
-
487
- When writing TypeScript:
488
- - Use camelCase for variables
489
- - Use PascalCase for types`);
490
- const skills = discoverSkills(tmpDir);
491
- expect(skills[0].instruction).toContain("Use camelCase for variables");
492
- expect(skills[0].instruction).toContain("Use PascalCase for types");
493
- });
494
- it("parses mcp from frontmatter", () => {
495
- writeSkillAt(".sweny", "our-crm", `---
496
- name: our-crm
497
- description: CRM integration
498
- mcp:
499
- command: npx
500
- args:
501
- - -y
502
- - "@company/crm-server"
503
- env:
504
- API_KEY: CRM API key
505
- ---
506
-
507
- Use the CRM tools to look up customer data.`);
508
- const skills = discoverSkills(tmpDir);
509
- expect(skills[0].mcp).toEqual({
510
- type: "stdio",
511
- command: "npx",
512
- args: ["-y", "@company/crm-server"],
513
- env: { API_KEY: "CRM API key" },
514
- });
515
- expect(skills[0].instruction).toContain("Use the CRM tools");
516
- });
517
- it("parses mcp with url (HTTP transport)", () => {
518
- writeSkillAt(".sweny", "http-tool", `---
519
- name: http-tool
520
- description: HTTP MCP
521
- mcp:
522
- url: https://mcp.example.com
523
- headers:
524
- Authorization: Bearer token
525
- ---
526
-
527
- Instructions.`);
528
- const skills = discoverSkills(tmpDir);
529
- expect(skills[0].mcp?.url).toBe("https://mcp.example.com");
530
- });
531
- it("returns empty array when no skill directories exist", () => {
532
- const skills = discoverSkills(tmpDir);
533
- expect(skills).toEqual([]);
534
- });
535
- it("merges skills from all directories", () => {
536
- writeSkillAt(".sweny", "skill-a", `---\nname: skill-a\ndescription: A\n---\nA.`);
537
- writeSkillAt(".claude", "skill-b", `---\nname: skill-b\ndescription: B\n---\nB.`);
538
- writeSkillAt(".agents", "skill-c", `---\nname: skill-c\ndescription: C\n---\nC.`);
539
- const skills = discoverSkills(tmpDir);
540
- expect(skills).toHaveLength(3);
541
- const ids = skills.map((s) => s.id).sort();
542
- expect(ids).toEqual(["skill-a", "skill-b", "skill-c"]);
543
- });
544
- it("rejects skill IDs with invalid characters", () => {
545
- writeSkillAt(".sweny", "valid-skill", `---\nname: ../../etc/passwd\ndescription: malicious\n---\nBody`);
546
- const skills = discoverSkills(tmpDir);
547
- expect(skills).toHaveLength(0);
548
- });
549
- it("rejects skill IDs with uppercase", () => {
550
- writeSkillAt(".sweny", "MySkill", `---\nname: MySkill\ndescription: bad\n---\nBody`);
551
- const skills = discoverSkills(tmpDir);
552
- expect(skills).toHaveLength(0);
553
- });
554
- it("rejects skill IDs with consecutive hyphens", () => {
555
- writeSkillAt(".sweny", "bad--name", `---\nname: bad--name\ndescription: bad\n---\nBody`);
556
- const skills = discoverSkills(tmpDir);
557
- expect(skills).toHaveLength(0);
558
- });
559
- it("rejects skill IDs longer than 64 chars", () => {
560
- const longId = "a".repeat(65);
561
- writeSkillAt(".sweny", longId, `---\nname: ${longId}\ndescription: long\n---\nBody`);
562
- const skills = discoverSkills(tmpDir);
563
- expect(skills).toHaveLength(0);
564
- });
565
- });
@@ -1,18 +0,0 @@
1
- /**
2
- * Spec Conformance Tests
3
- *
4
- * These tests validate that the @sweny-ai/core implementation conforms to the
5
- * SWEny Workflow Specification v1.0 (spec.sweny.ai). Each test references
6
- * the relevant spec section.
7
- *
8
- * Sections covered:
9
- * - Workflow structural validation
10
- * - Node input augmentation (rules, context, skill instructions)
11
- * - Edge routing algorithm
12
- * - Dry-run semantics
13
- * - Execution events ordering
14
- * - Execution trace structure
15
- * - Source resolution
16
- * - JSON Schema correctness
17
- */
18
- export {};