@sweny-ai/core 0.1.77 → 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 (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 +42 -4
  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,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 {};