@elevasis/core 0.34.2 → 0.35.0

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 (33) hide show
  1. package/dist/auth/index.d.ts +74 -2
  2. package/dist/auth/index.js +65 -30
  3. package/dist/index.d.ts +60 -2
  4. package/dist/index.js +52 -1
  5. package/dist/knowledge/index.d.ts +12 -0
  6. package/dist/organization-model/index.d.ts +60 -2
  7. package/dist/organization-model/index.js +52 -1
  8. package/dist/test-utils/index.d.ts +12 -0
  9. package/dist/test-utils/index.js +51 -0
  10. package/package.json +1 -1
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +69 -30
  12. package/src/auth/multi-tenancy/index.ts +29 -26
  13. package/src/auth/multi-tenancy/org-id.test.ts +139 -0
  14. package/src/auth/multi-tenancy/org-id.ts +112 -0
  15. package/src/business/acquisition/api-schemas.test.ts +456 -28
  16. package/src/business/acquisition/ontology-validation.ts +715 -23
  17. package/src/execution/engine/tools/platform/storage/__tests__/storage.test.ts +997 -998
  18. package/src/organization-model/__tests__/domains/systems.test.ts +61 -15
  19. package/src/organization-model/__tests__/domains/topology.test.ts +23 -0
  20. package/src/organization-model/__tests__/schema.test.ts +112 -0
  21. package/src/organization-model/domains/systems.ts +44 -0
  22. package/src/organization-model/domains/topology.ts +18 -1
  23. package/src/organization-model/published.ts +19 -1
  24. package/src/organization-model/schema-refinements.ts +23 -0
  25. package/src/organization-model/types.ts +17 -1
  26. package/src/platform/constants/versions.ts +1 -1
  27. package/src/platform/registry/__tests__/validation.test.ts +218 -4
  28. package/src/platform/registry/index.ts +28 -15
  29. package/src/platform/registry/validation.ts +172 -2
  30. package/src/reference/_generated/contracts.md +44 -0
  31. package/src/supabase/__tests__/helpers.test.ts +92 -51
  32. package/src/supabase/helpers.ts +40 -20
  33. package/src/supabase/index.ts +52 -52
