@open-mercato/ai-assistant 0.4.9-develop-de525127d6 → 0.4.9-develop-31d1a87765

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 (41) hide show
  1. package/AGENTS.md +78 -37
  2. package/dist/modules/ai_assistant/api/chat/route.js +104 -13
  3. package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
  4. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +97 -0
  5. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
  6. package/dist/modules/ai_assistant/lib/codemode-tools.js +610 -0
  7. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +7 -0
  8. package/dist/modules/ai_assistant/lib/http-server.js +65 -7
  9. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  10. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +16 -1
  11. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  12. package/dist/modules/ai_assistant/lib/mcp-server.js +17 -0
  13. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  14. package/dist/modules/ai_assistant/lib/opencode-handlers.js +32 -0
  15. package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +2 -2
  16. package/dist/modules/ai_assistant/lib/sandbox.js +124 -0
  17. package/dist/modules/ai_assistant/lib/sandbox.js.map +7 -0
  18. package/dist/modules/ai_assistant/lib/session-memory.js +103 -0
  19. package/dist/modules/ai_assistant/lib/session-memory.js.map +7 -0
  20. package/dist/modules/ai_assistant/lib/tool-loader.js +4 -114
  21. package/dist/modules/ai_assistant/lib/tool-loader.js.map +3 -3
  22. package/dist/modules/ai_assistant/lib/truncate.js +26 -0
  23. package/dist/modules/ai_assistant/lib/truncate.js.map +7 -0
  24. package/jest.config.cjs +23 -0
  25. package/package.json +6 -5
  26. package/src/modules/ai_assistant/api/chat/route.ts +110 -14
  27. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +129 -0
  28. package/src/modules/ai_assistant/lib/__tests__/sandbox.test.ts +642 -0
  29. package/src/modules/ai_assistant/lib/__tests__/session-memory.test.ts +82 -0
  30. package/src/modules/ai_assistant/lib/__tests__/truncate.test.ts +76 -0
  31. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +143 -0
  32. package/src/modules/ai_assistant/lib/codemode-tools.ts +864 -0
  33. package/src/modules/ai_assistant/lib/http-server.ts +86 -9
  34. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +19 -0
  35. package/src/modules/ai_assistant/lib/mcp-server.ts +21 -0
  36. package/src/modules/ai_assistant/lib/opencode-handlers.ts +40 -0
  37. package/src/modules/ai_assistant/lib/sandbox.ts +192 -0
  38. package/src/modules/ai_assistant/lib/session-memory.ts +174 -0
  39. package/src/modules/ai_assistant/lib/tool-loader.ts +11 -145
  40. package/src/modules/ai_assistant/lib/truncate.ts +45 -0
  41. package/src/modules/ai_assistant/lib/types.ts +2 -0
