ai-functions 2.0.2 → 2.1.3
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/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +361 -159
- package/dist/ai-promise.d.ts +47 -0
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +291 -3
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai.d.ts +17 -18
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +93 -39
- package/dist/ai.js.map +1 -1
- package/dist/batch-map.d.ts +46 -4
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +35 -2
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +116 -12
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +47 -2
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts +272 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +500 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache.d.ts +272 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +412 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +32 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +16 -1
- package/dist/context.js.map +1 -1
- package/dist/eval/runner.d.ts +2 -1
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js.map +1 -1
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +6 -10
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +27 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -42
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +17 -0
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +19 -1
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +303 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +539 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -9
- package/dist/schema.js.map +1 -1
- package/dist/tool-orchestration.d.ts +391 -0
- package/dist/tool-orchestration.d.ts.map +1 -0
- package/dist/tool-orchestration.js +663 -0
- package/dist/tool-orchestration.js.map +1 -0
- package/dist/types.d.ts +50 -33
- package/dist/types.d.ts.map +1 -1
- package/evalite.config.js +14 -0
- package/evals/classification.eval.js +97 -0
- package/evals/marketing.eval.js +289 -0
- package/evals/math.eval.js +83 -0
- package/evals/run-evals.js +151 -0
- package/evals/structured-output.eval.js +131 -0
- package/evals/writing.eval.js +105 -0
- package/examples/batch-blog-posts.js +128 -0
- package/package.json +26 -26
- package/src/ai-promise.ts +359 -3
- package/src/ai.ts +155 -110
- package/src/batch/anthropic.js +256 -0
- package/src/batch/bedrock.js +584 -0
- package/src/batch/cloudflare.js +287 -0
- package/src/batch/google.js +359 -0
- package/src/batch/index.js +30 -0
- package/src/batch/memory.js +187 -0
- package/src/batch/openai.js +402 -0
- package/src/batch-map.ts +46 -4
- package/src/batch-queue.ts +116 -12
- package/src/budget.ts +727 -0
- package/src/cache.ts +653 -0
- package/src/context.ts +33 -1
- package/src/eval/index.js +7 -0
- package/src/eval/models.js +119 -0
- package/src/eval/runner.js +147 -0
- package/src/eval/runner.ts +3 -2
- package/src/generate.ts +7 -12
- package/src/index.ts +231 -53
- package/src/primitives.ts +19 -1
- package/src/retry.ts +776 -0
- package/src/schema.ts +1 -10
- package/src/tool-orchestration.ts +1008 -0
- package/src/types.ts +59 -41
- package/test/ai-proxy.test.js +157 -0
- package/test/async-iterators.test.js +261 -0
- package/test/backward-compat.test.ts +147 -0
- package/test/batch-autosubmit-errors.test.ts +598 -0
- package/test/batch-background.test.js +352 -0
- package/test/batch-blog-posts.test.js +293 -0
- package/test/blog-generation.test.js +390 -0
- package/test/browse-read.test.js +480 -0
- package/test/budget-tracking.test.ts +800 -0
- package/test/cache.test.ts +712 -0
- package/test/context-isolation.test.ts +687 -0
- package/test/core-functions.test.js +490 -0
- package/test/decide.test.js +260 -0
- package/test/define.test.js +232 -0
- package/test/e2e-bedrock-manual.js +136 -0
- package/test/e2e-bedrock.test.js +164 -0
- package/test/e2e-flex-gateway.js +131 -0
- package/test/e2e-flex-manual.js +156 -0
- package/test/e2e-flex.test.js +174 -0
- package/test/e2e-google-manual.js +150 -0
- package/test/e2e-google.test.js +181 -0
- package/test/embeddings.test.js +220 -0
- package/test/evals/define-function.eval.test.js +309 -0
- package/test/evals/deterministic.eval.test.ts +376 -0
- package/test/evals/primitives.eval.test.js +360 -0
- package/test/function-types.test.js +407 -0
- package/test/generate-core.test.js +213 -0
- package/test/generate.test.js +143 -0
- package/test/generic-order.test.ts +342 -0
- package/test/implicit-batch.test.js +326 -0
- package/test/json-parse-error-handling.test.ts +463 -0
- package/test/retry.test.ts +1016 -0
- package/test/schema.test.js +96 -0
- package/test/streaming.test.ts +316 -0
- package/test/tagged-templates.test.js +240 -0
- package/test/tool-orchestration.test.ts +770 -0
- package/vitest.config.js +39 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for JSON.parse error handling in agent loop
|
|
3
|
+
*
|
|
4
|
+
* TDD RED PHASE: These tests expose missing try-catch around JSON.parse
|
|
5
|
+
* at ai.ts:567 in executeAgenticFunction.
|
|
6
|
+
*
|
|
7
|
+
* Issue: primitives.org.ai-drc
|
|
8
|
+
*
|
|
9
|
+
* The JSON.parse call at line 567 throws unhandled exceptions when the AI
|
|
10
|
+
* returns malformed JSON in toolCall.arguments, crashing the entire agent loop.
|
|
11
|
+
*
|
|
12
|
+
* Expected behavior after fix (GREEN phase):
|
|
13
|
+
* - Malformed JSON should be caught and result in a tool error
|
|
14
|
+
* - The agent loop should continue or gracefully fail with a meaningful error
|
|
15
|
+
* - No unhandled exceptions should crash the process
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Direct test of JSON.parse behavior to document the vulnerability
|
|
22
|
+
*
|
|
23
|
+
* This test suite demonstrates the exact error that occurs when
|
|
24
|
+
* JSON.parse receives malformed input - the same error that will
|
|
25
|
+
* crash executeAgenticFunction at ai.ts:567.
|
|
26
|
+
*/
|
|
27
|
+
describe('JSON.parse vulnerability demonstration', () => {
|
|
28
|
+
/**
|
|
29
|
+
* This is the exact line from ai.ts:567:
|
|
30
|
+
* const toolArgs = JSON.parse(response.toolCall.arguments || '{}')
|
|
31
|
+
*
|
|
32
|
+
* When response.toolCall.arguments contains malformed JSON,
|
|
33
|
+
* this throws an unhandled SyntaxError.
|
|
34
|
+
*/
|
|
35
|
+
describe('malformed JSON throws SyntaxError', () => {
|
|
36
|
+
it('throws on unclosed string', () => {
|
|
37
|
+
const malformedJson = '{"query": "test'
|
|
38
|
+
expect(() => JSON.parse(malformedJson)).toThrow(SyntaxError)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('throws on truncated JSON (token limit scenario)', () => {
|
|
42
|
+
const truncatedJson = '{"query": "find information about artificial intelli'
|
|
43
|
+
expect(() => JSON.parse(truncatedJson)).toThrow(SyntaxError)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('throws on invalid escape sequences', () => {
|
|
47
|
+
// Invalid escape \x is not valid JSON
|
|
48
|
+
const invalidEscapes = '{"data": "test\\xvalue"}'
|
|
49
|
+
expect(() => JSON.parse(invalidEscapes)).toThrow(SyntaxError)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('throws on trailing commas', () => {
|
|
53
|
+
const trailingComma = '{"a": "value",}'
|
|
54
|
+
expect(() => JSON.parse(trailingComma)).toThrow(SyntaxError)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('throws on single quotes (JavaScript-style)', () => {
|
|
58
|
+
const singleQuotes = "{'text': 'hello'}"
|
|
59
|
+
expect(() => JSON.parse(singleQuotes)).toThrow(SyntaxError)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('throws on incomplete nested objects', () => {
|
|
63
|
+
const incomplete = '{"config": {"nested": {"deep": {"value": "test"'
|
|
64
|
+
expect(() => JSON.parse(incomplete)).toThrow(SyntaxError)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('throws on plain text instead of JSON', () => {
|
|
68
|
+
const plainText = 'Just some plain text instead of JSON'
|
|
69
|
+
expect(() => JSON.parse(plainText)).toThrow(SyntaxError)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('throws on JSON with comments', () => {
|
|
73
|
+
const withComments = `{
|
|
74
|
+
// this is a comment
|
|
75
|
+
"value": 42
|
|
76
|
+
}`
|
|
77
|
+
expect(() => JSON.parse(withComments)).toThrow(SyntaxError)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('throws on invalid unicode escapes', () => {
|
|
81
|
+
const badUnicode = '{"text": "\\uXXXX invalid"}'
|
|
82
|
+
expect(() => JSON.parse(badUnicode)).toThrow(SyntaxError)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* These inputs work with the fallback '|| "{}"' but demonstrate
|
|
88
|
+
* the fragility of the current approach.
|
|
89
|
+
*/
|
|
90
|
+
describe('fallback behavior with || "{}"', () => {
|
|
91
|
+
it('empty string falls back to {}', () => {
|
|
92
|
+
const args = '' || '{}'
|
|
93
|
+
expect(() => JSON.parse(args)).not.toThrow()
|
|
94
|
+
expect(JSON.parse(args)).toEqual({})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('null falls back to {}', () => {
|
|
98
|
+
const args = (null as unknown as string) || '{}'
|
|
99
|
+
expect(() => JSON.parse(args)).not.toThrow()
|
|
100
|
+
expect(JSON.parse(args)).toEqual({})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('undefined falls back to {}', () => {
|
|
104
|
+
const args = (undefined as unknown as string) || '{}'
|
|
105
|
+
expect(() => JSON.parse(args)).not.toThrow()
|
|
106
|
+
expect(JSON.parse(args)).toEqual({})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('malformed string does NOT fall back (bug!)', () => {
|
|
110
|
+
// This is the bug: non-empty malformed string doesn't trigger fallback
|
|
111
|
+
const malformed = '{"broken'
|
|
112
|
+
const args = malformed || '{}'
|
|
113
|
+
expect(args).toBe('{"broken') // fallback NOT used because string is truthy
|
|
114
|
+
expect(() => JSON.parse(args)).toThrow(SyntaxError) // CRASH!
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Integration test using the actual executeAgenticFunction pathway
|
|
121
|
+
*
|
|
122
|
+
* These tests require mocking generateObject which is difficult due to
|
|
123
|
+
* module structure. For now, we document the expected behavior.
|
|
124
|
+
*/
|
|
125
|
+
describe('executeAgenticFunction JSON.parse error scenarios', () => {
|
|
126
|
+
/**
|
|
127
|
+
* Test scenario documentation for executeAgenticFunction at ai.ts:567
|
|
128
|
+
*
|
|
129
|
+
* Line 567: const toolArgs = JSON.parse(response.toolCall.arguments || '{}')
|
|
130
|
+
*
|
|
131
|
+
* The AI model generates a response like:
|
|
132
|
+
* {
|
|
133
|
+
* thinking: "I'll call the tool",
|
|
134
|
+
* toolCall: {
|
|
135
|
+
* name: "searchTool",
|
|
136
|
+
* arguments: '{"query": "test' // <-- MALFORMED!
|
|
137
|
+
* },
|
|
138
|
+
* finalResult: null
|
|
139
|
+
* }
|
|
140
|
+
*
|
|
141
|
+
* When arguments contains malformed JSON:
|
|
142
|
+
* 1. The || '{}' fallback doesn't help (string is truthy)
|
|
143
|
+
* 2. JSON.parse throws SyntaxError
|
|
144
|
+
* 3. SyntaxError bubbles up unhandled
|
|
145
|
+
* 4. The entire agent loop crashes
|
|
146
|
+
* 5. The user gets a cryptic "Unexpected end of JSON input" error
|
|
147
|
+
*/
|
|
148
|
+
describe('documented scenarios requiring fix', () => {
|
|
149
|
+
it('scenario: AI truncates response at token limit', () => {
|
|
150
|
+
// When the AI runs out of tokens, it may produce:
|
|
151
|
+
const truncatedResponse = {
|
|
152
|
+
thinking: 'I will search for the information',
|
|
153
|
+
toolCall: {
|
|
154
|
+
name: 'searchTool',
|
|
155
|
+
arguments: '{"query": "find detailed information about machine learning and artific',
|
|
156
|
+
},
|
|
157
|
+
finalResult: null,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Current behavior: CRASH
|
|
161
|
+
expect(() => JSON.parse(truncatedResponse.toolCall.arguments)).toThrow(SyntaxError)
|
|
162
|
+
|
|
163
|
+
// Expected behavior after fix:
|
|
164
|
+
// - Should catch SyntaxError
|
|
165
|
+
// - Should add error to toolResults: { error: 'Malformed JSON in tool arguments' }
|
|
166
|
+
// - Should continue the agent loop or gracefully fail
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('scenario: AI uses wrong JSON format (single quotes)', () => {
|
|
170
|
+
// Some AI models prefer Python/JavaScript style
|
|
171
|
+
const pythonStyleResponse = {
|
|
172
|
+
thinking: 'Calling the API',
|
|
173
|
+
toolCall: {
|
|
174
|
+
name: 'apiTool',
|
|
175
|
+
arguments: "{'endpoint': '/users', 'method': 'GET'}",
|
|
176
|
+
},
|
|
177
|
+
finalResult: null,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Current behavior: CRASH
|
|
181
|
+
expect(() => JSON.parse(pythonStyleResponse.toolCall.arguments)).toThrow(SyntaxError)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('scenario: AI includes explanation in arguments', () => {
|
|
185
|
+
// AI sometimes adds explanatory text
|
|
186
|
+
const explainedResponse = {
|
|
187
|
+
thinking: 'Let me call the tool',
|
|
188
|
+
toolCall: {
|
|
189
|
+
name: 'dataTool',
|
|
190
|
+
// AI mistakenly added comment/explanation
|
|
191
|
+
arguments: `Here are the arguments: {"key": "value"}`,
|
|
192
|
+
},
|
|
193
|
+
finalResult: null,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Current behavior: CRASH
|
|
197
|
+
expect(() => JSON.parse(explainedResponse.toolCall.arguments)).toThrow(SyntaxError)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('scenario: AI generates malformed nested structure', () => {
|
|
201
|
+
// Complex nested objects are more prone to errors
|
|
202
|
+
const nestedResponse = {
|
|
203
|
+
thinking: 'Building configuration',
|
|
204
|
+
toolCall: {
|
|
205
|
+
name: 'configTool',
|
|
206
|
+
arguments: '{"config": {"database": {"host": "localhost", "port": 5432, "options": {"ssl": true,',
|
|
207
|
+
},
|
|
208
|
+
finalResult: null,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Current behavior: CRASH
|
|
212
|
+
expect(() => JSON.parse(nestedResponse.toolCall.arguments)).toThrow(SyntaxError)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('scenario: AI uses trailing commas (common mistake)', () => {
|
|
216
|
+
// Trailing commas are valid in JavaScript but not JSON
|
|
217
|
+
const trailingCommaResponse = {
|
|
218
|
+
thinking: 'Setting options',
|
|
219
|
+
toolCall: {
|
|
220
|
+
name: 'optionsTool',
|
|
221
|
+
arguments: '{"option1": true, "option2": false,}',
|
|
222
|
+
},
|
|
223
|
+
finalResult: null,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Current behavior: CRASH
|
|
227
|
+
expect(() => JSON.parse(trailingCommaResponse.toolCall.arguments)).toThrow(SyntaxError)
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Proposed fix validation tests
|
|
234
|
+
*
|
|
235
|
+
* These tests define the expected behavior AFTER the fix is applied.
|
|
236
|
+
* They will fail now (RED) and pass after implementing try-catch (GREEN).
|
|
237
|
+
*/
|
|
238
|
+
describe('expected behavior after fix (GREEN phase targets)', () => {
|
|
239
|
+
/**
|
|
240
|
+
* Helper to simulate what the fixed code should do
|
|
241
|
+
*/
|
|
242
|
+
function safeParseToolArgs(argsString: string | null | undefined): {
|
|
243
|
+
success: boolean
|
|
244
|
+
args?: Record<string, unknown>
|
|
245
|
+
error?: string
|
|
246
|
+
} {
|
|
247
|
+
const fallback = argsString || '{}'
|
|
248
|
+
try {
|
|
249
|
+
return { success: true, args: JSON.parse(fallback) }
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: `Failed to parse tool arguments: ${(error as Error).message}`,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
it('should return error result for malformed JSON', () => {
|
|
259
|
+
const result = safeParseToolArgs('{"broken')
|
|
260
|
+
expect(result.success).toBe(false)
|
|
261
|
+
expect(result.error).toContain('Failed to parse tool arguments')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should return parsed args for valid JSON', () => {
|
|
265
|
+
const result = safeParseToolArgs('{"valid": "json"}')
|
|
266
|
+
expect(result.success).toBe(true)
|
|
267
|
+
expect(result.args).toEqual({ valid: 'json' })
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should handle empty/null/undefined with fallback', () => {
|
|
271
|
+
expect(safeParseToolArgs('')).toEqual({ success: true, args: {} })
|
|
272
|
+
expect(safeParseToolArgs(null)).toEqual({ success: true, args: {} })
|
|
273
|
+
expect(safeParseToolArgs(undefined)).toEqual({ success: true, args: {} })
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should provide meaningful error message for truncation', () => {
|
|
277
|
+
const result = safeParseToolArgs('{"query": "incomplete')
|
|
278
|
+
expect(result.success).toBe(false)
|
|
279
|
+
expect(result.error).toMatch(/Unexpected end of JSON|Unterminated string/)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should provide meaningful error message for syntax error', () => {
|
|
283
|
+
const result = safeParseToolArgs("{'single': 'quotes'}")
|
|
284
|
+
expect(result.success).toBe(false)
|
|
285
|
+
expect(result.error).toMatch(/Unexpected token|Expected property name/)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Test the specific code path that needs fixing
|
|
291
|
+
*
|
|
292
|
+
* This simulates the exact code at ai.ts:565-569
|
|
293
|
+
*/
|
|
294
|
+
describe('ai.ts:565-569 code path simulation', () => {
|
|
295
|
+
interface MockToolCall {
|
|
296
|
+
name: string
|
|
297
|
+
arguments: string
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
interface MockTool {
|
|
301
|
+
name: string
|
|
302
|
+
handler: (args: unknown) => Promise<unknown>
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Simulates the current (buggy) code at ai.ts:565-569
|
|
306
|
+
async function executeToolCallCurrentBehavior(
|
|
307
|
+
toolCall: MockToolCall,
|
|
308
|
+
tools: MockTool[]
|
|
309
|
+
): Promise<unknown> {
|
|
310
|
+
const tool = tools.find(t => t.name === toolCall.name)
|
|
311
|
+
if (tool) {
|
|
312
|
+
// Line 567 - THE BUG: No try-catch around JSON.parse
|
|
313
|
+
const toolArgs = JSON.parse(toolCall.arguments || '{}')
|
|
314
|
+
const toolResult = await tool.handler(toolArgs)
|
|
315
|
+
return { tool: toolCall.name, result: toolResult }
|
|
316
|
+
}
|
|
317
|
+
return { error: `Tool not found: ${toolCall.name}` }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Simulates the fixed code
|
|
321
|
+
async function executeToolCallFixedBehavior(
|
|
322
|
+
toolCall: MockToolCall,
|
|
323
|
+
tools: MockTool[]
|
|
324
|
+
): Promise<unknown> {
|
|
325
|
+
const tool = tools.find(t => t.name === toolCall.name)
|
|
326
|
+
if (tool) {
|
|
327
|
+
// FIXED: Try-catch around JSON.parse
|
|
328
|
+
let toolArgs: unknown
|
|
329
|
+
try {
|
|
330
|
+
toolArgs = JSON.parse(toolCall.arguments || '{}')
|
|
331
|
+
} catch (parseError) {
|
|
332
|
+
return {
|
|
333
|
+
tool: toolCall.name,
|
|
334
|
+
error: `Invalid JSON in tool arguments: ${(parseError as Error).message}`,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const toolResult = await tool.handler(toolArgs)
|
|
338
|
+
return { tool: toolCall.name, result: toolResult }
|
|
339
|
+
}
|
|
340
|
+
return { error: `Tool not found: ${toolCall.name}` }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const mockTools: MockTool[] = [
|
|
344
|
+
{
|
|
345
|
+
name: 'testTool',
|
|
346
|
+
handler: async (args) => ({ received: args }),
|
|
347
|
+
},
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
describe('current behavior (buggy)', () => {
|
|
351
|
+
it('crashes on malformed JSON - THIS IS THE BUG', async () => {
|
|
352
|
+
const malformedToolCall: MockToolCall = {
|
|
353
|
+
name: 'testTool',
|
|
354
|
+
arguments: '{"query": "broken',
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Current code THROWS - this is the bug we're exposing
|
|
358
|
+
await expect(
|
|
359
|
+
executeToolCallCurrentBehavior(malformedToolCall, mockTools)
|
|
360
|
+
).rejects.toThrow(SyntaxError)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('works fine with valid JSON', async () => {
|
|
364
|
+
const validToolCall: MockToolCall = {
|
|
365
|
+
name: 'testTool',
|
|
366
|
+
arguments: '{"query": "valid"}',
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const result = await executeToolCallCurrentBehavior(validToolCall, mockTools)
|
|
370
|
+
expect(result).toEqual({
|
|
371
|
+
tool: 'testTool',
|
|
372
|
+
result: { received: { query: 'valid' } },
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
describe('fixed behavior (target for GREEN phase)', () => {
|
|
378
|
+
it('handles malformed JSON gracefully', async () => {
|
|
379
|
+
const malformedToolCall: MockToolCall = {
|
|
380
|
+
name: 'testTool',
|
|
381
|
+
arguments: '{"query": "broken',
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Fixed code returns error result instead of throwing
|
|
385
|
+
const result = await executeToolCallFixedBehavior(malformedToolCall, mockTools)
|
|
386
|
+
expect(result).toEqual({
|
|
387
|
+
tool: 'testTool',
|
|
388
|
+
error: expect.stringContaining('Invalid JSON'),
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('still works with valid JSON', async () => {
|
|
393
|
+
const validToolCall: MockToolCall = {
|
|
394
|
+
name: 'testTool',
|
|
395
|
+
arguments: '{"query": "valid"}',
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const result = await executeToolCallFixedBehavior(validToolCall, mockTools)
|
|
399
|
+
expect(result).toEqual({
|
|
400
|
+
tool: 'testTool',
|
|
401
|
+
result: { received: { query: 'valid' } },
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Edge cases for comprehensive coverage
|
|
409
|
+
*/
|
|
410
|
+
describe('JSON.parse edge cases', () => {
|
|
411
|
+
describe('valid JSON that should parse', () => {
|
|
412
|
+
it('handles valid unicode escapes', () => {
|
|
413
|
+
const unicode = '{"text": "Hello \\u4e16\\u754c"}'
|
|
414
|
+
expect(() => JSON.parse(unicode)).not.toThrow()
|
|
415
|
+
expect(JSON.parse(unicode)).toEqual({ text: 'Hello \u4e16\u754c' })
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('handles valid escaped characters', () => {
|
|
419
|
+
const escaped = '{"path": "C:\\\\Users\\\\test"}'
|
|
420
|
+
expect(() => JSON.parse(escaped)).not.toThrow()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('handles valid newlines in strings', () => {
|
|
424
|
+
const newlines = '{"text": "line1\\nline2"}'
|
|
425
|
+
expect(() => JSON.parse(newlines)).not.toThrow()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('handles empty object', () => {
|
|
429
|
+
expect(JSON.parse('{}')).toEqual({})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('handles deeply nested valid JSON', () => {
|
|
433
|
+
const deep = '{"a":{"b":{"c":{"d":{"e":"value"}}}}}'
|
|
434
|
+
expect(() => JSON.parse(deep)).not.toThrow()
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
describe('invalid JSON that should fail', () => {
|
|
439
|
+
it('fails on NaN', () => {
|
|
440
|
+
expect(() => JSON.parse('{"value": NaN}')).toThrow(SyntaxError)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('fails on Infinity', () => {
|
|
444
|
+
expect(() => JSON.parse('{"value": Infinity}')).toThrow(SyntaxError)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('fails on undefined value', () => {
|
|
448
|
+
expect(() => JSON.parse('{"value": undefined}')).toThrow(SyntaxError)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('fails on unquoted keys', () => {
|
|
452
|
+
expect(() => JSON.parse('{key: "value"}')).toThrow(SyntaxError)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('fails on hex numbers', () => {
|
|
456
|
+
expect(() => JSON.parse('{"value": 0xFF}')).toThrow(SyntaxError)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('fails on binary literals', () => {
|
|
460
|
+
expect(() => JSON.parse('{"value": 0b1010}')).toThrow(SyntaxError)
|
|
461
|
+
})
|
|
462
|
+
})
|
|
463
|
+
})
|