@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.
- package/dist/browser.d.ts +11 -5
- package/dist/browser.js +11 -11
- package/dist/claude.d.ts +16 -0
- package/dist/claude.js +98 -9
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.js +56 -9
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +56 -57
- package/dist/cli/marketplace.js +35 -9
- package/dist/cli/publish.d.ts +10 -0
- package/dist/cli/publish.js +22 -14
- package/dist/loader.d.ts +40 -0
- package/dist/loader.js +73 -0
- package/dist/mcp-catalog.d.ts +47 -0
- package/dist/mcp-catalog.js +304 -0
- package/dist/mcp.d.ts +6 -9
- package/dist/mcp.js +24 -314
- package/dist/schema.d.ts +183 -151
- package/dist/schema.js +49 -7
- package/dist/skills/custom-loader.d.ts +40 -0
- package/dist/skills/custom-loader.js +107 -32
- package/dist/skills/index.d.ts +13 -1
- package/dist/skills/index.js +12 -3
- package/dist/studio.js +4 -1
- package/dist/templates.d.ts +29 -8
- package/dist/templates.js +30 -20
- package/dist/types.d.ts +12 -0
- package/dist/verify.js +96 -7
- package/package.json +8 -4
- package/dist/__tests__/cascade.test.d.ts +0 -11
- package/dist/__tests__/cascade.test.js +0 -452
- package/dist/__tests__/claude.test.d.ts +0 -1
- package/dist/__tests__/claude.test.js +0 -427
- package/dist/__tests__/custom-loader.test.d.ts +0 -1
- package/dist/__tests__/custom-loader.test.js +0 -116
- package/dist/__tests__/executor.test.d.ts +0 -1
- package/dist/__tests__/executor.test.js +0 -1635
- package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/datadog.integration.test.js +0 -23
- package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
- package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/github.integration.test.js +0 -37
- package/dist/__tests__/integration/harness.d.ts +0 -24
- package/dist/__tests__/integration/harness.js +0 -34
- package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/linear.integration.test.js +0 -15
- package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sentry.integration.test.js +0 -20
- package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/slack.integration.test.js +0 -22
- package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sources.integration.test.js +0 -123
- package/dist/__tests__/publish.test.d.ts +0 -1
- package/dist/__tests__/publish.test.js +0 -180
- package/dist/__tests__/requires.test.d.ts +0 -1
- package/dist/__tests__/requires.test.js +0 -82
- package/dist/__tests__/retry.test.d.ts +0 -1
- package/dist/__tests__/retry.test.js +0 -213
- package/dist/__tests__/schema.test.d.ts +0 -1
- package/dist/__tests__/schema.test.js +0 -600
- package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
- package/dist/__tests__/skill-config-integration.test.js +0 -204
- package/dist/__tests__/skills-index.test.d.ts +0 -1
- package/dist/__tests__/skills-index.test.js +0 -119
- package/dist/__tests__/skills.test.d.ts +0 -1
- package/dist/__tests__/skills.test.js +0 -565
- package/dist/__tests__/spec-conformance.test.d.ts +0 -18
- package/dist/__tests__/spec-conformance.test.js +0 -1084
- package/dist/__tests__/studio.test.d.ts +0 -1
- package/dist/__tests__/studio.test.js +0 -403
- package/dist/__tests__/templates.test.d.ts +0 -1
- package/dist/__tests__/templates.test.js +0 -49
- package/dist/__tests__/testing.test.d.ts +0 -1
- package/dist/__tests__/testing.test.js +0 -242
- package/dist/__tests__/verify.test.d.ts +0 -1
- package/dist/__tests__/verify.test.js +0 -517
- package/dist/__tests__/version-consistency.test.d.ts +0 -1
- package/dist/__tests__/version-consistency.test.js +0 -312
- package/dist/__tests__/workflow-builder.test.d.ts +0 -1
- package/dist/__tests__/workflow-builder.test.js +0 -110
- package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
- package/dist/__tests__/workflow-yaml.test.js +0 -201
- package/dist/cli/__tests__/diagram.test.d.ts +0 -1
- package/dist/cli/__tests__/diagram.test.js +0 -225
- package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
- package/dist/cli/__tests__/report-cloud.test.js +0 -165
- package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
- package/dist/cli/__tests__/upgrade.test.js +0 -317
- package/dist/cli/__tests__/version-check.test.d.ts +0 -1
- package/dist/cli/__tests__/version-check.test.js +0 -209
- package/dist/cli/config.test.d.ts +0 -1
- package/dist/cli/config.test.js +0 -200
- package/dist/cli/credentials.test.d.ts +0 -1
- package/dist/cli/credentials.test.js +0 -172
- package/dist/cli/e2e.test.d.ts +0 -1
- package/dist/cli/e2e.test.js +0 -690
- package/dist/cli/marketplace.test.d.ts +0 -1
- package/dist/cli/marketplace.test.js +0 -355
- package/dist/cli/new.test.d.ts +0 -1
- package/dist/cli/new.test.js +0 -1043
- package/dist/cli/output.test.d.ts +0 -1
- package/dist/cli/output.test.js +0 -180
- package/dist/cli/publish.test.d.ts +0 -1
- package/dist/cli/publish.test.js +0 -610
- package/dist/cli/renderer.test.d.ts +0 -1
- package/dist/cli/renderer.test.js +0 -302
- package/dist/executor.test.d.ts +0 -1
- package/dist/executor.test.js +0 -618
- package/dist/mcp.test.d.ts +0 -1
- package/dist/mcp.test.js +0 -735
- package/dist/mermaid.test.d.ts +0 -1
- package/dist/mermaid.test.js +0 -256
- package/dist/sources.test.d.ts +0 -1
- package/dist/sources.test.js +0 -265
- package/dist/sources.validation.test.d.ts +0 -12
- package/dist/sources.validation.test.js +0 -380
- package/dist/workflow-builder.test.d.ts +0 -1
- package/dist/workflow-builder.test.js +0 -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 {};
|