@@ -0,0 +1,642 @@
1
+ import { createSandbox, normalizeCode } from '../sandbox'
2
+
3
+ describe('normalizeCode', () => {
4
+ it('strips markdown code fences', () => {
5
+ expect(normalizeCode('```javascript\nasync () => 42\n```')).toBe('async () => 42')
6
+ })
7
+
8
+ it('strips ts fences', () => {
9
+ expect(normalizeCode('```ts\nasync () => 42\n```')).toBe('async () => 42')
10
+ })
11
+
12
+ it('auto-wraps bare expressions into async arrow functions', () => {
13
+ expect(normalizeCode('spec.paths')).toBe('async () => { return spec.paths }')
14
+ })
15
+
16
+ it('auto-wraps Object.keys(...) calls', () => {
17
+ expect(normalizeCode('Object.keys(spec.paths)')).toBe(
18
+ 'async () => { return Object.keys(spec.paths) }'
19
+ )
20
+ })
21
+
22
+ it('preserves async arrow functions', () => {
23
+ expect(normalizeCode('async () => spec.paths')).toBe('async () => spec.paths')
24
+ })
25
+
26
+ it('preserves async arrow functions with body', () => {
27
+ const code = 'async () => { const x = 1; return x }'
28
+ expect(normalizeCode(code)).toBe(code)
29
+ })
30
+
31
+ it('trims whitespace', () => {
32
+ expect(normalizeCode(' async () => 42 ')).toBe('async () => 42')
33
+ })
34
+
35
+ it('wraps const declarations without auto-return', () => {
36
+ expect(normalizeCode('const x = 1; return x')).toBe(
37
+ 'async () => { const x = 1; return x }'
38
+ )
39
+ })
40
+
41
+ it('wraps let declarations without auto-return', () => {
42
+ expect(normalizeCode('let x = 1; x')).toBe('async () => { let x = 1; x }')
43
+ })
44
+
45
+ it('wraps var declarations without auto-return', () => {
46
+ expect(normalizeCode('var x = 1')).toBe('async () => { var x = 1 }')
47
+ })
48
+
49
+ it('wraps for loops without auto-return', () => {
50
+ expect(normalizeCode('for (const x of [1]) { console.log(x) }')).toBe(
51
+ 'async () => { for (const x of [1]) { console.log(x) } }'
52
+ )
53
+ })
54
+
55
+ it('wraps if statements without auto-return', () => {
56
+ expect(normalizeCode('if (true) { return 1 }')).toBe(
57
+ 'async () => { if (true) { return 1 } }'
58
+ )
59
+ })
60
+
61
+ it('wraps try/catch without auto-return', () => {
62
+ expect(normalizeCode('try { return 1 } catch(e) { return 2 }')).toBe(
63
+ 'async () => { try { return 1 } catch(e) { return 2 } }'
64
+ )
65
+ })
66
+ })
67
+
68
+ describe('createSandbox', () => {
69
+ describe('basic execution', () => {
70
+ it('executes simple expressions', async () => {
71
+ const sandbox = createSandbox({})
72
+ const result = await sandbox.execute('async () => 1 + 2')
73
+ expect(result.error).toBeUndefined()
74
+ expect(result.result).toBe(3)
75
+ expect(result.durationMs).toBeGreaterThanOrEqual(0)
76
+ })
77
+
78
+ it('returns objects', async () => {
79
+ const sandbox = createSandbox({})
80
+ const result = await sandbox.execute('async () => ({ name: "test", count: 42 })')
81
+ expect(result.result).toEqual({ name: 'test', count: 42 })
82
+ })
83
+
84
+ it('handles arrays', async () => {
85
+ const sandbox = createSandbox({})
86
+ const result = await sandbox.execute('async () => [1, 2, 3].filter(x => x > 1)')
87
+ expect(result.result).toEqual([2, 3])
88
+ })
89
+
90
+ it('returns undefined for void functions', async () => {
91
+ const sandbox = createSandbox({})
92
+ const result = await sandbox.execute('async () => { const x = 1 }')
93
+ expect(result.error).toBeUndefined()
94
+ expect(result.result).toBeUndefined()
95
+ })
96
+ })
97
+
98
+ describe('injected globals', () => {
99
+ it('exposes custom globals', async () => {
100
+ const sandbox = createSandbox({ myValue: 42 })
101
+ const result = await sandbox.execute('async () => myValue')
102
+ expect(result.result).toBe(42)
103
+ })
104
+
105
+ it('exposes complex objects as globals', async () => {
106
+ const spec = {
107
+ paths: {
108
+ '/api/customers/companies': { get: { summary: 'List companies' } },
109
+ '/api/sales/orders': { get: { summary: 'List orders' } },
110
+ },
111
+ }
112
+ const sandbox = createSandbox({ spec })
113
+ const result = await sandbox.execute('async () => Object.keys(spec.paths)')
114
+ expect(result.result).toEqual(['/api/customers/companies', '/api/sales/orders'])
115
+ })
116
+
117
+ it('exposes helper functions as globals', async () => {
118
+ const spec = {
119
+ findEndpoints: (kw: string) =>
120
+ ['/api/customers/companies', '/api/sales/orders']
121
+ .filter((p) => p.includes(kw))
122
+ .map((p) => ({ path: p })),
123
+ }
124
+ const sandbox = createSandbox({ spec })
125
+ const result = await sandbox.execute(
126
+ 'async () => spec.findEndpoints("customer")'
127
+ )
128
+ expect(result.result).toEqual([{ path: '/api/customers/companies' }])
129
+ })
130
+
131
+ it('makes the context object available', async () => {
132
+ const context = { tenantId: 't1', organizationId: 'o1', userId: 'u1' }
133
+ const sandbox = createSandbox({ context })
134
+ const result = await sandbox.execute('async () => context.tenantId')
135
+ expect(result.result).toBe('t1')
136
+ })
137
+ })
138
+
139
+ describe('security restrictions', () => {
140
+ it('blocks require', async () => {
141
+ const sandbox = createSandbox({})
142
+ const result = await sandbox.execute("async () => require('fs')")
143
+ expect(result.error).toBeDefined()
144
+ })
145
+
146
+ it('blocks process', async () => {
147
+ const sandbox = createSandbox({})
148
+ const result = await sandbox.execute('async () => process.env')
149
+ expect(result.error).toBeDefined()
150
+ })
151
+
152
+ it('blocks fetch', async () => {
153
+ const sandbox = createSandbox({})
154
+ const result = await sandbox.execute("async () => fetch('http://evil.com')")
155
+ expect(result.error).toBeDefined()
156
+ })
157
+
158
+ it('blocks globalThis', async () => {
159
+ const sandbox = createSandbox({})
160
+ const result = await sandbox.execute('async () => globalThis')
161
+ expect(result.result).toBeUndefined()
162
+ })
163
+
164
+ it('blocks Buffer', async () => {
165
+ const sandbox = createSandbox({})
166
+ const result = await sandbox.execute("async () => Buffer.from('test')")
167
+ expect(result.error).toBeDefined()
168
+ })
169
+
170
+ it('blocks setTimeout', async () => {
171
+ const sandbox = createSandbox({})
172
+ const result = await sandbox.execute(
173
+ 'async () => setTimeout(() => {}, 100)'
174
+ )
175
+ expect(result.error).toBeDefined()
176
+ })
177
+
178
+ it('blocks setInterval', async () => {
179
+ const sandbox = createSandbox({})
180
+ const result = await sandbox.execute(
181
+ 'async () => setInterval(() => {}, 100)'
182
+ )
183
+ expect(result.error).toBeDefined()
184
+ })
185
+ })
186
+
187
+ describe('allowed built-ins', () => {
188
+ it('allows JSON', async () => {
189
+ const sandbox = createSandbox({})
190
+ const result = await sandbox.execute(
191
+ 'async () => JSON.parse(JSON.stringify({ a: 1 }))'
192
+ )
193
+ expect(result.result).toEqual({ a: 1 })
194
+ })
195
+
196
+ it('allows Object methods', async () => {
197
+ const sandbox = createSandbox({})
198
+ const result = await sandbox.execute(
199
+ 'async () => Object.keys({ a: 1, b: 2 })'
200
+ )
201
+ expect(result.result).toEqual(['a', 'b'])
202
+ })
203
+
204
+ it('allows Array methods', async () => {
205
+ const sandbox = createSandbox({})
206
+ const result = await sandbox.execute(
207
+ 'async () => [3, 1, 2].sort((a, b) => a - b)'
208
+ )
209
+ expect(result.result).toEqual([1, 2, 3])
210
+ })
211
+
212
+ it('allows Map and Set', async () => {
213
+ const sandbox = createSandbox({})
214
+ const result = await sandbox.execute(
215
+ 'async () => { const s = new Set([1,2,2,3]); return s.size }'
216
+ )
217
+ expect(result.result).toBe(3)
218
+ })
219
+
220
+ it('allows Math', async () => {
221
+ const sandbox = createSandbox({})
222
+ const result = await sandbox.execute('async () => Math.max(1, 5, 3)')
223
+ expect(result.result).toBe(5)
224
+ })
225
+
226
+ it('allows RegExp', async () => {
227
+ const sandbox = createSandbox({})
228
+ const result = await sandbox.execute(
229
+ "async () => /customer/.test('customers/companies')"
230
+ )
231
+ expect(result.result).toBe(true)
232
+ })
233
+
234
+ it('allows Promise', async () => {
235
+ const sandbox = createSandbox({})
236
+ const result = await sandbox.execute(
237
+ 'async () => Promise.resolve(42)'
238
+ )
239
+ expect(result.result).toBe(42)
240
+ })
241
+ })
242
+
243
+ describe('console capture', () => {
244
+ it('captures console.log output', async () => {
245
+ const sandbox = createSandbox({})
246
+ const result = await sandbox.execute(
247
+ 'async () => { console.log("hello"); return 42 }'
248
+ )
249
+ expect(result.result).toBe(42)
250
+ expect(result.logs).toContain('hello')
251
+ })
252
+
253
+ it('captures multiple console methods', async () => {
254
+ const sandbox = createSandbox({})
255
+ const result = await sandbox.execute(
256
+ 'async () => { console.info("a"); console.warn("b"); console.error("c"); return true }'
257
+ )
258
+ expect(result.logs).toEqual(['a', 'b', 'c'])
259
+ })
260
+
261
+ it('serializes objects in console output', async () => {
262
+ const sandbox = createSandbox({})
263
+ const result = await sandbox.execute(
264
+ 'async () => { console.log("data:", { x: 1 }); return true }'
265
+ )
266
+ expect(result.logs[0]).toContain('data:')
267
+ expect(result.logs[0]).toContain('"x":1')
268
+ })
269
+ })
270
+
271
+ describe('error handling', () => {
272
+ it('catches runtime errors', async () => {
273
+ const sandbox = createSandbox({})
274
+ const result = await sandbox.execute(
275
+ 'async () => { throw new Error("boom") }'
276
+ )
277
+ expect(result.error).toBe('boom')
278
+ expect(result.result).toBeNull()
279
+ })
280
+
281
+ it('catches type errors', async () => {
282
+ const sandbox = createSandbox({})
283
+ const result = await sandbox.execute(
284
+ 'async () => { const x = null; return x.foo }'
285
+ )
286
+ expect(result.error).toBeDefined()
287
+ })
288
+
289
+ it('catches syntax errors', async () => {
290
+ const sandbox = createSandbox({})
291
+ const result = await sandbox.execute('async () => { if ( }')
292
+ expect(result.error).toBeDefined()
293
+ })
294
+ })
295
+
296
+ describe('timeout', () => {
297
+ it('times out long-running code', async () => {
298
+ const sandbox = createSandbox({}, { timeout: 100 })
299
+ const result = await sandbox.execute(
300
+ 'async () => { while(true) {} }'
301
+ )
302
+ expect(result.error).toBeDefined()
303
+ expect(result.error).toMatch(/timed out/i)
304
+ }, 10_000)
305
+ })
306
+
307
+ describe('async api.request() integration', () => {
308
+ it('executes async api.request calls', async () => {
309
+ const api = {
310
+ request: jest.fn().mockResolvedValue({
311
+ success: true,
312
+ statusCode: 200,
313
+ data: { items: [{ id: '1', name: 'ACME' }] },
314
+ }),
315
+ }
316
+ const sandbox = createSandbox({ api })
317
+ const result = await sandbox.execute(
318
+ 'async () => api.request({ method: "GET", path: "/api/customers/companies" })'
319
+ )
320
+ expect(result.error).toBeUndefined()
321
+ expect(result.result).toEqual({
322
+ success: true,
323
+ statusCode: 200,
324
+ data: { items: [{ id: '1', name: 'ACME' }] },
325
+ })
326
+ expect(api.request).toHaveBeenCalledWith({
327
+ method: 'GET',
328
+ path: '/api/customers/companies',
329
+ })
330
+ })
331
+
332
+ it('executes multiple sequential api.request calls', async () => {
333
+ let callCount = 0
334
+ const api = {
335
+ request: jest.fn().mockImplementation(async (params: { path: string }) => {
336
+ callCount++
337
+ return {
338
+ success: true,
339
+ statusCode: 200,
340
+ data: { id: callCount, path: params.path },
341
+ }
342
+ }),
343
+ }
344
+ const sandbox = createSandbox({ api })
345
+ const result = await sandbox.execute(`
346
+ async () => {
347
+ const companies = await api.request({ method: "GET", path: "/api/customers/companies" })
348
+ const people = await api.request({ method: "GET", path: "/api/customers/people" })
349
+ return { companies: companies.data, people: people.data }
350
+ }
351
+ `)
352
+ expect(result.error).toBeUndefined()
353
+ expect(api.request).toHaveBeenCalledTimes(2)
354
+ expect(result.result).toEqual({
355
+ companies: { id: 1, path: '/api/customers/companies' },
356
+ people: { id: 2, path: '/api/customers/people' },
357
+ })
358
+ })
359
+
360
+ it('handles api.request errors gracefully', async () => {
361
+ const api = {
362
+ request: jest.fn().mockResolvedValue({
363
+ success: false,
364
+ statusCode: 404,
365
+ error: 'Not found',
366
+ }),
367
+ }
368
+ const sandbox = createSandbox({ api })
369
+ const result = await sandbox.execute(
370
+ 'async () => api.request({ method: "GET", path: "/api/nonexistent" })'
371
+ )
372
+ expect(result.error).toBeUndefined()
373
+ expect(result.result).toEqual({
374
+ success: false,
375
+ statusCode: 404,
376
+ error: 'Not found',
377
+ })
378
+ })
379
+
380
+ it('handles api.request rejections', async () => {
381
+ const api = {
382
+ request: jest.fn().mockRejectedValue(new Error('Network error')),
383
+ }
384
+ const sandbox = createSandbox({ api })
385
+ const result = await sandbox.execute(
386
+ 'async () => api.request({ method: "GET", path: "/api/fail" })'
387
+ )
388
+ expect(result.error).toBe('Network error')
389
+ })
390
+
391
+ it('supports write operations in sandbox', async () => {
392
+ const api = {
393
+ request: jest.fn().mockResolvedValue({
394
+ success: true,
395
+ statusCode: 201,
396
+ data: { id: 'new-id', name: 'New Company' },
397
+ }),
398
+ }
399
+ const sandbox = createSandbox({ api })
400
+ const result = await sandbox.execute(`
401
+ async () => api.request({
402
+ method: "POST",
403
+ path: "/api/customers/companies",
404
+ body: { name: "New Company", isActive: true }
405
+ })
406
+ `)
407
+ expect(result.error).toBeUndefined()
408
+ expect(api.request).toHaveBeenCalledWith({
409
+ method: 'POST',
410
+ path: '/api/customers/companies',
411
+ body: { name: 'New Company', isActive: true },
412
+ })
413
+ })
414
+
415
+ it('supports processing API results with JS logic', async () => {
416
+ const api = {
417
+ request: jest.fn().mockResolvedValue({
418
+ success: true,
419
+ statusCode: 200,
420
+ data: {
421
+ items: [
422
+ { id: '1', name: 'ACME', city: 'New York' },
423
+ { id: '2', name: 'Beta', city: 'London' },
424
+ { id: '3', name: 'Gamma', city: 'New York' },
425
+ ],
426
+ },
427
+ }),
428
+ }
429
+ const sandbox = createSandbox({ api })
430
+ const result = await sandbox.execute(`
431
+ async () => {
432
+ const res = await api.request({ method: "GET", path: "/api/customers/companies" })
433
+ return res.data.items
434
+ .filter(c => c.city === "New York")
435
+ .map(c => c.name)
436
+ }
437
+ `)
438
+ expect(result.result).toEqual(['ACME', 'Gamma'])
439
+ })
440
+ })
441
+
442
+ describe('spec search patterns', () => {
443
+ const spec = {
444
+ paths: {
445
+ '/api/customers/companies': {
446
+ get: { summary: 'List companies', parameters: [{ name: 'name', in: 'query' }] },
447
+ post: { summary: 'Create company', requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } } } } },
448
+ },
449
+ '/api/customers/people': {
450
+ get: { summary: 'List people' },
451
+ },
452
+ '/api/sales/orders': {
453
+ get: { summary: 'List orders' },
454
+ post: { summary: 'Create order' },
455
+ },
456
+ },
457
+ entitySchemas: [
458
+ { className: 'Company', tableName: 'customers_companies', module: 'customers', fields: [{ name: 'id', type: 'string' }, { name: 'name', type: 'string' }] },
459
+ { className: 'Person', tableName: 'customers_people', module: 'customers', fields: [{ name: 'id', type: 'string' }, { name: 'firstName', type: 'string' }] },
460
+ ],
461
+ findEndpoints: (kw: string) =>
462
+ Object.entries(spec.paths)
463
+ .filter(([p]) => p.toLowerCase().includes(kw.toLowerCase()))
464
+ .map(([path, methods]) => ({
465
+ path,
466
+ methods: Object.keys(methods).filter((m) => m !== 'parameters'),
467
+ })),
468
+ describeEntity: (kw: string) =>
469
+ spec.entitySchemas.find(
470
+ (e) =>
471
+ e.className.toLowerCase().includes(kw.toLowerCase()) ||
472
+ e.tableName.toLowerCase().includes(kw.toLowerCase())
473
+ ) || null,
474
+ }
475
+
476
+ it('finds endpoints by keyword', async () => {
477
+ const sandbox = createSandbox({ spec })
478
+ const result = await sandbox.execute(
479
+ 'async () => spec.findEndpoints("customer")'
480
+ )
481
+ expect(result.result).toEqual([
482
+ { path: '/api/customers/companies', methods: ['get', 'post'] },
483
+ { path: '/api/customers/people', methods: ['get'] },
484
+ ])
485
+ })
486
+
487
+ it('reads endpoint details directly from paths', async () => {
488
+ const sandbox = createSandbox({ spec })
489
+ const result = await sandbox.execute(
490
+ 'async () => spec.paths["/api/customers/companies"].get'
491
+ )
492
+ expect(result.result).toEqual({
493
+ summary: 'List companies',
494
+ parameters: [{ name: 'name', in: 'query' }],
495
+ })
496
+ })
497
+
498
+ it('extracts request body schema', async () => {
499
+ const sandbox = createSandbox({ spec })
500
+ const result = await sandbox.execute(
501
+ 'async () => spec.paths["/api/customers/companies"].post.requestBody.content["application/json"].schema'
502
+ )
503
+ expect(result.result).toEqual({
504
+ type: 'object',
505
+ properties: { name: { type: 'string' } },
506
+ required: ['name'],
507
+ })
508
+ })
509
+
510
+ it('discovers entity schemas by keyword', async () => {
511
+ const sandbox = createSandbox({ spec })
512
+ const result = await sandbox.execute(
513
+ 'async () => spec.describeEntity("company")'
514
+ )
515
+ expect(result.result).toEqual({
516
+ className: 'Company',
517
+ tableName: 'customers_companies',
518
+ module: 'customers',
519
+ fields: [
520
+ { name: 'id', type: 'string' },
521
+ { name: 'name', type: 'string' },
522
+ ],
523
+ })
524
+ })
525
+
526
+ it('lists all available paths', async () => {
527
+ const sandbox = createSandbox({ spec })
528
+ const result = await sandbox.execute(
529
+ 'async () => Object.keys(spec.paths)'
530
+ )
531
+ expect(result.result).toEqual([
532
+ '/api/customers/companies',
533
+ '/api/customers/people',
534
+ '/api/sales/orders',
535
+ ])
536
+ })
537
+
538
+ it('combines search + execute pattern', async () => {
539
+ const api = {
540
+ request: jest.fn().mockResolvedValue({
541
+ success: true,
542
+ statusCode: 200,
543
+ data: { items: [{ id: '1', name: 'ACME' }] },
544
+ }),
545
+ }
546
+
547
+ // Step 1: search for endpoints
548
+ const searchSandbox = createSandbox({ spec })
549
+ const searchResult = await searchSandbox.execute(
550
+ 'async () => spec.findEndpoints("companies")'
551
+ )
552
+ expect(searchResult.result).toEqual([
553
+ { path: '/api/customers/companies', methods: ['get', 'post'] },
554
+ ])
555
+
556
+ // Step 2: execute API call using discovered path
557
+ const execSandbox = createSandbox({ api })
558
+ const execResult = await execSandbox.execute(`
559
+ async () => api.request({
560
+ method: "GET",
561
+ path: "/api/customers/companies"
562
+ })
563
+ `)
564
+ expect(execResult.result).toEqual({
565
+ success: true,
566
+ statusCode: 200,
567
+ data: { items: [{ id: '1', name: 'ACME' }] },
568
+ })
569
+ })
570
+ })
571
+
572
+ describe('statement execution', () => {
573
+ it('executes code with const declarations', async () => {
574
+ const api = {
575
+ request: jest.fn().mockResolvedValue({
576
+ success: true,
577
+ statusCode: 200,
578
+ data: { items: [{ id: '1', name: 'ACME' }] },
579
+ }),
580
+ }
581
+ const sandbox = createSandbox({ api })
582
+ const result = await sandbox.execute(
583
+ 'const result = await api.request({ method: "GET", path: "/api/customers/companies" }); return result'
584
+ )
585
+ expect(result.error).toBeUndefined()
586
+ expect(result.result).toEqual({
587
+ success: true,
588
+ statusCode: 200,
589
+ data: { items: [{ id: '1', name: 'ACME' }] },
590
+ })
591
+ })
592
+
593
+ it('executes for loops', async () => {
594
+ const sandbox = createSandbox({})
595
+ const result = await sandbox.execute(
596
+ 'const items = [1, 2, 3]; let sum = 0; for (const item of items) { sum += item }; return sum'
597
+ )
598
+ expect(result.error).toBeUndefined()
599
+ expect(result.result).toBe(6)
600
+ })
601
+
602
+ it('executes try/catch blocks', async () => {
603
+ const sandbox = createSandbox({})
604
+ const result = await sandbox.execute(
605
+ 'try { throw new Error("test") } catch (e) { return e.message }'
606
+ )
607
+ expect(result.error).toBeUndefined()
608
+ expect(result.result).toBe('test')
609
+ })
610
+
611
+ it('executes if/else blocks', async () => {
612
+ const sandbox = createSandbox({ val: 42 })
613
+ const result = await sandbox.execute(
614
+ 'if (val > 10) { return "big" } else { return "small" }'
615
+ )
616
+ expect(result.error).toBeUndefined()
617
+ expect(result.result).toBe('big')
618
+ })
619
+
620
+ it('executes let declarations', async () => {
621
+ const sandbox = createSandbox({})
622
+ const result = await sandbox.execute('let x = 5; x = x * 2; return x')
623
+ expect(result.error).toBeUndefined()
624
+ expect(result.result).toBe(10)
625
+ })
626
+ })
627
+
628
+ describe('auto-wrapping bare expressions', () => {
629
+ it('handles bare property access', async () => {
630
+ const sandbox = createSandbox({ spec: { paths: { '/a': 1 } } })
631
+ const result = await sandbox.execute('Object.keys(spec.paths)')
632
+ expect(result.result).toEqual(['/a'])
633
+ })
634
+
635
+ it('handles bare function calls', async () => {
636
+ const fn = jest.fn().mockReturnValue('result')
637
+ const sandbox = createSandbox({ fn })
638
+ const result = await sandbox.execute('fn()')
639
+ expect(result.result).toBe('result')
640
+ })
641
+ })
642
+ })