@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,427 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- // ─── tryParseJSON tests (via inlined copy since it's private) ────
3
- describe("JSON extraction strategies", () => {
4
- function tryParseJSON(text) {
5
- if (!text)
6
- return {};
7
- const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
8
- if (codeBlockMatch) {
9
- try {
10
- const parsed = JSON.parse(codeBlockMatch[1].trim());
11
- if (typeof parsed === "object" && parsed !== null)
12
- return parsed;
13
- }
14
- catch {
15
- /* try next */
16
- }
17
- }
18
- const lastBrace = text.lastIndexOf("}");
19
- if (lastBrace !== -1) {
20
- let depth = 0;
21
- for (let i = lastBrace; i >= 0; i--) {
22
- if (text[i] === "}")
23
- depth++;
24
- else if (text[i] === "{")
25
- depth--;
26
- if (depth === 0) {
27
- try {
28
- const parsed = JSON.parse(text.slice(i, lastBrace + 1));
29
- if (typeof parsed === "object" && parsed !== null)
30
- return parsed;
31
- }
32
- catch {
33
- /* try next */
34
- }
35
- break;
36
- }
37
- }
38
- }
39
- try {
40
- const parsed = JSON.parse(text.trim());
41
- if (typeof parsed === "object" && parsed !== null)
42
- return parsed;
43
- }
44
- catch {
45
- /* fall through */
46
- }
47
- return {};
48
- }
49
- it("extracts JSON from code block", () => {
50
- const text = 'Here are the results:\n```json\n{"severity": "high", "count": 42}\n```';
51
- expect(tryParseJSON(text)).toEqual({ severity: "high", count: 42 });
52
- });
53
- it("extracts JSON from code block without json label", () => {
54
- const text = 'Results:\n```\n{"ok": true}\n```';
55
- expect(tryParseJSON(text)).toEqual({ ok: true });
56
- });
57
- it("extracts JSON from inline brace block", () => {
58
- const text = 'The analysis is complete. {"result": "success", "items": [1,2,3]}';
59
- expect(tryParseJSON(text)).toEqual({ result: "success", items: [1, 2, 3] });
60
- });
61
- it("extracts last JSON object when multiple exist", () => {
62
- const text = 'First: {"a": 1}\nThen: {"b": 2}';
63
- expect(tryParseJSON(text)).toEqual({ b: 2 });
64
- });
65
- it("handles nested JSON objects", () => {
66
- const text = 'Output: {"outer": {"inner": true}}';
67
- expect(tryParseJSON(text)).toEqual({ outer: { inner: true } });
68
- });
69
- it("parses pure JSON text", () => {
70
- const text = '{"pure": "json"}';
71
- expect(tryParseJSON(text)).toEqual({ pure: "json" });
72
- });
73
- it("returns empty object for no JSON", () => {
74
- expect(tryParseJSON("No JSON here at all")).toEqual({});
75
- });
76
- it("returns empty object for invalid JSON", () => {
77
- expect(tryParseJSON("{invalid json}")).toEqual({});
78
- });
79
- it("handles JSON with braces in strings", () => {
80
- const text = '{"message": "use {curly} braces"}';
81
- expect(tryParseJSON(text)).toEqual({ message: "use {curly} braces" });
82
- });
83
- it("handles multiline JSON in code block", () => {
84
- const text = '```json\n{\n "a": 1,\n "b": {\n "c": 2\n }\n}\n```';
85
- expect(tryParseJSON(text)).toEqual({ a: 1, b: { c: 2 } });
86
- });
87
- it("prefers code block over inline JSON", () => {
88
- const text = '{"inline": true}\n```json\n{"codeblock": true}\n```';
89
- expect(tryParseJSON(text)).toEqual({ codeblock: true });
90
- });
91
- it("handles empty code block gracefully", () => {
92
- const text = '```json\n\n```\n{"fallback": true}';
93
- expect(tryParseJSON(text)).toEqual({ fallback: true });
94
- });
95
- });
96
- // ─── ClaudeClient tests ──────────────────────────────────────────
97
- // Mock the agent SDK to test ClaudeClient behavior
98
- describe("ClaudeClient", () => {
99
- let mockQuery;
100
- let mockCreateSdkMcpServer;
101
- let mockSdkTool;
102
- let ClaudeClient;
103
- /** Helper: create an async generator that yields the given messages */
104
- function makeStream(messages) {
105
- return (async function* () {
106
- for (const msg of messages) {
107
- yield msg;
108
- }
109
- })();
110
- }
111
- beforeEach(async () => {
112
- mockQuery = vi.fn();
113
- mockCreateSdkMcpServer = vi.fn().mockReturnValue({ type: "sdk", name: "sweny-core" });
114
- mockSdkTool = vi.fn().mockImplementation((name, desc, schema, handler) => ({
115
- name,
116
- description: desc,
117
- inputSchema: schema,
118
- handler,
119
- }));
120
- vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
121
- query: mockQuery,
122
- createSdkMcpServer: mockCreateSdkMcpServer,
123
- tool: mockSdkTool,
124
- }));
125
- const mod = await import("../claude.js");
126
- ClaudeClient = mod.ClaudeClient;
127
- });
128
- afterEach(() => {
129
- vi.restoreAllMocks();
130
- vi.resetModules();
131
- });
132
- it("sends instruction and context in prompt", async () => {
133
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: '{"done": true}' }]));
134
- const client = new ClaudeClient();
135
- await client.run({
136
- instruction: "Analyze the logs",
137
- context: { alert: "cpu spike" },
138
- tools: [],
139
- });
140
- const callArgs = mockQuery.mock.calls[0][0];
141
- expect(callArgs.prompt).toContain("Analyze the logs");
142
- expect(callArgs.prompt).toContain("cpu spike");
143
- });
144
- it("passes system prompt and permissionMode", async () => {
145
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "ok" }]));
146
- const client = new ClaudeClient();
147
- await client.run({ instruction: "x", context: {}, tools: [] });
148
- const opts = mockQuery.mock.calls[0][0].options;
149
- expect(opts.systemPrompt).toContain("automated workflow");
150
- expect(opts.permissionMode).toBe("bypassPermissions");
151
- });
152
- it("returns success with parsed JSON data", async () => {
153
- mockQuery.mockReturnValueOnce(makeStream([
154
- { type: "result", subtype: "success", result: 'Analysis complete.\n```json\n{"severity": "high"}\n```' },
155
- ]));
156
- const client = new ClaudeClient();
157
- const result = await client.run({
158
- instruction: "Check severity",
159
- context: {},
160
- tools: [],
161
- });
162
- expect(result.status).toBe("success");
163
- expect(result.data.severity).toBe("high");
164
- });
165
- it("creates MCP server with converted tools", async () => {
166
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "done" }]));
167
- const handler = vi.fn().mockResolvedValue({ ok: true });
168
- const client = new ClaudeClient();
169
- await client.run({
170
- instruction: "Do work",
171
- context: {},
172
- tools: [
173
- {
174
- name: "my_tool",
175
- description: "A test tool",
176
- input_schema: {
177
- type: "object",
178
- properties: { query: { type: "string", description: "Search query" } },
179
- required: ["query"],
180
- },
181
- handler,
182
- },
183
- ],
184
- });
185
- // tool() was called for the tool
186
- expect(mockSdkTool).toHaveBeenCalledOnce();
187
- expect(mockSdkTool.mock.calls[0][0]).toBe("my_tool");
188
- expect(mockSdkTool.mock.calls[0][1]).toBe("A test tool");
189
- // MCP server was created
190
- expect(mockCreateSdkMcpServer).toHaveBeenCalledOnce();
191
- // mcpServers was passed to query
192
- const opts = mockQuery.mock.calls[0][0].options;
193
- expect(opts.mcpServers).toBeDefined();
194
- expect(opts.mcpServers["sweny-core"]).toBeDefined();
195
- });
196
- it("does not create MCP server when no tools", async () => {
197
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "done" }]));
198
- const client = new ClaudeClient();
199
- await client.run({ instruction: "x", context: {}, tools: [] });
200
- const opts = mockQuery.mock.calls[0][0].options;
201
- expect(opts.mcpServers).toBeUndefined();
202
- });
203
- it("tracks tool calls via handler wrapper", async () => {
204
- // Simulate: query returns success, but the tool handler was invoked by MCP internally
205
- // We test the handler wrapper directly since query is mocked
206
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "done" }]));
207
- const handler = vi.fn().mockResolvedValue({ items: [1, 2] });
208
- const client = new ClaudeClient();
209
- const result = await client.run({
210
- instruction: "x",
211
- context: {},
212
- tools: [{ name: "fetch_data", description: "d", input_schema: {}, handler }],
213
- });
214
- // The SDK tool handler was created — invoke it directly to test wrapping
215
- const sdkHandler = mockSdkTool.mock.calls[0][3];
216
- const callResult = await sdkHandler({ q: "test" }, {});
217
- expect(callResult.content[0].text).toContain("items");
218
- expect(callResult.isError).toBeUndefined();
219
- // Tool call should be tracked
220
- expect(result.toolCalls.length).toBeGreaterThanOrEqual(0); // stream didn't trigger it, but direct call did
221
- });
222
- it("wraps tool handler errors as isError", async () => {
223
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "done" }]));
224
- const handler = vi.fn().mockRejectedValue(new Error("tool broke"));
225
- const client = new ClaudeClient();
226
- await client.run({
227
- instruction: "x",
228
- context: {},
229
- tools: [{ name: "bad_tool", description: "d", input_schema: {}, handler }],
230
- });
231
- // Invoke the SDK handler directly
232
- const sdkHandler = mockSdkTool.mock.calls[0][3];
233
- const callResult = await sdkHandler({}, {});
234
- expect(callResult.content[0].text).toContain("tool broke");
235
- expect(callResult.isError).toBe(true);
236
- });
237
- it("returns failed on query error", async () => {
238
- mockQuery.mockReturnValueOnce((async function* () {
239
- throw new Error("connection refused");
240
- })());
241
- const client = new ClaudeClient();
242
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
243
- expect(result.status).toBe("failed");
244
- expect(result.data.error).toContain("connection refused");
245
- });
246
- it("returns failed on error result subtype", async () => {
247
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "error_max_turns", errors: ["Too many turns"], is_error: true }]));
248
- const client = new ClaudeClient();
249
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
250
- expect(result.status).toBe("failed");
251
- expect(result.data.error).toContain("Too many turns");
252
- });
253
- // terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
254
- // When the turn budget is exhausted the SDK now emits subtype='success'
255
- // alongside terminal_reason='max_turns' instead of the old error subtype,
256
- // so the claude.ts handler has to look at both fields or silently accept
257
- // truncated output as a success.
258
- describe("terminal_reason handling (claude-agent-sdk v0.2.91+)", () => {
259
- it("returns failed when terminal_reason is max_turns", async () => {
260
- mockQuery.mockReturnValueOnce(makeStream([
261
- {
262
- type: "result",
263
- subtype: "success",
264
- result: "partial output",
265
- terminal_reason: "max_turns",
266
- },
267
- ]));
268
- const client = new ClaudeClient();
269
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
270
- expect(result.status).toBe("failed");
271
- expect(result.data.error).toContain("max_turns");
272
- expect(result.data.error).toContain("terminated early");
273
- });
274
- it("returns failed for any non-completed terminal_reason", async () => {
275
- // Future SDK versions may add new terminal reasons (aborted_tools,
276
- // blocking_limit, etc.) — treat anything that isn't 'completed' as a
277
- // failure rather than assume it's recoverable.
278
- mockQuery.mockReturnValueOnce(makeStream([
279
- {
280
- type: "result",
281
- subtype: "success",
282
- result: "partial",
283
- terminal_reason: "aborted_tools",
284
- },
285
- ]));
286
- const client = new ClaudeClient();
287
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
288
- expect(result.status).toBe("failed");
289
- expect(result.data.error).toContain("aborted_tools");
290
- });
291
- it("returns success when terminal_reason is 'completed'", async () => {
292
- mockQuery.mockReturnValueOnce(makeStream([
293
- {
294
- type: "result",
295
- subtype: "success",
296
- result: '{"ok": true}',
297
- terminal_reason: "completed",
298
- },
299
- ]));
300
- const client = new ClaudeClient();
301
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
302
- expect(result.status).toBe("success");
303
- expect(result.data.ok).toBe(true);
304
- });
305
- it("returns success when terminal_reason is absent (pre-v0.2.91 SDK)", async () => {
306
- // Old SDKs never set terminal_reason. The guard must not treat an
307
- // absent field as a failure or every caller would break on downgrade.
308
- mockQuery.mockReturnValueOnce(makeStream([
309
- {
310
- type: "result",
311
- subtype: "success",
312
- result: '{"done": true}',
313
- // terminal_reason deliberately omitted
314
- },
315
- ]));
316
- const client = new ClaudeClient();
317
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
318
- expect(result.status).toBe("success");
319
- expect(result.data.done).toBe(true);
320
- });
321
- it("preserves toolCalls when failing on terminal_reason", async () => {
322
- // A max_turns failure should still surface any tool calls that
323
- // happened before the budget ran out, so debuggers can see what
324
- // Claude was trying to do.
325
- mockQuery.mockReturnValueOnce(makeStream([
326
- {
327
- type: "assistant",
328
- message: {
329
- content: [
330
- {
331
- type: "tool_use",
332
- name: "search_code",
333
- input: { query: "foo" },
334
- },
335
- ],
336
- },
337
- },
338
- {
339
- type: "result",
340
- subtype: "success",
341
- result: "partial",
342
- terminal_reason: "max_turns",
343
- },
344
- ]));
345
- const client = new ClaudeClient();
346
- const result = await client.run({ instruction: "x", context: {}, tools: [] });
347
- expect(result.status).toBe("failed");
348
- expect(result.toolCalls).toHaveLength(1);
349
- expect(result.toolCalls[0].tool).toBe("search_code");
350
- });
351
- });
352
- it("passes model option to query", async () => {
353
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "ok" }]));
354
- const client = new ClaudeClient({ model: "claude-sonnet-4-20250514" });
355
- await client.run({ instruction: "x", context: {}, tools: [] });
356
- expect(mockQuery.mock.calls[0][0].options.model).toBe("claude-sonnet-4-20250514");
357
- });
358
- it("evaluate returns exact match", async () => {
359
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "handle_high" }]));
360
- const client = new ClaudeClient();
361
- const result = await client.evaluate({
362
- question: "Which path?",
363
- context: {},
364
- choices: [
365
- { id: "handle_high", description: "High severity" },
366
- { id: "handle_low", description: "Low severity" },
367
- ],
368
- });
369
- expect(result).toBe("handle_high");
370
- });
371
- it("evaluate returns fuzzy match", async () => {
372
- mockQuery.mockReturnValueOnce(makeStream([
373
- { type: "result", subtype: "success", result: "I think the answer is handle_low based on the data" },
374
- ]));
375
- const client = new ClaudeClient();
376
- const result = await client.evaluate({
377
- question: "Which path?",
378
- context: {},
379
- choices: [
380
- { id: "handle_high", description: "High" },
381
- { id: "handle_low", description: "Low" },
382
- ],
383
- });
384
- expect(result).toBe("handle_low");
385
- });
386
- it("evaluate falls back to first choice", async () => {
387
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "I'm not sure" }]));
388
- const client = new ClaudeClient();
389
- const result = await client.evaluate({
390
- question: "Which?",
391
- context: {},
392
- choices: [
393
- { id: "a", description: "A" },
394
- { id: "b", description: "B" },
395
- ],
396
- });
397
- expect(result).toBe("a");
398
- });
399
- it("evaluate falls back on query error", async () => {
400
- mockQuery.mockReturnValueOnce((async function* () {
401
- throw new Error("offline");
402
- })());
403
- const client = new ClaudeClient();
404
- const result = await client.evaluate({
405
- question: "Which?",
406
- context: {},
407
- choices: [
408
- { id: "fallback", description: "Fallback" },
409
- { id: "other", description: "Other" },
410
- ],
411
- });
412
- expect(result).toBe("fallback");
413
- });
414
- it("includes output schema in prompt when provided", async () => {
415
- mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: '{"severity": "high"}' }]));
416
- const client = new ClaudeClient();
417
- await client.run({
418
- instruction: "Analyze",
419
- context: {},
420
- tools: [],
421
- outputSchema: { type: "object", properties: { severity: { type: "string" } } },
422
- });
423
- const prompt = mockQuery.mock.calls[0][0].prompt;
424
- expect(prompt).toContain("Required Output");
425
- expect(prompt).toContain("severity");
426
- });
427
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,116 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { readdirSync, readFileSync, existsSync } from "node:fs";
3
- vi.mock("node:fs", () => ({
4
- readdirSync: vi.fn(),
5
- readFileSync: vi.fn(),
6
- existsSync: vi.fn(),
7
- }));
8
- const mockedExistsSync = vi.mocked(existsSync);
9
- const mockedReaddirSync = vi.mocked(readdirSync);
10
- const mockedReadFileSync = vi.mocked(readFileSync);
11
- // Import after mocking
12
- const { discoverSkills } = await import("../skills/custom-loader.js");
13
- describe("custom-loader", () => {
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- });
17
- describe("config parsing from SKILL.md frontmatter", () => {
18
- it("parses config fields from frontmatter", () => {
19
- const skillMd = `---
20
- name: tax-tool
21
- description: Processes tax filings
22
- config:
23
- MY_TAX_API_KEY:
24
- description: API key for the tax service
25
- required: true
26
- MY_TAX_REGION:
27
- description: Tax region code
28
- required: false
29
- ---
30
- Do tax stuff.
31
- `;
32
- mockedExistsSync.mockImplementation((p) => {
33
- const s = String(p);
34
- return s.endsWith(".sweny/skills") || s.endsWith("tax-tool/SKILL.md");
35
- });
36
- mockedReaddirSync.mockReturnValue([{ name: "tax-tool", isDirectory: () => true }]);
37
- mockedReadFileSync.mockReturnValue(skillMd);
38
- const skills = discoverSkills("/fake");
39
- expect(skills).toHaveLength(1);
40
- const skill = skills[0];
41
- expect(skill.id).toBe("tax-tool");
42
- expect(skill.config).toEqual({
43
- MY_TAX_API_KEY: {
44
- description: "API key for the tax service",
45
- required: true,
46
- env: "MY_TAX_API_KEY",
47
- },
48
- MY_TAX_REGION: {
49
- description: "Tax region code",
50
- required: false,
51
- env: "MY_TAX_REGION",
52
- },
53
- });
54
- });
55
- it("produces config: {} when no config block in frontmatter", () => {
56
- const skillMd = `---
57
- name: simple-skill
58
- description: A simple skill
59
- ---
60
- Instructions here.
61
- `;
62
- mockedExistsSync.mockImplementation((p) => {
63
- const s = String(p);
64
- return s.endsWith(".sweny/skills") || s.endsWith("simple-skill/SKILL.md");
65
- });
66
- mockedReaddirSync.mockReturnValue([{ name: "simple-skill", isDirectory: () => true }]);
67
- mockedReadFileSync.mockReturnValue(skillMd);
68
- const skills = discoverSkills("/fake");
69
- expect(skills).toHaveLength(1);
70
- expect(skills[0].config).toEqual({});
71
- });
72
- it("sets env field to the key name automatically", () => {
73
- const skillMd = `---
74
- name: my-api
75
- description: API skill
76
- config:
77
- CUSTOM_API_TOKEN:
78
- description: Token for the API
79
- required: true
80
- ---
81
- Use the API.
82
- `;
83
- mockedExistsSync.mockImplementation((p) => {
84
- const s = String(p);
85
- return s.endsWith(".sweny/skills") || s.endsWith("my-api/SKILL.md");
86
- });
87
- mockedReaddirSync.mockReturnValue([{ name: "my-api", isDirectory: () => true }]);
88
- mockedReadFileSync.mockReturnValue(skillMd);
89
- const skills = discoverSkills("/fake");
90
- expect(skills[0].config.CUSTOM_API_TOKEN.env).toBe("CUSTOM_API_TOKEN");
91
- });
92
- it("handles config entries with missing description gracefully", () => {
93
- const skillMd = `---
94
- name: sparse-config
95
- description: Sparse config skill
96
- config:
97
- SOME_KEY:
98
- required: true
99
- ---
100
- Body.
101
- `;
102
- mockedExistsSync.mockImplementation((p) => {
103
- const s = String(p);
104
- return s.endsWith(".sweny/skills") || s.endsWith("sparse-config/SKILL.md");
105
- });
106
- mockedReaddirSync.mockReturnValue([{ name: "sparse-config", isDirectory: () => true }]);
107
- mockedReadFileSync.mockReturnValue(skillMd);
108
- const skills = discoverSkills("/fake");
109
- expect(skills).toHaveLength(1);
110
- // Falls back to key name as description
111
- expect(skills[0].config.SOME_KEY.description).toBe("SOME_KEY");
112
- expect(skills[0].config.SOME_KEY.required).toBe(true);
113
- expect(skills[0].config.SOME_KEY.env).toBe("SOME_KEY");
114
- });
115
- });
116
- });
@@ -1 +0,0 @@
1
- export {};