@@ -1,998 +1,997 @@
1
- /* eslint-disable no-undef */
2
- import { describe, it, expect, vi, beforeEach } from 'vitest'
3
- import {
4
- createStorageUploadTool,
5
- createStorageSignedUrlTool,
6
- createStorageDownloadTool,
7
- createStorageDeleteTool,
8
- createStorageListTool,
9
- StorageUploadInputSchema,
10
- StorageSignedUrlInputSchema,
11
- StorageDownloadInputSchema,
12
- StorageDeleteInputSchema,
13
- StorageListInputSchema
14
- } from '../index'
15
- import { ToolingError } from '../../../types'
16
- import { getToolServices } from '../../../registry'
17
- import { createMockExecutionContext } from '../../../../test-utils/mocks'
18
-
19
- vi.mock('../../../registry', () => ({
20
- getToolServices: vi.fn()
21
- }))
22
-
23
- describe('Storage Platform Tools', () => {
24
- const mockConfig = {
25
- name: 'test_storage_tool',
26
- description: 'Test storage tool description'
27
- }
28
-
29
- const mockContext = createMockExecutionContext({
30
- organizationId: 'org-123',
31
- executionId: 'exec-456',
32
- resourceId: 'agent-789'
33
- })
34
-
35
- const mockStorageService = {
36
- upload: vi.fn(),
37
- download: vi.fn(),
38
- createSignedUrl: vi.fn(),
39
- delete: vi.fn(),
40
- list: vi.fn()
41
- }
42
-
43
- beforeEach(() => {
44
- vi.clearAllMocks()
45
- vi.mocked(getToolServices).mockReturnValue({
46
- commandQueueService: null,
47
- taskSchedulerService: null,
48
- notificationsService: null,
49
- emailService: null,
50
- credentialsService: null,
51
- integrationService: null,
52
- leadService: null,
53
- storageService: mockStorageService
54
- })
55
- })
56
-
57
- describe('createStorageUploadTool', () => {
58
- describe('tool creation', () => {
59
- it('creates tool with correct name and description', () => {
60
- const tool = createStorageUploadTool({ name: 'upload_file', description: 'Upload file' })
61
-
62
- expect(tool.name).toBe('upload_file')
63
- expect(tool.description).toBe('Upload file')
64
- })
65
-
66
- it('uses StorageUploadInputSchema as inputSchema', () => {
67
- const tool = createStorageUploadTool(mockConfig)
68
-
69
- expect(tool.inputSchema).toBe(StorageUploadInputSchema)
70
- })
71
- })
72
-
73
- describe('execute', () => {
74
- it('uploads file with organizationId from context', async () => {
75
- mockStorageService.upload.mockResolvedValue({
76
- path: 'files/test.pdf',
77
- fullPath: 'org-123/files/test.pdf',
78
- size: 1024
79
- })
80
-
81
- const tool = createStorageUploadTool(mockConfig)
82
- const result = await tool.execute({
83
- input: {
84
- bucket: 'org-files',
85
- path: 'files/test.pdf',
86
- content: Buffer.from('test content').toString('base64'),
87
- contentType: 'application/pdf'
88
- },
89
- executionContext: mockContext
90
- })
91
-
92
- expect(mockStorageService.upload).toHaveBeenCalledWith({
93
- organizationId: 'org-123',
94
- bucket: 'org-files',
95
- path: 'files/test.pdf',
96
- file: expect.any(Buffer),
97
- contentType: 'application/pdf',
98
- upsert: undefined
99
- })
100
- expect(result).toEqual({
101
- success: true,
102
- path: 'files/test.pdf',
103
- fullPath: 'org-123/files/test.pdf'
104
- })
105
- })
106
-
107
- it('passes upsert flag when provided', async () => {
108
- mockStorageService.upload.mockResolvedValue({
109
- path: 'files/test.pdf',
110
- fullPath: 'org-123/files/test.pdf',
111
- size: 1024
112
- })
113
-
114
- const tool = createStorageUploadTool(mockConfig)
115
- await tool.execute({
116
- input: {
117
- bucket: 'org-files',
118
- path: 'files/test.pdf',
119
- content: Buffer.from('test').toString('base64'),
120
- contentType: 'application/pdf',
121
- upsert: true
122
- },
123
- executionContext: mockContext
124
- })
125
-
126
- expect(mockStorageService.upload).toHaveBeenCalledWith(
127
- expect.objectContaining({
128
- upsert: true
129
- })
130
- )
131
- })
132
-
133
- it('decodes base64 content to Buffer', async () => {
134
- mockStorageService.upload.mockResolvedValue({
135
- path: 'files/test.pdf',
136
- fullPath: 'org-123/files/test.pdf',
137
- size: 12
138
- })
139
-
140
- const originalContent = 'test content'
141
- const base64Content = Buffer.from(originalContent).toString('base64')
142
-
143
- const tool = createStorageUploadTool(mockConfig)
144
- await tool.execute({
145
- input: {
146
- bucket: 'org-files',
147
- path: 'files/test.pdf',
148
- content: base64Content,
149
- contentType: 'application/pdf'
150
- },
151
- executionContext: mockContext
152
- })
153
-
154
- const uploadCall = mockStorageService.upload.mock.calls[0][0]
155
- expect(uploadCall.file).toBeInstanceOf(Buffer)
156
- expect(uploadCall.file.toString()).toBe(originalContent)
157
- })
158
- })
159
-
160
- describe('error handling', () => {
161
- it('throws Error when executionContext is missing', async () => {
162
- const tool = createStorageUploadTool(mockConfig)
163
-
164
- await expect(
165
- tool.execute({
166
- input: {
167
- bucket: 'test',
168
- path: 'test.pdf',
169
- content: 'dGVzdA==',
170
- contentType: 'application/pdf'
171
- }
172
- })
173
- ).rejects.toThrow('ExecutionContext is unavailable')
174
- })
175
-
176
- it('throws ToolingError when storageService is unavailable', async () => {
177
- vi.mocked(getToolServices).mockReturnValue({
178
- commandQueueService: null,
179
- taskSchedulerService: null,
180
- notificationsService: null,
181
- emailService: null,
182
- credentialsService: null,
183
- integrationService: null,
184
- leadService: null,
185
- storageService: null
186
- })
187
-
188
- const tool = createStorageUploadTool(mockConfig)
189
-
190
- try {
191
- await tool.execute({
192
- input: {
193
- bucket: 'test',
194
- path: 'test.pdf',
195
- content: 'dGVzdA==',
196
- contentType: 'application/pdf'
197
- },
198
- executionContext: mockContext
199
- })
200
- expect.fail('Should have thrown')
201
- } catch (error) {
202
- expect(error).toBeInstanceOf(ToolingError)
203
- const toolingError = error as ToolingError
204
- expect(toolingError.errorType).toBe('service_unavailable')
205
- expect(toolingError.message).toBe('StorageService is unavailable')
206
- }
207
- })
208
-
209
- it('propagates errors from storageService', async () => {
210
- mockStorageService.upload.mockRejectedValue(new Error('Upload failed'))
211
-
212
- const tool = createStorageUploadTool(mockConfig)
213
-
214
- await expect(
215
- tool.execute({
216
- input: {
217
- bucket: 'test',
218
- path: 'test.pdf',
219
- content: 'dGVzdA==',
220
- contentType: 'application/pdf'
221
- },
222
- executionContext: mockContext
223
- })
224
- ).rejects.toThrow('Upload failed')
225
- })
226
- })
227
- })
228
-
229
- describe('createStorageSignedUrlTool', () => {
230
- describe('tool creation', () => {
231
- it('creates tool with correct name and description', () => {
232
- const tool = createStorageSignedUrlTool({
233
- name: 'get_signed_url',
234
- description: 'Get signed URL'
235
- })
236
-
237
- expect(tool.name).toBe('get_signed_url')
238
- expect(tool.description).toBe('Get signed URL')
239
- })
240
-
241
- it('uses StorageSignedUrlInputSchema as inputSchema', () => {
242
- const tool = createStorageSignedUrlTool(mockConfig)
243
-
244
- expect(tool.inputSchema).toBe(StorageSignedUrlInputSchema)
245
- })
246
- })
247
-
248
- describe('execute', () => {
249
- it('creates signed URL with organizationId from context', async () => {
250
- const expiresAt = new Date('2025-01-24T12:00:00Z')
251
- mockStorageService.createSignedUrl.mockResolvedValue({
252
- signedUrl: 'https://storage.supabase.co/signed/test-url',
253
- path: 'files/test.pdf',
254
- expiresAt
255
- })
256
-
257
- const tool = createStorageSignedUrlTool(mockConfig)
258
- const result = await tool.execute({
259
- input: {
260
- bucket: 'org-files',
261
- path: 'files/test.pdf',
262
- expiresIn: 3600
263
- },
264
- executionContext: mockContext
265
- })
266
-
267
- expect(mockStorageService.createSignedUrl).toHaveBeenCalledWith({
268
- organizationId: 'org-123',
269
- bucket: 'org-files',
270
- path: 'files/test.pdf',
271
- expiresIn: 3600
272
- })
273
- expect(result).toEqual({
274
- success: true,
275
- signedUrl: 'https://storage.supabase.co/signed/test-url',
276
- expiresAt: '2025-01-24T12:00:00.000Z'
277
- })
278
- })
279
-
280
- it('uses default expiresIn when not provided (via schema)', async () => {
281
- const expiresAt = new Date('2025-01-25T12:00:00Z')
282
- mockStorageService.createSignedUrl.mockResolvedValue({
283
- signedUrl: 'https://storage.supabase.co/signed/test-url',
284
- path: 'files/test.pdf',
285
- expiresAt
286
- })
287
-
288
- const tool = createStorageSignedUrlTool(mockConfig)
289
- // Parse input through schema to get default
290
- const parsedInput = StorageSignedUrlInputSchema.parse({
291
- bucket: 'org-files',
292
- path: 'files/test.pdf'
293
- })
294
-
295
- await tool.execute({
296
- input: parsedInput,
297
- executionContext: mockContext
298
- })
299
-
300
- expect(mockStorageService.createSignedUrl).toHaveBeenCalledWith(
301
- expect.objectContaining({
302
- expiresIn: 86400 // Default 24 hours
303
- })
304
- )
305
- })
306
- })
307
-
308
- describe('error handling', () => {
309
- it('throws Error when executionContext is missing', async () => {
310
- const tool = createStorageSignedUrlTool(mockConfig)
311
-
312
- await expect(
313
- tool.execute({
314
- input: {
315
- bucket: 'org-files',
316
- path: 'files/test.pdf',
317
- expiresIn: 3600
318
- }
319
- })
320
- ).rejects.toThrow('ExecutionContext is unavailable')
321
- })
322
-
323
- it('throws ToolingError when storageService is unavailable', async () => {
324
- vi.mocked(getToolServices).mockReturnValue({
325
- commandQueueService: null,
326
- taskSchedulerService: null,
327
- notificationsService: null,
328
- emailService: null,
329
- credentialsService: null,
330
- integrationService: null,
331
- leadService: null,
332
- storageService: null
333
- })
334
-
335
- const tool = createStorageSignedUrlTool(mockConfig)
336
-
337
- try {
338
- await tool.execute({
339
- input: {
340
- bucket: 'org-files',
341
- path: 'files/test.pdf',
342
- expiresIn: 3600
343
- },
344
- executionContext: mockContext
345
- })
346
- expect.fail('Should have thrown')
347
- } catch (error) {
348
- expect(error).toBeInstanceOf(ToolingError)
349
- expect((error as ToolingError).errorType).toBe('service_unavailable')
350
- }
351
- })
352
- })
353
- })
354
-
355
- describe('createStorageDownloadTool', () => {
356
- describe('tool creation', () => {
357
- it('creates tool with correct name and description', () => {
358
- const tool = createStorageDownloadTool({
359
- name: 'download_file',
360
- description: 'Download file'
361
- })
362
-
363
- expect(tool.name).toBe('download_file')
364
- expect(tool.description).toBe('Download file')
365
- })
366
-
367
- it('uses StorageDownloadInputSchema as inputSchema', () => {
368
- const tool = createStorageDownloadTool(mockConfig)
369
-
370
- expect(tool.inputSchema).toBe(StorageDownloadInputSchema)
371
- })
372
- })
373
-
374
- describe('execute', () => {
375
- it('downloads file and returns base64 content', async () => {
376
- const fileContent = 'test file content'
377
- const mockBlob = new Blob([fileContent], { type: 'application/pdf' })
378
- mockStorageService.download.mockResolvedValue(mockBlob)
379
-
380
- const tool = createStorageDownloadTool(mockConfig)
381
- const result = await tool.execute({
382
- input: {
383
- bucket: 'org-files',
384
- path: 'files/test.pdf'
385
- },
386
- executionContext: mockContext
387
- })
388
-
389
- expect(mockStorageService.download).toHaveBeenCalledWith({
390
- organizationId: 'org-123',
391
- bucket: 'org-files',
392
- path: 'files/test.pdf'
393
- })
394
- expect(result.success).toBe(true)
395
- expect(result.content).toBeTruthy()
396
- // Verify base64 decodes back to original content
397
- expect(Buffer.from(result.content, 'base64').toString()).toBe(fileContent)
398
- expect(result.contentType).toBe('application/pdf')
399
- })
400
-
401
- it('handles blob without type', async () => {
402
- const mockBlob = new Blob(['content'])
403
- Object.defineProperty(mockBlob, 'type', { value: '' })
404
- mockStorageService.download.mockResolvedValue(mockBlob)
405
-
406
- const tool = createStorageDownloadTool(mockConfig)
407
- const result = await tool.execute({
408
- input: {
409
- bucket: 'org-files',
410
- path: 'files/test.bin'
411
- },
412
- executionContext: mockContext
413
- })
414
-
415
- expect(result.success).toBe(true)
416
- expect(result.contentType).toBeUndefined()
417
- })
418
- })
419
-
420
- describe('error handling', () => {
421
- it('throws Error when executionContext is missing', async () => {
422
- const tool = createStorageDownloadTool(mockConfig)
423
-
424
- await expect(
425
- tool.execute({
426
- input: {
427
- bucket: 'org-files',
428
- path: 'files/test.pdf'
429
- }
430
- })
431
- ).rejects.toThrow('ExecutionContext is unavailable')
432
- })
433
-
434
- it('throws ToolingError when storageService is unavailable', async () => {
435
- vi.mocked(getToolServices).mockReturnValue({
436
- commandQueueService: null,
437
- taskSchedulerService: null,
438
- notificationsService: null,
439
- emailService: null,
440
- credentialsService: null,
441
- integrationService: null,
442
- leadService: null,
443
- storageService: null
444
- })
445
-
446
- const tool = createStorageDownloadTool(mockConfig)
447
-
448
- try {
449
- await tool.execute({
450
- input: {
451
- bucket: 'org-files',
452
- path: 'files/test.pdf'
453
- },
454
- executionContext: mockContext
455
- })
456
- expect.fail('Should have thrown')
457
- } catch (error) {
458
- expect(error).toBeInstanceOf(ToolingError)
459
- expect((error as ToolingError).errorType).toBe('service_unavailable')
460
- }
461
- })
462
-
463
- it('propagates errors from storageService', async () => {
464
- mockStorageService.download.mockRejectedValue(new Error('File not found'))
465
-
466
- const tool = createStorageDownloadTool(mockConfig)
467
-
468
- await expect(
469
- tool.execute({
470
- input: {
471
- bucket: 'org-files',
472
- path: 'files/nonexistent.pdf'
473
- },
474
- executionContext: mockContext
475
- })
476
- ).rejects.toThrow('File not found')
477
- })
478
- })
479
- })
480
-
481
- describe('createStorageDeleteTool', () => {
482
- describe('tool creation', () => {
483
- it('creates tool with correct name and description', () => {
484
- const tool = createStorageDeleteTool({
485
- name: 'delete_file',
486
- description: 'Delete file'
487
- })
488
-
489
- expect(tool.name).toBe('delete_file')
490
- expect(tool.description).toBe('Delete file')
491
- })
492
-
493
- it('uses StorageDeleteInputSchema as inputSchema', () => {
494
- const tool = createStorageDeleteTool(mockConfig)
495
-
496
- expect(tool.inputSchema).toBe(StorageDeleteInputSchema)
497
- })
498
- })
499
-
500
- describe('execute', () => {
501
- it('deletes file with organizationId from context', async () => {
502
- mockStorageService.delete.mockResolvedValue(undefined)
503
-
504
- const tool = createStorageDeleteTool(mockConfig)
505
- const result = await tool.execute({
506
- input: {
507
- bucket: 'org-files',
508
- path: 'files/test.pdf'
509
- },
510
- executionContext: mockContext
511
- })
512
-
513
- expect(mockStorageService.delete).toHaveBeenCalledWith({
514
- organizationId: 'org-123',
515
- bucket: 'org-files',
516
- path: 'files/test.pdf'
517
- })
518
- expect(result).toEqual({ success: true })
519
- })
520
- })
521
-
522
- describe('error handling', () => {
523
- it('throws Error when executionContext is missing', async () => {
524
- const tool = createStorageDeleteTool(mockConfig)
525
-
526
- await expect(
527
- tool.execute({
528
- input: {
529
- bucket: 'org-files',
530
- path: 'files/test.pdf'
531
- }
532
- })
533
- ).rejects.toThrow('ExecutionContext is unavailable')
534
- })
535
-
536
- it('throws ToolingError when storageService is unavailable', async () => {
537
- vi.mocked(getToolServices).mockReturnValue({
538
- commandQueueService: null,
539
- taskSchedulerService: null,
540
- notificationsService: null,
541
- emailService: null,
542
- credentialsService: null,
543
- integrationService: null,
544
- leadService: null,
545
- storageService: null
546
- })
547
-
548
- const tool = createStorageDeleteTool(mockConfig)
549
-
550
- try {
551
- await tool.execute({
552
- input: {
553
- bucket: 'org-files',
554
- path: 'files/test.pdf'
555
- },
556
- executionContext: mockContext
557
- })
558
- expect.fail('Should have thrown')
559
- } catch (error) {
560
- expect(error).toBeInstanceOf(ToolingError)
561
- expect((error as ToolingError).errorType).toBe('service_unavailable')
562
- }
563
- })
564
-
565
- it('propagates errors from storageService', async () => {
566
- mockStorageService.delete.mockRejectedValue(new Error('Permission denied'))
567
-
568
- const tool = createStorageDeleteTool(mockConfig)
569
-
570
- await expect(
571
- tool.execute({
572
- input: {
573
- bucket: 'org-files',
574
- path: 'files/protected.pdf'
575
- },
576
- executionContext: mockContext
577
- })
578
- ).rejects.toThrow('Permission denied')
579
- })
580
- })
581
- })
582
-
583
- describe('createStorageListTool', () => {
584
- describe('tool creation', () => {
585
- it('creates tool with correct name and description', () => {
586
- const tool = createStorageListTool({
587
- name: 'list_files',
588
- description: 'List files'
589
- })
590
-
591
- expect(tool.name).toBe('list_files')
592
- expect(tool.description).toBe('List files')
593
- })
594
-
595
- it('uses StorageListInputSchema as inputSchema', () => {
596
- const tool = createStorageListTool(mockConfig)
597
-
598
- expect(tool.inputSchema).toBe(StorageListInputSchema)
599
- })
600
- })
601
-
602
- describe('execute', () => {
603
- it('lists files with organizationId from context', async () => {
604
- mockStorageService.list.mockResolvedValue(['file1.pdf', 'file2.png', 'file3.doc'])
605
-
606
- const tool = createStorageListTool(mockConfig)
607
- const result = await tool.execute({
608
- input: {
609
- bucket: 'org-files',
610
- prefix: 'proposals/'
611
- },
612
- executionContext: mockContext
613
- })
614
-
615
- expect(mockStorageService.list).toHaveBeenCalledWith({
616
- organizationId: 'org-123',
617
- bucket: 'org-files',
618
- prefix: 'proposals/'
619
- })
620
- expect(result).toEqual({
621
- success: true,
622
- files: ['file1.pdf', 'file2.png', 'file3.doc']
623
- })
624
- })
625
-
626
- it('lists files without prefix', async () => {
627
- mockStorageService.list.mockResolvedValue(['file1.pdf'])
628
-
629
- const tool = createStorageListTool(mockConfig)
630
- await tool.execute({
631
- input: {
632
- bucket: 'org-files'
633
- },
634
- executionContext: mockContext
635
- })
636
-
637
- expect(mockStorageService.list).toHaveBeenCalledWith({
638
- organizationId: 'org-123',
639
- bucket: 'org-files',
640
- prefix: undefined
641
- })
642
- })
643
-
644
- it('returns empty array when no files found', async () => {
645
- mockStorageService.list.mockResolvedValue([])
646
-
647
- const tool = createStorageListTool(mockConfig)
648
- const result = await tool.execute({
649
- input: {
650
- bucket: 'org-files',
651
- prefix: 'empty-folder/'
652
- },
653
- executionContext: mockContext
654
- })
655
-
656
- expect(result).toEqual({
657
- success: true,
658
- files: []
659
- })
660
- })
661
- })
662
-
663
- describe('error handling', () => {
664
- it('throws Error when executionContext is missing', async () => {
665
- const tool = createStorageListTool(mockConfig)
666
-
667
- await expect(
668
- tool.execute({
669
- input: {
670
- bucket: 'org-files'
671
- }
672
- })
673
- ).rejects.toThrow('ExecutionContext is unavailable')
674
- })
675
-
676
- it('throws ToolingError when storageService is unavailable', async () => {
677
- vi.mocked(getToolServices).mockReturnValue({
678
- commandQueueService: null,
679
- taskSchedulerService: null,
680
- notificationsService: null,
681
- emailService: null,
682
- credentialsService: null,
683
- integrationService: null,
684
- leadService: null,
685
- storageService: null
686
- })
687
-
688
- const tool = createStorageListTool(mockConfig)
689
-
690
- try {
691
- await tool.execute({
692
- input: {
693
- bucket: 'org-files'
694
- },
695
- executionContext: mockContext
696
- })
697
- expect.fail('Should have thrown')
698
- } catch (error) {
699
- expect(error).toBeInstanceOf(ToolingError)
700
- expect((error as ToolingError).errorType).toBe('service_unavailable')
701
- }
702
- })
703
-
704
- it('propagates errors from storageService', async () => {
705
- mockStorageService.list.mockRejectedValue(new Error('Bucket not found'))
706
-
707
- const tool = createStorageListTool(mockConfig)
708
-
709
- await expect(
710
- tool.execute({
711
- input: {
712
- bucket: 'nonexistent-bucket'
713
- },
714
- executionContext: mockContext
715
- })
716
- ).rejects.toThrow('Bucket not found')
717
- })
718
- })
719
- })
720
-
721
- describe('concurrent execution', () => {
722
- it('handles multiple concurrent operations', async () => {
723
- mockStorageService.upload.mockResolvedValue({
724
- path: 'file1.pdf',
725
- fullPath: 'org-123/file1.pdf',
726
- size: 100
727
- })
728
- mockStorageService.list.mockResolvedValue(['file1.pdf', 'file2.pdf'])
729
- mockStorageService.createSignedUrl.mockResolvedValue({
730
- signedUrl: 'https://signed-url',
731
- path: 'file1.pdf',
732
- expiresAt: new Date()
733
- })
734
-
735
- const uploadTool = createStorageUploadTool({
736
- name: 'upload',
737
- description: 'Upload'
738
- })
739
- const listTool = createStorageListTool({
740
- name: 'list',
741
- description: 'List'
742
- })
743
- const signedUrlTool = createStorageSignedUrlTool({
744
- name: 'signed_url',
745
- description: 'Signed URL'
746
- })
747
-
748
- const results = await Promise.all([
749
- uploadTool.execute({
750
- input: {
751
- bucket: 'files',
752
- path: 'file1.pdf',
753
- content: 'dGVzdA==',
754
- contentType: 'application/pdf'
755
- },
756
- executionContext: mockContext
757
- }),
758
- listTool.execute({
759
- input: { bucket: 'files' },
760
- executionContext: mockContext
761
- }),
762
- signedUrlTool.execute({
763
- input: {
764
- bucket: 'files',
765
- path: 'file1.pdf',
766
- expiresIn: 3600
767
- },
768
- executionContext: mockContext
769
- })
770
- ])
771
-
772
- expect(results).toHaveLength(3)
773
- expect(results[0]).toEqual({
774
- success: true,
775
- path: 'file1.pdf',
776
- fullPath: 'org-123/file1.pdf'
777
- })
778
- expect(results[1]).toEqual({
779
- success: true,
780
- files: ['file1.pdf', 'file2.pdf']
781
- })
782
- expect(results[2].success).toBe(true)
783
- expect(results[2].signedUrl).toBe('https://signed-url')
784
- })
785
- })
786
- })
787
-
788
- describe('Storage Input Schemas', () => {
789
- describe('StorageUploadInputSchema', () => {
790
- it('accepts valid upload input with all required fields', () => {
791
- const result = StorageUploadInputSchema.safeParse({
792
- bucket: 'org-files',
793
- path: 'files/test.pdf',
794
- content: 'dGVzdA==',
795
- contentType: 'application/pdf'
796
- })
797
-
798
- expect(result.success).toBe(true)
799
- })
800
-
801
- it('accepts optional upsert flag', () => {
802
- const result = StorageUploadInputSchema.safeParse({
803
- bucket: 'org-files',
804
- path: 'files/test.pdf',
805
- content: 'dGVzdA==',
806
- contentType: 'application/pdf',
807
- upsert: true
808
- })
809
-
810
- expect(result.success).toBe(true)
811
- if (result.success) {
812
- expect(result.data.upsert).toBe(true)
813
- }
814
- })
815
-
816
- it('rejects missing bucket', () => {
817
- const result = StorageUploadInputSchema.safeParse({
818
- path: 'files/test.pdf',
819
- content: 'dGVzdA==',
820
- contentType: 'application/pdf'
821
- })
822
-
823
- expect(result.success).toBe(false)
824
- })
825
-
826
- it('rejects missing path', () => {
827
- const result = StorageUploadInputSchema.safeParse({
828
- bucket: 'org-files',
829
- content: 'dGVzdA==',
830
- contentType: 'application/pdf'
831
- })
832
-
833
- expect(result.success).toBe(false)
834
- })
835
-
836
- it('rejects missing content', () => {
837
- const result = StorageUploadInputSchema.safeParse({
838
- bucket: 'org-files',
839
- path: 'files/test.pdf',
840
- contentType: 'application/pdf'
841
- })
842
-
843
- expect(result.success).toBe(false)
844
- })
845
-
846
- it('rejects missing contentType', () => {
847
- const result = StorageUploadInputSchema.safeParse({
848
- bucket: 'org-files',
849
- path: 'files/test.pdf',
850
- content: 'dGVzdA=='
851
- })
852
-
853
- expect(result.success).toBe(false)
854
- })
855
- })
856
-
857
- describe('StorageSignedUrlInputSchema', () => {
858
- it('accepts valid signed URL input', () => {
859
- const result = StorageSignedUrlInputSchema.safeParse({
860
- bucket: 'org-files',
861
- path: 'files/test.pdf',
862
- expiresIn: 3600
863
- })
864
-
865
- expect(result.success).toBe(true)
866
- })
867
-
868
- it('uses default expiresIn when not provided', () => {
869
- const result = StorageSignedUrlInputSchema.safeParse({
870
- bucket: 'org-files',
871
- path: 'files/test.pdf'
872
- })
873
-
874
- expect(result.success).toBe(true)
875
- if (result.success) {
876
- expect(result.data.expiresIn).toBe(86400) // Default 24 hours
877
- }
878
- })
879
-
880
- it('accepts custom expiresIn value', () => {
881
- const result = StorageSignedUrlInputSchema.safeParse({
882
- bucket: 'org-files',
883
- path: 'files/test.pdf',
884
- expiresIn: 604800 // 7 days
885
- })
886
-
887
- expect(result.success).toBe(true)
888
- if (result.success) {
889
- expect(result.data.expiresIn).toBe(604800)
890
- }
891
- })
892
-
893
- it('rejects missing bucket', () => {
894
- const result = StorageSignedUrlInputSchema.safeParse({
895
- path: 'files/test.pdf',
896
- expiresIn: 3600
897
- })
898
-
899
- expect(result.success).toBe(false)
900
- })
901
-
902
- it('rejects missing path', () => {
903
- const result = StorageSignedUrlInputSchema.safeParse({
904
- bucket: 'org-files',
905
- expiresIn: 3600
906
- })
907
-
908
- expect(result.success).toBe(false)
909
- })
910
- })
911
-
912
- describe('StorageDownloadInputSchema', () => {
913
- it('accepts valid download input', () => {
914
- const result = StorageDownloadInputSchema.safeParse({
915
- bucket: 'org-files',
916
- path: 'files/test.pdf'
917
- })
918
-
919
- expect(result.success).toBe(true)
920
- })
921
-
922
- it('rejects missing bucket', () => {
923
- const result = StorageDownloadInputSchema.safeParse({
924
- path: 'files/test.pdf'
925
- })
926
-
927
- expect(result.success).toBe(false)
928
- })
929
-
930
- it('rejects missing path', () => {
931
- const result = StorageDownloadInputSchema.safeParse({
932
- bucket: 'org-files'
933
- })
934
-
935
- expect(result.success).toBe(false)
936
- })
937
- })
938
-
939
- describe('StorageDeleteInputSchema', () => {
940
- it('accepts valid delete input', () => {
941
- const result = StorageDeleteInputSchema.safeParse({
942
- bucket: 'org-files',
943
- path: 'files/test.pdf'
944
- })
945
-
946
- expect(result.success).toBe(true)
947
- })
948
-
949
- it('rejects missing bucket', () => {
950
- const result = StorageDeleteInputSchema.safeParse({
951
- path: 'files/test.pdf'
952
- })
953
-
954
- expect(result.success).toBe(false)
955
- })
956
-
957
- it('rejects missing path', () => {
958
- const result = StorageDeleteInputSchema.safeParse({
959
- bucket: 'org-files'
960
- })
961
-
962
- expect(result.success).toBe(false)
963
- })
964
- })
965
-
966
- describe('StorageListInputSchema', () => {
967
- it('accepts valid list input with prefix', () => {
968
- const result = StorageListInputSchema.safeParse({
969
- bucket: 'org-files',
970
- prefix: 'proposals/'
971
- })
972
-
973
- expect(result.success).toBe(true)
974
- if (result.success) {
975
- expect(result.data.prefix).toBe('proposals/')
976
- }
977
- })
978
-
979
- it('accepts list input without prefix', () => {
980
- const result = StorageListInputSchema.safeParse({
981
- bucket: 'org-files'
982
- })
983
-
984
- expect(result.success).toBe(true)
985
- if (result.success) {
986
- expect(result.data.prefix).toBeUndefined()
987
- }
988
- })
989
-
990
- it('rejects missing bucket', () => {
991
- const result = StorageListInputSchema.safeParse({
992
- prefix: 'proposals/'
993
- })
994
-
995
- expect(result.success).toBe(false)
996
- })
997
- })
998
- })
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import {
3
+ createStorageUploadTool,
4
+ createStorageSignedUrlTool,
5
+ createStorageDownloadTool,
6
+ createStorageDeleteTool,
7
+ createStorageListTool,
8
+ StorageUploadInputSchema,
9
+ StorageSignedUrlInputSchema,
10
+ StorageDownloadInputSchema,
11
+ StorageDeleteInputSchema,
12
+ StorageListInputSchema
13
+ } from '../index'
14
+ import { ToolingError } from '../../../types'
15
+ import { getToolServices } from '../../../registry'
16
+ import { createMockExecutionContext } from '../../../../test-utils/mocks'
17
+
18
+ vi.mock('../../../registry', () => ({
19
+ getToolServices: vi.fn()
20
+ }))
21
+
22
+ describe('Storage Platform Tools', () => {
23
+ const mockConfig = {
24
+ name: 'test_storage_tool',
25
+ description: 'Test storage tool description'
26
+ }
27
+
28
+ const mockContext = createMockExecutionContext({
29
+ organizationId: 'org-123',
30
+ executionId: 'exec-456',
31
+ resourceId: 'agent-789'
32
+ })
33
+
34
+ const mockStorageService = {
35
+ upload: vi.fn(),
36
+ download: vi.fn(),
37
+ createSignedUrl: vi.fn(),
38
+ delete: vi.fn(),
39
+ list: vi.fn()
40
+ }
41
+
42
+ beforeEach(() => {
43
+ vi.clearAllMocks()
44
+ vi.mocked(getToolServices).mockReturnValue({
45
+ commandQueueService: null,
46
+ taskSchedulerService: null,
47
+ notificationsService: null,
48
+ emailService: null,
49
+ credentialsService: null,
50
+ integrationService: null,
51
+ leadService: null,
52
+ storageService: mockStorageService
53
+ })
54
+ })
55
+
56
+ describe('createStorageUploadTool', () => {
57
+ describe('tool creation', () => {
58
+ it('creates tool with correct name and description', () => {
59
+ const tool = createStorageUploadTool({ name: 'upload_file', description: 'Upload file' })
60
+
61
+ expect(tool.name).toBe('upload_file')
62
+ expect(tool.description).toBe('Upload file')
63
+ })
64
+
65
+ it('uses StorageUploadInputSchema as inputSchema', () => {
66
+ const tool = createStorageUploadTool(mockConfig)
67
+
68
+ expect(tool.inputSchema).toBe(StorageUploadInputSchema)
69
+ })
70
+ })
71
+
72
+ describe('execute', () => {
73
+ it('uploads file with organizationId from context', async () => {
74
+ mockStorageService.upload.mockResolvedValue({
75
+ path: 'files/test.pdf',
76
+ fullPath: 'org-123/files/test.pdf',
77
+ size: 1024
78
+ })
79
+
80
+ const tool = createStorageUploadTool(mockConfig)
81
+ const result = await tool.execute({
82
+ input: {
83
+ bucket: 'org-files',
84
+ path: 'files/test.pdf',
85
+ content: Buffer.from('test content').toString('base64'),
86
+ contentType: 'application/pdf'
87
+ },
88
+ executionContext: mockContext
89
+ })
90
+
91
+ expect(mockStorageService.upload).toHaveBeenCalledWith({
92
+ organizationId: 'org-123',
93
+ bucket: 'org-files',
94
+ path: 'files/test.pdf',
95
+ file: expect.any(Buffer),
96
+ contentType: 'application/pdf',
97
+ upsert: undefined
98
+ })
99
+ expect(result).toEqual({
100
+ success: true,
101
+ path: 'files/test.pdf',
102
+ fullPath: 'org-123/files/test.pdf'
103
+ })
104
+ })
105
+
106
+ it('passes upsert flag when provided', async () => {
107
+ mockStorageService.upload.mockResolvedValue({
108
+ path: 'files/test.pdf',
109
+ fullPath: 'org-123/files/test.pdf',
110
+ size: 1024
111
+ })
112
+
113
+ const tool = createStorageUploadTool(mockConfig)
114
+ await tool.execute({
115
+ input: {
116
+ bucket: 'org-files',
117
+ path: 'files/test.pdf',
118
+ content: Buffer.from('test').toString('base64'),
119
+ contentType: 'application/pdf',
120
+ upsert: true
121
+ },
122
+ executionContext: mockContext
123
+ })
124
+
125
+ expect(mockStorageService.upload).toHaveBeenCalledWith(
126
+ expect.objectContaining({
127
+ upsert: true
128
+ })
129
+ )
130
+ })
131
+
132
+ it('decodes base64 content to Buffer', async () => {
133
+ mockStorageService.upload.mockResolvedValue({
134
+ path: 'files/test.pdf',
135
+ fullPath: 'org-123/files/test.pdf',
136
+ size: 12
137
+ })
138
+
139
+ const originalContent = 'test content'
140
+ const base64Content = Buffer.from(originalContent).toString('base64')
141
+
142
+ const tool = createStorageUploadTool(mockConfig)
143
+ await tool.execute({
144
+ input: {
145
+ bucket: 'org-files',
146
+ path: 'files/test.pdf',
147
+ content: base64Content,
148
+ contentType: 'application/pdf'
149
+ },
150
+ executionContext: mockContext
151
+ })
152
+
153
+ const uploadCall = mockStorageService.upload.mock.calls[0][0]
154
+ expect(uploadCall.file).toBeInstanceOf(Buffer)
155
+ expect(uploadCall.file.toString()).toBe(originalContent)
156
+ })
157
+ })
158
+
159
+ describe('error handling', () => {
160
+ it('throws Error when executionContext is missing', async () => {
161
+ const tool = createStorageUploadTool(mockConfig)
162
+
163
+ await expect(
164
+ tool.execute({
165
+ input: {
166
+ bucket: 'test',
167
+ path: 'test.pdf',
168
+ content: 'dGVzdA==',
169
+ contentType: 'application/pdf'
170
+ }
171
+ })
172
+ ).rejects.toThrow('ExecutionContext is unavailable')
173
+ })
174
+
175
+ it('throws ToolingError when storageService is unavailable', async () => {
176
+ vi.mocked(getToolServices).mockReturnValue({
177
+ commandQueueService: null,
178
+ taskSchedulerService: null,
179
+ notificationsService: null,
180
+ emailService: null,
181
+ credentialsService: null,
182
+ integrationService: null,
183
+ leadService: null,
184
+ storageService: null
185
+ })
186
+
187
+ const tool = createStorageUploadTool(mockConfig)
188
+
189
+ try {
190
+ await tool.execute({
191
+ input: {
192
+ bucket: 'test',
193
+ path: 'test.pdf',
194
+ content: 'dGVzdA==',
195
+ contentType: 'application/pdf'
196
+ },
197
+ executionContext: mockContext
198
+ })
199
+ expect.fail('Should have thrown')
200
+ } catch (error) {
201
+ expect(error).toBeInstanceOf(ToolingError)
202
+ const toolingError = error as ToolingError
203
+ expect(toolingError.errorType).toBe('service_unavailable')
204
+ expect(toolingError.message).toBe('StorageService is unavailable')
205
+ }
206
+ })
207
+
208
+ it('propagates errors from storageService', async () => {
209
+ mockStorageService.upload.mockRejectedValue(new Error('Upload failed'))
210
+
211
+ const tool = createStorageUploadTool(mockConfig)
212
+
213
+ await expect(
214
+ tool.execute({
215
+ input: {
216
+ bucket: 'test',
217
+ path: 'test.pdf',
218
+ content: 'dGVzdA==',
219
+ contentType: 'application/pdf'
220
+ },
221
+ executionContext: mockContext
222
+ })
223
+ ).rejects.toThrow('Upload failed')
224
+ })
225
+ })
226
+ })
227
+
228
+ describe('createStorageSignedUrlTool', () => {
229
+ describe('tool creation', () => {
230
+ it('creates tool with correct name and description', () => {
231
+ const tool = createStorageSignedUrlTool({
232
+ name: 'get_signed_url',
233
+ description: 'Get signed URL'
234
+ })
235
+
236
+ expect(tool.name).toBe('get_signed_url')
237
+ expect(tool.description).toBe('Get signed URL')
238
+ })
239
+
240
+ it('uses StorageSignedUrlInputSchema as inputSchema', () => {
241
+ const tool = createStorageSignedUrlTool(mockConfig)
242
+
243
+ expect(tool.inputSchema).toBe(StorageSignedUrlInputSchema)
244
+ })
245
+ })
246
+
247
+ describe('execute', () => {
248
+ it('creates signed URL with organizationId from context', async () => {
249
+ const expiresAt = new Date('2025-01-24T12:00:00Z')
250
+ mockStorageService.createSignedUrl.mockResolvedValue({
251
+ signedUrl: 'https://storage.supabase.co/signed/test-url',
252
+ path: 'files/test.pdf',
253
+ expiresAt
254
+ })
255
+
256
+ const tool = createStorageSignedUrlTool(mockConfig)
257
+ const result = await tool.execute({
258
+ input: {
259
+ bucket: 'org-files',
260
+ path: 'files/test.pdf',
261
+ expiresIn: 3600
262
+ },
263
+ executionContext: mockContext
264
+ })
265
+
266
+ expect(mockStorageService.createSignedUrl).toHaveBeenCalledWith({
267
+ organizationId: 'org-123',
268
+ bucket: 'org-files',
269
+ path: 'files/test.pdf',
270
+ expiresIn: 3600
271
+ })
272
+ expect(result).toEqual({
273
+ success: true,
274
+ signedUrl: 'https://storage.supabase.co/signed/test-url',
275
+ expiresAt: '2025-01-24T12:00:00.000Z'
276
+ })
277
+ })
278
+
279
+ it('uses default expiresIn when not provided (via schema)', async () => {
280
+ const expiresAt = new Date('2025-01-25T12:00:00Z')
281
+ mockStorageService.createSignedUrl.mockResolvedValue({
282
+ signedUrl: 'https://storage.supabase.co/signed/test-url',
283
+ path: 'files/test.pdf',
284
+ expiresAt
285
+ })
286
+
287
+ const tool = createStorageSignedUrlTool(mockConfig)
288
+ // Parse input through schema to get default
289
+ const parsedInput = StorageSignedUrlInputSchema.parse({
290
+ bucket: 'org-files',
291
+ path: 'files/test.pdf'
292
+ })
293
+
294
+ await tool.execute({
295
+ input: parsedInput,
296
+ executionContext: mockContext
297
+ })
298
+
299
+ expect(mockStorageService.createSignedUrl).toHaveBeenCalledWith(
300
+ expect.objectContaining({
301
+ expiresIn: 86400 // Default 24 hours
302
+ })
303
+ )
304
+ })
305
+ })
306
+
307
+ describe('error handling', () => {
308
+ it('throws Error when executionContext is missing', async () => {
309
+ const tool = createStorageSignedUrlTool(mockConfig)
310
+
311
+ await expect(
312
+ tool.execute({
313
+ input: {
314
+ bucket: 'org-files',
315
+ path: 'files/test.pdf',
316
+ expiresIn: 3600
317
+ }
318
+ })
319
+ ).rejects.toThrow('ExecutionContext is unavailable')
320
+ })
321
+
322
+ it('throws ToolingError when storageService is unavailable', async () => {
323
+ vi.mocked(getToolServices).mockReturnValue({
324
+ commandQueueService: null,
325
+ taskSchedulerService: null,
326
+ notificationsService: null,
327
+ emailService: null,
328
+ credentialsService: null,
329
+ integrationService: null,
330
+ leadService: null,
331
+ storageService: null
332
+ })
333
+
334
+ const tool = createStorageSignedUrlTool(mockConfig)
335
+
336
+ try {
337
+ await tool.execute({
338
+ input: {
339
+ bucket: 'org-files',
340
+ path: 'files/test.pdf',
341
+ expiresIn: 3600
342
+ },
343
+ executionContext: mockContext
344
+ })
345
+ expect.fail('Should have thrown')
346
+ } catch (error) {
347
+ expect(error).toBeInstanceOf(ToolingError)
348
+ expect((error as ToolingError).errorType).toBe('service_unavailable')
349
+ }
350
+ })
351
+ })
352
+ })
353
+
354
+ describe('createStorageDownloadTool', () => {
355
+ describe('tool creation', () => {
356
+ it('creates tool with correct name and description', () => {
357
+ const tool = createStorageDownloadTool({
358
+ name: 'download_file',
359
+ description: 'Download file'
360
+ })
361
+
362
+ expect(tool.name).toBe('download_file')
363
+ expect(tool.description).toBe('Download file')
364
+ })
365
+
366
+ it('uses StorageDownloadInputSchema as inputSchema', () => {
367
+ const tool = createStorageDownloadTool(mockConfig)
368
+
369
+ expect(tool.inputSchema).toBe(StorageDownloadInputSchema)
370
+ })
371
+ })
372
+
373
+ describe('execute', () => {
374
+ it('downloads file and returns base64 content', async () => {
375
+ const fileContent = 'test file content'
376
+ const mockBlob = new Blob([fileContent], { type: 'application/pdf' })
377
+ mockStorageService.download.mockResolvedValue(mockBlob)
378
+
379
+ const tool = createStorageDownloadTool(mockConfig)
380
+ const result = await tool.execute({
381
+ input: {
382
+ bucket: 'org-files',
383
+ path: 'files/test.pdf'
384
+ },
385
+ executionContext: mockContext
386
+ })
387
+
388
+ expect(mockStorageService.download).toHaveBeenCalledWith({
389
+ organizationId: 'org-123',
390
+ bucket: 'org-files',
391
+ path: 'files/test.pdf'
392
+ })
393
+ expect(result.success).toBe(true)
394
+ expect(result.content).toBeTruthy()
395
+ // Verify base64 decodes back to original content
396
+ expect(Buffer.from(result.content, 'base64').toString()).toBe(fileContent)
397
+ expect(result.contentType).toBe('application/pdf')
398
+ })
399
+
400
+ it('handles blob without type', async () => {
401
+ const mockBlob = new Blob(['content'])
402
+ Object.defineProperty(mockBlob, 'type', { value: '' })
403
+ mockStorageService.download.mockResolvedValue(mockBlob)
404
+
405
+ const tool = createStorageDownloadTool(mockConfig)
406
+ const result = await tool.execute({
407
+ input: {
408
+ bucket: 'org-files',
409
+ path: 'files/test.bin'
410
+ },
411
+ executionContext: mockContext
412
+ })
413
+
414
+ expect(result.success).toBe(true)
415
+ expect(result.contentType).toBeUndefined()
416
+ })
417
+ })
418
+
419
+ describe('error handling', () => {
420
+ it('throws Error when executionContext is missing', async () => {
421
+ const tool = createStorageDownloadTool(mockConfig)
422
+
423
+ await expect(
424
+ tool.execute({
425
+ input: {
426
+ bucket: 'org-files',
427
+ path: 'files/test.pdf'
428
+ }
429
+ })
430
+ ).rejects.toThrow('ExecutionContext is unavailable')
431
+ })
432
+
433
+ it('throws ToolingError when storageService is unavailable', async () => {
434
+ vi.mocked(getToolServices).mockReturnValue({
435
+ commandQueueService: null,
436
+ taskSchedulerService: null,
437
+ notificationsService: null,
438
+ emailService: null,
439
+ credentialsService: null,
440
+ integrationService: null,
441
+ leadService: null,
442
+ storageService: null
443
+ })
444
+
445
+ const tool = createStorageDownloadTool(mockConfig)
446
+
447
+ try {
448
+ await tool.execute({
449
+ input: {
450
+ bucket: 'org-files',
451
+ path: 'files/test.pdf'
452
+ },
453
+ executionContext: mockContext
454
+ })
455
+ expect.fail('Should have thrown')
456
+ } catch (error) {
457
+ expect(error).toBeInstanceOf(ToolingError)
458
+ expect((error as ToolingError).errorType).toBe('service_unavailable')
459
+ }
460
+ })
461
+
462
+ it('propagates errors from storageService', async () => {
463
+ mockStorageService.download.mockRejectedValue(new Error('File not found'))
464
+
465
+ const tool = createStorageDownloadTool(mockConfig)
466
+
467
+ await expect(
468
+ tool.execute({
469
+ input: {
470
+ bucket: 'org-files',
471
+ path: 'files/nonexistent.pdf'
472
+ },
473
+ executionContext: mockContext
474
+ })
475
+ ).rejects.toThrow('File not found')
476
+ })
477
+ })
478
+ })
479
+
480
+ describe('createStorageDeleteTool', () => {
481
+ describe('tool creation', () => {
482
+ it('creates tool with correct name and description', () => {
483
+ const tool = createStorageDeleteTool({
484
+ name: 'delete_file',
485
+ description: 'Delete file'
486
+ })
487
+
488
+ expect(tool.name).toBe('delete_file')
489
+ expect(tool.description).toBe('Delete file')
490
+ })
491
+
492
+ it('uses StorageDeleteInputSchema as inputSchema', () => {
493
+ const tool = createStorageDeleteTool(mockConfig)
494
+
495
+ expect(tool.inputSchema).toBe(StorageDeleteInputSchema)
496
+ })
497
+ })
498
+
499
+ describe('execute', () => {
500
+ it('deletes file with organizationId from context', async () => {
501
+ mockStorageService.delete.mockResolvedValue(undefined)
502
+
503
+ const tool = createStorageDeleteTool(mockConfig)
504
+ const result = await tool.execute({
505
+ input: {
506
+ bucket: 'org-files',
507
+ path: 'files/test.pdf'
508
+ },
509
+ executionContext: mockContext
510
+ })
511
+
512
+ expect(mockStorageService.delete).toHaveBeenCalledWith({
513
+ organizationId: 'org-123',
514
+ bucket: 'org-files',
515
+ path: 'files/test.pdf'
516
+ })
517
+ expect(result).toEqual({ success: true })
518
+ })
519
+ })
520
+
521
+ describe('error handling', () => {
522
+ it('throws Error when executionContext is missing', async () => {
523
+ const tool = createStorageDeleteTool(mockConfig)
524
+
525
+ await expect(
526
+ tool.execute({
527
+ input: {
528
+ bucket: 'org-files',
529
+ path: 'files/test.pdf'
530
+ }
531
+ })
532
+ ).rejects.toThrow('ExecutionContext is unavailable')
533
+ })
534
+
535
+ it('throws ToolingError when storageService is unavailable', async () => {
536
+ vi.mocked(getToolServices).mockReturnValue({
537
+ commandQueueService: null,
538
+ taskSchedulerService: null,
539
+ notificationsService: null,
540
+ emailService: null,
541
+ credentialsService: null,
542
+ integrationService: null,
543
+ leadService: null,
544
+ storageService: null
545
+ })
546
+
547
+ const tool = createStorageDeleteTool(mockConfig)
548
+
549
+ try {
550
+ await tool.execute({
551
+ input: {
552
+ bucket: 'org-files',
553
+ path: 'files/test.pdf'
554
+ },
555
+ executionContext: mockContext
556
+ })
557
+ expect.fail('Should have thrown')
558
+ } catch (error) {
559
+ expect(error).toBeInstanceOf(ToolingError)
560
+ expect((error as ToolingError).errorType).toBe('service_unavailable')
561
+ }
562
+ })
563
+
564
+ it('propagates errors from storageService', async () => {
565
+ mockStorageService.delete.mockRejectedValue(new Error('Permission denied'))
566
+
567
+ const tool = createStorageDeleteTool(mockConfig)
568
+
569
+ await expect(
570
+ tool.execute({
571
+ input: {
572
+ bucket: 'org-files',
573
+ path: 'files/protected.pdf'
574
+ },
575
+ executionContext: mockContext
576
+ })
577
+ ).rejects.toThrow('Permission denied')
578
+ })
579
+ })
580
+ })
581
+
582
+ describe('createStorageListTool', () => {
583
+ describe('tool creation', () => {
584
+ it('creates tool with correct name and description', () => {
585
+ const tool = createStorageListTool({
586
+ name: 'list_files',
587
+ description: 'List files'
588
+ })
589
+
590
+ expect(tool.name).toBe('list_files')
591
+ expect(tool.description).toBe('List files')
592
+ })
593
+
594
+ it('uses StorageListInputSchema as inputSchema', () => {
595
+ const tool = createStorageListTool(mockConfig)
596
+
597
+ expect(tool.inputSchema).toBe(StorageListInputSchema)
598
+ })
599
+ })
600
+
601
+ describe('execute', () => {
602
+ it('lists files with organizationId from context', async () => {
603
+ mockStorageService.list.mockResolvedValue(['file1.pdf', 'file2.png', 'file3.doc'])
604
+
605
+ const tool = createStorageListTool(mockConfig)
606
+ const result = await tool.execute({
607
+ input: {
608
+ bucket: 'org-files',
609
+ prefix: 'proposals/'
610
+ },
611
+ executionContext: mockContext
612
+ })
613
+
614
+ expect(mockStorageService.list).toHaveBeenCalledWith({
615
+ organizationId: 'org-123',
616
+ bucket: 'org-files',
617
+ prefix: 'proposals/'
618
+ })
619
+ expect(result).toEqual({
620
+ success: true,
621
+ files: ['file1.pdf', 'file2.png', 'file3.doc']
622
+ })
623
+ })
624
+
625
+ it('lists files without prefix', async () => {
626
+ mockStorageService.list.mockResolvedValue(['file1.pdf'])
627
+
628
+ const tool = createStorageListTool(mockConfig)
629
+ await tool.execute({
630
+ input: {
631
+ bucket: 'org-files'
632
+ },
633
+ executionContext: mockContext
634
+ })
635
+
636
+ expect(mockStorageService.list).toHaveBeenCalledWith({
637
+ organizationId: 'org-123',
638
+ bucket: 'org-files',
639
+ prefix: undefined
640
+ })
641
+ })
642
+
643
+ it('returns empty array when no files found', async () => {
644
+ mockStorageService.list.mockResolvedValue([])
645
+
646
+ const tool = createStorageListTool(mockConfig)
647
+ const result = await tool.execute({
648
+ input: {
649
+ bucket: 'org-files',
650
+ prefix: 'empty-folder/'
651
+ },
652
+ executionContext: mockContext
653
+ })
654
+
655
+ expect(result).toEqual({
656
+ success: true,
657
+ files: []
658
+ })
659
+ })
660
+ })
661
+
662
+ describe('error handling', () => {
663
+ it('throws Error when executionContext is missing', async () => {
664
+ const tool = createStorageListTool(mockConfig)
665
+
666
+ await expect(
667
+ tool.execute({
668
+ input: {
669
+ bucket: 'org-files'
670
+ }
671
+ })
672
+ ).rejects.toThrow('ExecutionContext is unavailable')
673
+ })
674
+
675
+ it('throws ToolingError when storageService is unavailable', async () => {
676
+ vi.mocked(getToolServices).mockReturnValue({
677
+ commandQueueService: null,
678
+ taskSchedulerService: null,
679
+ notificationsService: null,
680
+ emailService: null,
681
+ credentialsService: null,
682
+ integrationService: null,
683
+ leadService: null,
684
+ storageService: null
685
+ })
686
+
687
+ const tool = createStorageListTool(mockConfig)
688
+
689
+ try {
690
+ await tool.execute({
691
+ input: {
692
+ bucket: 'org-files'
693
+ },
694
+ executionContext: mockContext
695
+ })
696
+ expect.fail('Should have thrown')
697
+ } catch (error) {
698
+ expect(error).toBeInstanceOf(ToolingError)
699
+ expect((error as ToolingError).errorType).toBe('service_unavailable')
700
+ }
701
+ })
702
+
703
+ it('propagates errors from storageService', async () => {
704
+ mockStorageService.list.mockRejectedValue(new Error('Bucket not found'))
705
+
706
+ const tool = createStorageListTool(mockConfig)
707
+
708
+ await expect(
709
+ tool.execute({
710
+ input: {
711
+ bucket: 'nonexistent-bucket'
712
+ },
713
+ executionContext: mockContext
714
+ })
715
+ ).rejects.toThrow('Bucket not found')
716
+ })
717
+ })
718
+ })
719
+
720
+ describe('concurrent execution', () => {
721
+ it('handles multiple concurrent operations', async () => {
722
+ mockStorageService.upload.mockResolvedValue({
723
+ path: 'file1.pdf',
724
+ fullPath: 'org-123/file1.pdf',
725
+ size: 100
726
+ })
727
+ mockStorageService.list.mockResolvedValue(['file1.pdf', 'file2.pdf'])
728
+ mockStorageService.createSignedUrl.mockResolvedValue({
729
+ signedUrl: 'https://signed-url',
730
+ path: 'file1.pdf',
731
+ expiresAt: new Date()
732
+ })
733
+
734
+ const uploadTool = createStorageUploadTool({
735
+ name: 'upload',
736
+ description: 'Upload'
737
+ })
738
+ const listTool = createStorageListTool({
739
+ name: 'list',
740
+ description: 'List'
741
+ })
742
+ const signedUrlTool = createStorageSignedUrlTool({
743
+ name: 'signed_url',
744
+ description: 'Signed URL'
745
+ })
746
+
747
+ const results = await Promise.all([
748
+ uploadTool.execute({
749
+ input: {
750
+ bucket: 'files',
751
+ path: 'file1.pdf',
752
+ content: 'dGVzdA==',
753
+ contentType: 'application/pdf'
754
+ },
755
+ executionContext: mockContext
756
+ }),
757
+ listTool.execute({
758
+ input: { bucket: 'files' },
759
+ executionContext: mockContext
760
+ }),
761
+ signedUrlTool.execute({
762
+ input: {
763
+ bucket: 'files',
764
+ path: 'file1.pdf',
765
+ expiresIn: 3600
766
+ },
767
+ executionContext: mockContext
768
+ })
769
+ ])
770
+
771
+ expect(results).toHaveLength(3)
772
+ expect(results[0]).toEqual({
773
+ success: true,
774
+ path: 'file1.pdf',
775
+ fullPath: 'org-123/file1.pdf'
776
+ })
777
+ expect(results[1]).toEqual({
778
+ success: true,
779
+ files: ['file1.pdf', 'file2.pdf']
780
+ })
781
+ expect(results[2].success).toBe(true)
782
+ expect(results[2].signedUrl).toBe('https://signed-url')
783
+ })
784
+ })
785
+ })
786
+
787
+ describe('Storage Input Schemas', () => {
788
+ describe('StorageUploadInputSchema', () => {
789
+ it('accepts valid upload input with all required fields', () => {
790
+ const result = StorageUploadInputSchema.safeParse({
791
+ bucket: 'org-files',
792
+ path: 'files/test.pdf',
793
+ content: 'dGVzdA==',
794
+ contentType: 'application/pdf'
795
+ })
796
+
797
+ expect(result.success).toBe(true)
798
+ })
799
+
800
+ it('accepts optional upsert flag', () => {
801
+ const result = StorageUploadInputSchema.safeParse({
802
+ bucket: 'org-files',
803
+ path: 'files/test.pdf',
804
+ content: 'dGVzdA==',
805
+ contentType: 'application/pdf',
806
+ upsert: true
807
+ })
808
+
809
+ expect(result.success).toBe(true)
810
+ if (result.success) {
811
+ expect(result.data.upsert).toBe(true)
812
+ }
813
+ })
814
+
815
+ it('rejects missing bucket', () => {
816
+ const result = StorageUploadInputSchema.safeParse({
817
+ path: 'files/test.pdf',
818
+ content: 'dGVzdA==',
819
+ contentType: 'application/pdf'
820
+ })
821
+
822
+ expect(result.success).toBe(false)
823
+ })
824
+
825
+ it('rejects missing path', () => {
826
+ const result = StorageUploadInputSchema.safeParse({
827
+ bucket: 'org-files',
828
+ content: 'dGVzdA==',
829
+ contentType: 'application/pdf'
830
+ })
831
+
832
+ expect(result.success).toBe(false)
833
+ })
834
+
835
+ it('rejects missing content', () => {
836
+ const result = StorageUploadInputSchema.safeParse({
837
+ bucket: 'org-files',
838
+ path: 'files/test.pdf',
839
+ contentType: 'application/pdf'
840
+ })
841
+
842
+ expect(result.success).toBe(false)
843
+ })
844
+
845
+ it('rejects missing contentType', () => {
846
+ const result = StorageUploadInputSchema.safeParse({
847
+ bucket: 'org-files',
848
+ path: 'files/test.pdf',
849
+ content: 'dGVzdA=='
850
+ })
851
+
852
+ expect(result.success).toBe(false)
853
+ })
854
+ })
855
+
856
+ describe('StorageSignedUrlInputSchema', () => {
857
+ it('accepts valid signed URL input', () => {
858
+ const result = StorageSignedUrlInputSchema.safeParse({
859
+ bucket: 'org-files',
860
+ path: 'files/test.pdf',
861
+ expiresIn: 3600
862
+ })
863
+
864
+ expect(result.success).toBe(true)
865
+ })
866
+
867
+ it('uses default expiresIn when not provided', () => {
868
+ const result = StorageSignedUrlInputSchema.safeParse({
869
+ bucket: 'org-files',
870
+ path: 'files/test.pdf'
871
+ })
872
+
873
+ expect(result.success).toBe(true)
874
+ if (result.success) {
875
+ expect(result.data.expiresIn).toBe(86400) // Default 24 hours
876
+ }
877
+ })
878
+
879
+ it('accepts custom expiresIn value', () => {
880
+ const result = StorageSignedUrlInputSchema.safeParse({
881
+ bucket: 'org-files',
882
+ path: 'files/test.pdf',
883
+ expiresIn: 604800 // 7 days
884
+ })
885
+
886
+ expect(result.success).toBe(true)
887
+ if (result.success) {
888
+ expect(result.data.expiresIn).toBe(604800)
889
+ }
890
+ })
891
+
892
+ it('rejects missing bucket', () => {
893
+ const result = StorageSignedUrlInputSchema.safeParse({
894
+ path: 'files/test.pdf',
895
+ expiresIn: 3600
896
+ })
897
+
898
+ expect(result.success).toBe(false)
899
+ })
900
+
901
+ it('rejects missing path', () => {
902
+ const result = StorageSignedUrlInputSchema.safeParse({
903
+ bucket: 'org-files',
904
+ expiresIn: 3600
905
+ })
906
+
907
+ expect(result.success).toBe(false)
908
+ })
909
+ })
910
+
911
+ describe('StorageDownloadInputSchema', () => {
912
+ it('accepts valid download input', () => {
913
+ const result = StorageDownloadInputSchema.safeParse({
914
+ bucket: 'org-files',
915
+ path: 'files/test.pdf'
916
+ })
917
+
918
+ expect(result.success).toBe(true)
919
+ })
920
+
921
+ it('rejects missing bucket', () => {
922
+ const result = StorageDownloadInputSchema.safeParse({
923
+ path: 'files/test.pdf'
924
+ })
925
+
926
+ expect(result.success).toBe(false)
927
+ })
928
+
929
+ it('rejects missing path', () => {
930
+ const result = StorageDownloadInputSchema.safeParse({
931
+ bucket: 'org-files'
932
+ })
933
+
934
+ expect(result.success).toBe(false)
935
+ })
936
+ })
937
+
938
+ describe('StorageDeleteInputSchema', () => {
939
+ it('accepts valid delete input', () => {
940
+ const result = StorageDeleteInputSchema.safeParse({
941
+ bucket: 'org-files',
942
+ path: 'files/test.pdf'
943
+ })
944
+
945
+ expect(result.success).toBe(true)
946
+ })
947
+
948
+ it('rejects missing bucket', () => {
949
+ const result = StorageDeleteInputSchema.safeParse({
950
+ path: 'files/test.pdf'
951
+ })
952
+
953
+ expect(result.success).toBe(false)
954
+ })
955
+
956
+ it('rejects missing path', () => {
957
+ const result = StorageDeleteInputSchema.safeParse({
958
+ bucket: 'org-files'
959
+ })
960
+
961
+ expect(result.success).toBe(false)
962
+ })
963
+ })
964
+
965
+ describe('StorageListInputSchema', () => {
966
+ it('accepts valid list input with prefix', () => {
967
+ const result = StorageListInputSchema.safeParse({
968
+ bucket: 'org-files',
969
+ prefix: 'proposals/'
970
+ })
971
+
972
+ expect(result.success).toBe(true)
973
+ if (result.success) {
974
+ expect(result.data.prefix).toBe('proposals/')
975
+ }
976
+ })
977
+
978
+ it('accepts list input without prefix', () => {
979
+ const result = StorageListInputSchema.safeParse({
980
+ bucket: 'org-files'
981
+ })
982
+
983
+ expect(result.success).toBe(true)
984
+ if (result.success) {
985
+ expect(result.data.prefix).toBeUndefined()
986
+ }
987
+ })
988
+
989
+ it('rejects missing bucket', () => {
990
+ const result = StorageListInputSchema.safeParse({
991
+ prefix: 'proposals/'
992
+ })
993
+
994
+ expect(result.success).toBe(false)
995
+ })
996
+ })
997
+ })