@sweny-ai/core 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +16 -0
  4. package/dist/claude.js +98 -9
  5. package/dist/cli/config.d.ts +22 -0
  6. package/dist/cli/config.js +56 -9
  7. package/dist/cli/main.d.ts +1 -1
  8. package/dist/cli/main.js +56 -57
  9. package/dist/cli/marketplace.js +35 -9
  10. package/dist/cli/publish.d.ts +10 -0
  11. package/dist/cli/publish.js +22 -14
  12. package/dist/loader.d.ts +40 -0
  13. package/dist/loader.js +73 -0
  14. package/dist/mcp-catalog.d.ts +47 -0
  15. package/dist/mcp-catalog.js +304 -0
  16. package/dist/mcp.d.ts +6 -9
  17. package/dist/mcp.js +24 -314
  18. package/dist/schema.d.ts +183 -151
  19. package/dist/schema.js +49 -7
  20. package/dist/skills/custom-loader.d.ts +40 -0
  21. package/dist/skills/custom-loader.js +107 -32
  22. package/dist/skills/index.d.ts +13 -1
  23. package/dist/skills/index.js +12 -3
  24. package/dist/studio.js +4 -1
  25. package/dist/templates.d.ts +29 -8
  26. package/dist/templates.js +30 -20
  27. package/dist/types.d.ts +12 -0
  28. package/dist/verify.js +96 -7
  29. package/package.json +8 -4
  30. package/dist/__tests__/cascade.test.d.ts +0 -11
  31. package/dist/__tests__/cascade.test.js +0 -452
  32. package/dist/__tests__/claude.test.d.ts +0 -1
  33. package/dist/__tests__/claude.test.js +0 -427
  34. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  35. package/dist/__tests__/custom-loader.test.js +0 -116
  36. package/dist/__tests__/executor.test.d.ts +0 -1
  37. package/dist/__tests__/executor.test.js +0 -1635
  38. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  39. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  40. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  41. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  42. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  43. package/dist/__tests__/integration/github.integration.test.js +0 -37
  44. package/dist/__tests__/integration/harness.d.ts +0 -24
  45. package/dist/__tests__/integration/harness.js +0 -34
  46. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  48. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  50. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  52. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  53. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  54. package/dist/__tests__/publish.test.d.ts +0 -1
  55. package/dist/__tests__/publish.test.js +0 -180
  56. package/dist/__tests__/requires.test.d.ts +0 -1
  57. package/dist/__tests__/requires.test.js +0 -82
  58. package/dist/__tests__/retry.test.d.ts +0 -1
  59. package/dist/__tests__/retry.test.js +0 -213
  60. package/dist/__tests__/schema.test.d.ts +0 -1
  61. package/dist/__tests__/schema.test.js +0 -600
  62. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  63. package/dist/__tests__/skill-config-integration.test.js +0 -204
  64. package/dist/__tests__/skills-index.test.d.ts +0 -1
  65. package/dist/__tests__/skills-index.test.js +0 -119
  66. package/dist/__tests__/skills.test.d.ts +0 -1
  67. package/dist/__tests__/skills.test.js +0 -565
  68. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  69. package/dist/__tests__/spec-conformance.test.js +0 -1084
  70. package/dist/__tests__/studio.test.d.ts +0 -1
  71. package/dist/__tests__/studio.test.js +0 -403
  72. package/dist/__tests__/templates.test.d.ts +0 -1
  73. package/dist/__tests__/templates.test.js +0 -49
  74. package/dist/__tests__/testing.test.d.ts +0 -1
  75. package/dist/__tests__/testing.test.js +0 -242
  76. package/dist/__tests__/verify.test.d.ts +0 -1
  77. package/dist/__tests__/verify.test.js +0 -517
  78. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  79. package/dist/__tests__/version-consistency.test.js +0 -312
  80. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  81. package/dist/__tests__/workflow-builder.test.js +0 -110
  82. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  83. package/dist/__tests__/workflow-yaml.test.js +0 -201
  84. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  85. package/dist/cli/__tests__/diagram.test.js +0 -225
  86. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  87. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  88. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  89. package/dist/cli/__tests__/upgrade.test.js +0 -317
  90. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  91. package/dist/cli/__tests__/version-check.test.js +0 -209
  92. package/dist/cli/config.test.d.ts +0 -1
  93. package/dist/cli/config.test.js +0 -200
  94. package/dist/cli/credentials.test.d.ts +0 -1
  95. package/dist/cli/credentials.test.js +0 -172
  96. package/dist/cli/e2e.test.d.ts +0 -1
  97. package/dist/cli/e2e.test.js +0 -690
  98. package/dist/cli/marketplace.test.d.ts +0 -1
  99. package/dist/cli/marketplace.test.js +0 -355
  100. package/dist/cli/new.test.d.ts +0 -1
  101. package/dist/cli/new.test.js +0 -1043
  102. package/dist/cli/output.test.d.ts +0 -1
  103. package/dist/cli/output.test.js +0 -180
  104. package/dist/cli/publish.test.d.ts +0 -1
  105. package/dist/cli/publish.test.js +0 -610
  106. package/dist/cli/renderer.test.d.ts +0 -1
  107. package/dist/cli/renderer.test.js +0 -302
  108. package/dist/executor.test.d.ts +0 -1
  109. package/dist/executor.test.js +0 -618
  110. package/dist/mcp.test.d.ts +0 -1
  111. package/dist/mcp.test.js +0 -735
  112. package/dist/mermaid.test.d.ts +0 -1
  113. package/dist/mermaid.test.js +0 -256
  114. package/dist/sources.test.d.ts +0 -1
  115. package/dist/sources.test.js +0 -265
  116. package/dist/sources.validation.test.d.ts +0 -12
  117. package/dist/sources.validation.test.js +0 -380
  118. package/dist/workflow-builder.test.d.ts +0 -1
  119. package/dist/workflow-builder.test.js +0 -123
@@ -1,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 {};