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.
Files changed (130) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/CHANGELOG.md +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +361 -159
  5. package/dist/ai-promise.d.ts +47 -0
  6. package/dist/ai-promise.d.ts.map +1 -1
  7. package/dist/ai-promise.js +291 -3
  8. package/dist/ai-promise.js.map +1 -1
  9. package/dist/ai.d.ts +17 -18
  10. package/dist/ai.d.ts.map +1 -1
  11. package/dist/ai.js +93 -39
  12. package/dist/ai.js.map +1 -1
  13. package/dist/batch-map.d.ts +46 -4
  14. package/dist/batch-map.d.ts.map +1 -1
  15. package/dist/batch-map.js +35 -2
  16. package/dist/batch-map.js.map +1 -1
  17. package/dist/batch-queue.d.ts +116 -12
  18. package/dist/batch-queue.d.ts.map +1 -1
  19. package/dist/batch-queue.js +47 -2
  20. package/dist/batch-queue.js.map +1 -1
  21. package/dist/budget.d.ts +272 -0
  22. package/dist/budget.d.ts.map +1 -0
  23. package/dist/budget.js +500 -0
  24. package/dist/budget.js.map +1 -0
  25. package/dist/cache.d.ts +272 -0
  26. package/dist/cache.d.ts.map +1 -0
  27. package/dist/cache.js +412 -0
  28. package/dist/cache.js.map +1 -0
  29. package/dist/context.d.ts +32 -1
  30. package/dist/context.d.ts.map +1 -1
  31. package/dist/context.js +16 -1
  32. package/dist/context.js.map +1 -1
  33. package/dist/eval/runner.d.ts +2 -1
  34. package/dist/eval/runner.d.ts.map +1 -1
  35. package/dist/eval/runner.js.map +1 -1
  36. package/dist/generate.d.ts.map +1 -1
  37. package/dist/generate.js +6 -10
  38. package/dist/generate.js.map +1 -1
  39. package/dist/index.d.ts +27 -20
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +72 -42
  42. package/dist/index.js.map +1 -1
  43. package/dist/primitives.d.ts +17 -0
  44. package/dist/primitives.d.ts.map +1 -1
  45. package/dist/primitives.js +19 -1
  46. package/dist/primitives.js.map +1 -1
  47. package/dist/retry.d.ts +303 -0
  48. package/dist/retry.d.ts.map +1 -0
  49. package/dist/retry.js +539 -0
  50. package/dist/retry.js.map +1 -0
  51. package/dist/schema.d.ts.map +1 -1
  52. package/dist/schema.js +1 -9
  53. package/dist/schema.js.map +1 -1
  54. package/dist/tool-orchestration.d.ts +391 -0
  55. package/dist/tool-orchestration.d.ts.map +1 -0
  56. package/dist/tool-orchestration.js +663 -0
  57. package/dist/tool-orchestration.js.map +1 -0
  58. package/dist/types.d.ts +50 -33
  59. package/dist/types.d.ts.map +1 -1
  60. package/evalite.config.js +14 -0
  61. package/evals/classification.eval.js +97 -0
  62. package/evals/marketing.eval.js +289 -0
  63. package/evals/math.eval.js +83 -0
  64. package/evals/run-evals.js +151 -0
  65. package/evals/structured-output.eval.js +131 -0
  66. package/evals/writing.eval.js +105 -0
  67. package/examples/batch-blog-posts.js +128 -0
  68. package/package.json +26 -26
  69. package/src/ai-promise.ts +359 -3
  70. package/src/ai.ts +155 -110
  71. package/src/batch/anthropic.js +256 -0
  72. package/src/batch/bedrock.js +584 -0
  73. package/src/batch/cloudflare.js +287 -0
  74. package/src/batch/google.js +359 -0
  75. package/src/batch/index.js +30 -0
  76. package/src/batch/memory.js +187 -0
  77. package/src/batch/openai.js +402 -0
  78. package/src/batch-map.ts +46 -4
  79. package/src/batch-queue.ts +116 -12
  80. package/src/budget.ts +727 -0
  81. package/src/cache.ts +653 -0
  82. package/src/context.ts +33 -1
  83. package/src/eval/index.js +7 -0
  84. package/src/eval/models.js +119 -0
  85. package/src/eval/runner.js +147 -0
  86. package/src/eval/runner.ts +3 -2
  87. package/src/generate.ts +7 -12
  88. package/src/index.ts +231 -53
  89. package/src/primitives.ts +19 -1
  90. package/src/retry.ts +776 -0
  91. package/src/schema.ts +1 -10
  92. package/src/tool-orchestration.ts +1008 -0
  93. package/src/types.ts +59 -41
  94. package/test/ai-proxy.test.js +157 -0
  95. package/test/async-iterators.test.js +261 -0
  96. package/test/backward-compat.test.ts +147 -0
  97. package/test/batch-autosubmit-errors.test.ts +598 -0
  98. package/test/batch-background.test.js +352 -0
  99. package/test/batch-blog-posts.test.js +293 -0
  100. package/test/blog-generation.test.js +390 -0
  101. package/test/browse-read.test.js +480 -0
  102. package/test/budget-tracking.test.ts +800 -0
  103. package/test/cache.test.ts +712 -0
  104. package/test/context-isolation.test.ts +687 -0
  105. package/test/core-functions.test.js +490 -0
  106. package/test/decide.test.js +260 -0
  107. package/test/define.test.js +232 -0
  108. package/test/e2e-bedrock-manual.js +136 -0
  109. package/test/e2e-bedrock.test.js +164 -0
  110. package/test/e2e-flex-gateway.js +131 -0
  111. package/test/e2e-flex-manual.js +156 -0
  112. package/test/e2e-flex.test.js +174 -0
  113. package/test/e2e-google-manual.js +150 -0
  114. package/test/e2e-google.test.js +181 -0
  115. package/test/embeddings.test.js +220 -0
  116. package/test/evals/define-function.eval.test.js +309 -0
  117. package/test/evals/deterministic.eval.test.ts +376 -0
  118. package/test/evals/primitives.eval.test.js +360 -0
  119. package/test/function-types.test.js +407 -0
  120. package/test/generate-core.test.js +213 -0
  121. package/test/generate.test.js +143 -0
  122. package/test/generic-order.test.ts +342 -0
  123. package/test/implicit-batch.test.js +326 -0
  124. package/test/json-parse-error-handling.test.ts +463 -0
  125. package/test/retry.test.ts +1016 -0
  126. package/test/schema.test.js +96 -0
  127. package/test/streaming.test.ts +316 -0
  128. package/test/tagged-templates.test.js +240 -0
  129. package/test/tool-orchestration.test.ts +770 -0
  130. 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
+ })