ai-database 0.0.0-development → 0.2.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 (79) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +102 -0
  3. package/README.md +402 -47
  4. package/TESTING.md +410 -0
  5. package/TEST_SUMMARY.md +250 -0
  6. package/TODO.md +128 -0
  7. package/dist/ai-promise-db.d.ts +370 -0
  8. package/dist/ai-promise-db.d.ts.map +1 -0
  9. package/dist/ai-promise-db.js +839 -0
  10. package/dist/ai-promise-db.js.map +1 -0
  11. package/dist/authorization.d.ts +531 -0
  12. package/dist/authorization.d.ts.map +1 -0
  13. package/dist/authorization.js +632 -0
  14. package/dist/authorization.js.map +1 -0
  15. package/dist/durable-clickhouse.d.ts +193 -0
  16. package/dist/durable-clickhouse.d.ts.map +1 -0
  17. package/dist/durable-clickhouse.js +422 -0
  18. package/dist/durable-clickhouse.js.map +1 -0
  19. package/dist/durable-promise.d.ts +182 -0
  20. package/dist/durable-promise.d.ts.map +1 -0
  21. package/dist/durable-promise.js +409 -0
  22. package/dist/durable-promise.js.map +1 -0
  23. package/dist/execution-queue.d.ts +239 -0
  24. package/dist/execution-queue.d.ts.map +1 -0
  25. package/dist/execution-queue.js +400 -0
  26. package/dist/execution-queue.js.map +1 -0
  27. package/dist/index.d.ts +54 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/linguistic.d.ts +115 -0
  32. package/dist/linguistic.d.ts.map +1 -0
  33. package/dist/linguistic.js +379 -0
  34. package/dist/linguistic.js.map +1 -0
  35. package/dist/memory-provider.d.ts +304 -0
  36. package/dist/memory-provider.d.ts.map +1 -0
  37. package/dist/memory-provider.js +785 -0
  38. package/dist/memory-provider.js.map +1 -0
  39. package/dist/schema.d.ts +899 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +1165 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/tests.d.ts +107 -0
  44. package/dist/tests.d.ts.map +1 -0
  45. package/dist/tests.js +568 -0
  46. package/dist/tests.js.map +1 -0
  47. package/dist/types.d.ts +972 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +126 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +37 -23
  52. package/src/ai-promise-db.ts +1243 -0
  53. package/src/authorization.ts +1102 -0
  54. package/src/durable-clickhouse.ts +596 -0
  55. package/src/durable-promise.ts +582 -0
  56. package/src/execution-queue.ts +608 -0
  57. package/src/index.test.ts +868 -0
  58. package/src/index.ts +337 -0
  59. package/src/linguistic.ts +404 -0
  60. package/src/memory-provider.test.ts +1036 -0
  61. package/src/memory-provider.ts +1119 -0
  62. package/src/schema.test.ts +1254 -0
  63. package/src/schema.ts +2296 -0
  64. package/src/tests.ts +725 -0
  65. package/src/types.ts +1177 -0
  66. package/test/README.md +153 -0
  67. package/test/edge-cases.test.ts +646 -0
  68. package/test/provider-resolution.test.ts +402 -0
  69. package/tsconfig.json +9 -0
  70. package/vitest.config.ts +19 -0
  71. package/LICENSE +0 -21
  72. package/dist/types/database.d.ts +0 -46
  73. package/dist/types/document.d.ts +0 -15
  74. package/dist/types/index.d.ts +0 -5
  75. package/dist/types/mdxdb/embedding.d.ts +0 -7
  76. package/dist/types/mdxdb/types.d.ts +0 -59
  77. package/dist/types/synthetic.d.ts +0 -9
  78. package/dist/types/tools.d.ts +0 -10
  79. package/dist/types/vector.d.ts +0 -16
package/src/tests.ts ADDED
@@ -0,0 +1,725 @@
1
+ /**
2
+ * Unified Compliance Test Suite for Database Adapters
3
+ *
4
+ * Provides reusable test factories that any DBClient/DBClientExtended
5
+ * implementation can use to verify compliance with the interface.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createTests } from 'ai-database/tests'
10
+ * import { createClickHouseDatabase } from '@mdxdb/clickhouse'
11
+ *
12
+ * createTests('ClickHouse', () => createClickHouseDatabase({ url: '...' }))
13
+ * ```
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+
18
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
19
+ import type {
20
+ DBClient,
21
+ DBClientExtended,
22
+ Thing,
23
+ Relationship,
24
+ QueryOptions,
25
+ CreateOptions,
26
+ Event,
27
+ Action,
28
+ Artifact,
29
+ } from './types.js'
30
+
31
+ // =============================================================================
32
+ // Test Fixtures
33
+ // =============================================================================
34
+
35
+ export const fixtures = {
36
+ /** Sample namespace for tests */
37
+ ns: 'test.example.com',
38
+
39
+ /** Sample users */
40
+ users: [
41
+ { id: 'user-1', name: 'Alice', email: 'alice@example.com', role: 'admin' },
42
+ { id: 'user-2', name: 'Bob', email: 'bob@example.com', role: 'user' },
43
+ { id: 'user-3', name: 'Charlie', email: 'charlie@example.com', role: 'user' },
44
+ ],
45
+
46
+ /** Sample posts */
47
+ posts: [
48
+ { id: 'post-1', title: 'Hello World', content: 'First post', authorId: 'user-1' },
49
+ { id: 'post-2', title: 'Second Post', content: 'More content', authorId: 'user-1' },
50
+ { id: 'post-3', title: 'Bobs Post', content: 'From Bob', authorId: 'user-2' },
51
+ ],
52
+
53
+ /** Sample tags */
54
+ tags: [
55
+ { id: 'tag-1', name: 'typescript' },
56
+ { id: 'tag-2', name: 'database' },
57
+ { id: 'tag-3', name: 'testing' },
58
+ ],
59
+ }
60
+
61
+ // =============================================================================
62
+ // Test Options
63
+ // =============================================================================
64
+
65
+ export interface CreateTestsOptions<T extends DBClient = DBClient> {
66
+ /** Factory function to create the client */
67
+ factory: () => T | Promise<T>
68
+ /** Optional cleanup function called after all tests */
69
+ cleanup?: () => void | Promise<void>
70
+ /** Skip certain test groups */
71
+ skip?: {
72
+ relationships?: boolean
73
+ search?: boolean
74
+ events?: boolean
75
+ actions?: boolean
76
+ artifacts?: boolean
77
+ }
78
+ /** Custom namespace (defaults to fixtures.ns) */
79
+ ns?: string
80
+ }
81
+
82
+ // =============================================================================
83
+ // Main Test Factory
84
+ // =============================================================================
85
+
86
+ /**
87
+ * Create a comprehensive test suite for a DBClient implementation
88
+ *
89
+ * @example Basic usage
90
+ * ```ts
91
+ * import { createTests } from 'ai-database/tests'
92
+ *
93
+ * createTests('Memory', {
94
+ * factory: () => new MemoryDBClient()
95
+ * })
96
+ * ```
97
+ *
98
+ * @example With cleanup
99
+ * ```ts
100
+ * createTests('SQLite', {
101
+ * factory: async () => {
102
+ * const db = await createSQLiteClient({ path: ':memory:' })
103
+ * return db
104
+ * },
105
+ * cleanup: async () => {
106
+ * // cleanup temp files
107
+ * }
108
+ * })
109
+ * ```
110
+ *
111
+ * @example Skip certain tests
112
+ * ```ts
113
+ * createTests('Filesystem', {
114
+ * factory: () => createFsClient({ root: tempDir }),
115
+ * skip: { events: true, actions: true, artifacts: true }
116
+ * })
117
+ * ```
118
+ */
119
+ export function createTests<T extends DBClient>(
120
+ name: string,
121
+ options: CreateTestsOptions<T>
122
+ ): void {
123
+ const { factory, cleanup, skip = {}, ns = fixtures.ns } = options
124
+
125
+ describe(`${name} Compliance Tests`, () => {
126
+ let client: T
127
+
128
+ beforeAll(async () => {
129
+ client = await factory()
130
+ })
131
+
132
+ afterAll(async () => {
133
+ await client.close?.()
134
+ await cleanup?.()
135
+ })
136
+
137
+ // =========================================================================
138
+ // Thing CRUD Operations
139
+ // =========================================================================
140
+
141
+ describe('Things - CRUD', () => {
142
+ const testThing = {
143
+ ns,
144
+ type: 'TestEntity',
145
+ id: 'crud-test-1',
146
+ data: { name: 'Test', value: 42 },
147
+ }
148
+
149
+ afterAll(async () => {
150
+ // Cleanup test data
151
+ try {
152
+ await client.delete(`https://${ns}/TestEntity/crud-test-1`)
153
+ await client.delete(`https://${ns}/TestEntity/crud-test-2`)
154
+ } catch {
155
+ // Ignore cleanup errors
156
+ }
157
+ })
158
+
159
+ it('creates a thing', async () => {
160
+ const thing = await client.create(testThing)
161
+
162
+ expect(thing).toBeDefined()
163
+ expect(thing.ns).toBe(ns)
164
+ expect(thing.type).toBe('TestEntity')
165
+ expect(thing.id).toBe('crud-test-1')
166
+ expect(thing.data.name).toBe('Test')
167
+ expect(thing.data.value).toBe(42)
168
+ expect(thing.createdAt).toBeInstanceOf(Date)
169
+ expect(thing.updatedAt).toBeInstanceOf(Date)
170
+ })
171
+
172
+ it('gets a thing by URL', async () => {
173
+ const thing = await client.get(`https://${ns}/TestEntity/crud-test-1`)
174
+
175
+ expect(thing).not.toBeNull()
176
+ expect(thing?.data.name).toBe('Test')
177
+ })
178
+
179
+ it('gets a thing by ID components', async () => {
180
+ const thing = await client.getById(ns, 'TestEntity', 'crud-test-1')
181
+
182
+ expect(thing).not.toBeNull()
183
+ expect(thing?.data.name).toBe('Test')
184
+ })
185
+
186
+ it('returns null for non-existent thing', async () => {
187
+ const thing = await client.get(`https://${ns}/TestEntity/does-not-exist`)
188
+
189
+ expect(thing).toBeNull()
190
+ })
191
+
192
+ it('updates a thing', async () => {
193
+ const updated = await client.update(`https://${ns}/TestEntity/crud-test-1`, {
194
+ data: { name: 'Updated', value: 100 },
195
+ })
196
+
197
+ expect(updated.data.name).toBe('Updated')
198
+ expect(updated.data.value).toBe(100)
199
+ expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(updated.createdAt.getTime())
200
+ })
201
+
202
+ it('upserts a new thing', async () => {
203
+ const thing = await client.upsert({
204
+ ns,
205
+ type: 'TestEntity',
206
+ id: 'crud-test-2',
207
+ data: { name: 'Upserted', value: 200 },
208
+ })
209
+
210
+ expect(thing.id).toBe('crud-test-2')
211
+ expect(thing.data.name).toBe('Upserted')
212
+ })
213
+
214
+ it('upserts an existing thing', async () => {
215
+ const thing = await client.upsert({
216
+ ns,
217
+ type: 'TestEntity',
218
+ id: 'crud-test-2',
219
+ data: { name: 'Upserted Again', value: 300 },
220
+ })
221
+
222
+ expect(thing.data.name).toBe('Upserted Again')
223
+ expect(thing.data.value).toBe(300)
224
+ })
225
+
226
+ it('deletes a thing', async () => {
227
+ // Create then delete
228
+ await client.create({
229
+ ns,
230
+ type: 'TestEntity',
231
+ id: 'to-delete',
232
+ data: { temp: true },
233
+ })
234
+
235
+ const deleted = await client.delete(`https://${ns}/TestEntity/to-delete`)
236
+ expect(deleted).toBe(true)
237
+
238
+ const thing = await client.get(`https://${ns}/TestEntity/to-delete`)
239
+ expect(thing).toBeNull()
240
+ })
241
+
242
+ it('returns false when deleting non-existent thing', async () => {
243
+ const deleted = await client.delete(`https://${ns}/TestEntity/never-existed`)
244
+ expect(deleted).toBe(false)
245
+ })
246
+ })
247
+
248
+ // =========================================================================
249
+ // Query Operations
250
+ // =========================================================================
251
+
252
+ describe('Things - Queries', () => {
253
+ beforeAll(async () => {
254
+ // Seed test data
255
+ for (const user of fixtures.users) {
256
+ await client.upsert({
257
+ ns,
258
+ type: 'User',
259
+ id: user.id,
260
+ data: user,
261
+ })
262
+ }
263
+ for (const post of fixtures.posts) {
264
+ await client.upsert({
265
+ ns,
266
+ type: 'Post',
267
+ id: post.id,
268
+ data: post,
269
+ })
270
+ }
271
+ })
272
+
273
+ afterAll(async () => {
274
+ // Cleanup
275
+ for (const user of fixtures.users) {
276
+ try { await client.delete(`https://${ns}/User/${user.id}`) } catch { /* ignore */ }
277
+ }
278
+ for (const post of fixtures.posts) {
279
+ try { await client.delete(`https://${ns}/Post/${post.id}`) } catch { /* ignore */ }
280
+ }
281
+ })
282
+
283
+ it('lists all things', async () => {
284
+ const things = await client.list({ ns })
285
+
286
+ expect(things.length).toBeGreaterThanOrEqual(fixtures.users.length + fixtures.posts.length)
287
+ })
288
+
289
+ it('lists things by type', async () => {
290
+ const users = await client.list({ ns, type: 'User' })
291
+
292
+ expect(users.length).toBe(fixtures.users.length)
293
+ expect(users.every(u => u.type === 'User')).toBe(true)
294
+ })
295
+
296
+ it('finds things with where clause', async () => {
297
+ const admins = await client.find({
298
+ ns,
299
+ type: 'User',
300
+ where: { role: 'admin' },
301
+ })
302
+
303
+ expect(admins.length).toBe(1)
304
+ expect(admins[0]?.data.name).toBe('Alice')
305
+ })
306
+
307
+ it('lists with limit', async () => {
308
+ const things = await client.list({ ns, type: 'User', limit: 2 })
309
+
310
+ expect(things.length).toBe(2)
311
+ })
312
+
313
+ it('lists with offset', async () => {
314
+ const all = await client.list({ ns, type: 'User', orderBy: 'id' })
315
+ const offset = await client.list({ ns, type: 'User', orderBy: 'id', offset: 1, limit: 2 })
316
+
317
+ expect(offset.length).toBe(2)
318
+ expect(offset[0]?.id).toBe(all[1]?.id)
319
+ })
320
+
321
+ it('lists with ordering', async () => {
322
+ const asc = await client.list({ ns, type: 'User', orderBy: 'id', order: 'asc' })
323
+ const desc = await client.list({ ns, type: 'User', orderBy: 'id', order: 'desc' })
324
+
325
+ expect(asc[0]?.id).toBe('user-1')
326
+ expect(desc[0]?.id).toBe('user-3')
327
+ })
328
+
329
+ it('iterates with forEach', async () => {
330
+ const ids: string[] = []
331
+
332
+ await client.forEach({ ns, type: 'User' }, (thing) => {
333
+ ids.push(thing.id)
334
+ })
335
+
336
+ expect(ids.length).toBe(fixtures.users.length)
337
+ })
338
+ })
339
+
340
+ // =========================================================================
341
+ // Search Operations
342
+ // =========================================================================
343
+
344
+ if (!skip.search) {
345
+ describe('Things - Search', () => {
346
+ beforeAll(async () => {
347
+ // Ensure test data exists
348
+ for (const post of fixtures.posts) {
349
+ await client.upsert({
350
+ ns,
351
+ type: 'Post',
352
+ id: post.id,
353
+ data: post,
354
+ })
355
+ }
356
+ })
357
+
358
+ it('searches by query string', async () => {
359
+ const results = await client.search({
360
+ ns,
361
+ type: 'Post',
362
+ query: 'Hello',
363
+ })
364
+
365
+ expect(results.length).toBeGreaterThanOrEqual(1)
366
+ expect(results.some(r => r.data.title === 'Hello World')).toBe(true)
367
+ })
368
+
369
+ it('searches across fields', async () => {
370
+ const results = await client.search({
371
+ ns,
372
+ type: 'Post',
373
+ query: 'Bob',
374
+ })
375
+
376
+ expect(results.some(r => r.data.title === 'Bobs Post')).toBe(true)
377
+ })
378
+ })
379
+ }
380
+
381
+ // =========================================================================
382
+ // Relationship Operations
383
+ // =========================================================================
384
+
385
+ if (!skip.relationships) {
386
+ describe('Relationships', () => {
387
+ const authorUrl = `https://${ns}/User/user-1`
388
+ const postUrl = `https://${ns}/Post/post-1`
389
+
390
+ beforeAll(async () => {
391
+ // Ensure entities exist
392
+ await client.upsert({ ns, type: 'User', id: 'user-1', data: fixtures.users[0]! })
393
+ await client.upsert({ ns, type: 'Post', id: 'post-1', data: fixtures.posts[0]! })
394
+ })
395
+
396
+ afterAll(async () => {
397
+ try { await client.unrelate(postUrl, 'author', authorUrl) } catch { /* ignore */ }
398
+ })
399
+
400
+ it('creates a relationship', async () => {
401
+ const rel = await client.relate({
402
+ type: 'author',
403
+ from: postUrl,
404
+ to: authorUrl,
405
+ })
406
+
407
+ expect(rel).toBeDefined()
408
+ expect(rel.type).toBe('author')
409
+ expect(rel.from).toBe(postUrl)
410
+ expect(rel.to).toBe(authorUrl)
411
+ })
412
+
413
+ it('queries outbound related things', async () => {
414
+ const authors = await client.related(postUrl, 'author', 'to')
415
+
416
+ expect(authors.length).toBe(1)
417
+ expect(authors[0]?.id).toBe('user-1')
418
+ })
419
+
420
+ it('queries inbound references', async () => {
421
+ const posts = await client.references(authorUrl, 'author')
422
+
423
+ expect(posts.length).toBeGreaterThanOrEqual(1)
424
+ expect(posts.some(p => p.id === 'post-1')).toBe(true)
425
+ })
426
+
427
+ it('lists relationships', async () => {
428
+ const rels = await client.relationships(postUrl, 'author')
429
+
430
+ expect(rels.length).toBeGreaterThanOrEqual(1)
431
+ expect(rels[0]?.type).toBe('author')
432
+ })
433
+
434
+ it('removes a relationship', async () => {
435
+ const removed = await client.unrelate(postUrl, 'author', authorUrl)
436
+ expect(removed).toBe(true)
437
+
438
+ const rels = await client.relationships(postUrl, 'author')
439
+ expect(rels.length).toBe(0)
440
+ })
441
+ })
442
+ }
443
+ })
444
+ }
445
+
446
+ // =============================================================================
447
+ // Extended Test Factory (Events, Actions, Artifacts)
448
+ // =============================================================================
449
+
450
+ /**
451
+ * Create extended tests for DBClientExtended implementations
452
+ *
453
+ * Includes all DBClient tests plus Events, Actions, and Artifacts
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * import { createExtendedTests } from 'ai-database/tests'
458
+ *
459
+ * createExtendedTests('ClickHouse', {
460
+ * factory: () => createClickHouseDatabase({ url: '...' })
461
+ * })
462
+ * ```
463
+ */
464
+ export function createExtendedTests<T extends DBClientExtended>(
465
+ name: string,
466
+ options: CreateTestsOptions<T>
467
+ ): void {
468
+ const { factory, cleanup, skip = {}, ns = fixtures.ns } = options
469
+
470
+ // Run base DBClient tests
471
+ createTests(name, options as CreateTestsOptions<DBClient>)
472
+
473
+ describe(`${name} Extended Compliance Tests`, () => {
474
+ let client: T
475
+
476
+ beforeAll(async () => {
477
+ client = await factory()
478
+ })
479
+
480
+ afterAll(async () => {
481
+ await client.close?.()
482
+ await cleanup?.()
483
+ })
484
+
485
+ // =========================================================================
486
+ // Event Operations
487
+ // =========================================================================
488
+
489
+ if (!skip.events) {
490
+ describe('Events', () => {
491
+ let eventId: string
492
+
493
+ it('tracks an event', async () => {
494
+ const event = await client.track({
495
+ type: 'User.created',
496
+ source: 'test-suite',
497
+ data: { userId: 'user-1', action: 'signup' },
498
+ })
499
+
500
+ eventId = event.id
501
+
502
+ expect(event).toBeDefined()
503
+ expect(event.id).toBeDefined()
504
+ expect(event.type).toBe('User.created')
505
+ expect(event.source).toBe('test-suite')
506
+ expect(event.timestamp).toBeInstanceOf(Date)
507
+ })
508
+
509
+ it('gets an event by ID', async () => {
510
+ const event = await client.getEvent(eventId)
511
+
512
+ expect(event).not.toBeNull()
513
+ expect(event?.type).toBe('User.created')
514
+ })
515
+
516
+ it('queries events by type', async () => {
517
+ const events = await client.queryEvents({ type: 'User.created' })
518
+
519
+ expect(events.length).toBeGreaterThanOrEqual(1)
520
+ expect(events.every(e => e.type === 'User.created')).toBe(true)
521
+ })
522
+
523
+ it('queries events by source', async () => {
524
+ const events = await client.queryEvents({ source: 'test-suite' })
525
+
526
+ expect(events.length).toBeGreaterThanOrEqual(1)
527
+ })
528
+
529
+ it('tracks event with correlation ID', async () => {
530
+ const event = await client.track({
531
+ type: 'Order.placed',
532
+ source: 'test-suite',
533
+ data: { orderId: 'order-1' },
534
+ correlationId: 'session-123',
535
+ })
536
+
537
+ expect(event.correlationId).toBe('session-123')
538
+
539
+ const related = await client.queryEvents({ correlationId: 'session-123' })
540
+ expect(related.some(e => e.id === event.id)).toBe(true)
541
+ })
542
+ })
543
+ }
544
+
545
+ // =========================================================================
546
+ // Action Operations
547
+ // =========================================================================
548
+
549
+ if (!skip.actions) {
550
+ describe('Actions', () => {
551
+ let actionId: string
552
+
553
+ it('sends an action (pending)', async () => {
554
+ const action = await client.send({
555
+ actor: 'user:test-user',
556
+ object: `https://${ns}/Order/order-1`,
557
+ action: 'approve',
558
+ })
559
+
560
+ actionId = action.id
561
+
562
+ expect(action).toBeDefined()
563
+ expect(action.status).toBe('pending')
564
+ expect(action.actor).toBe('user:test-user')
565
+ expect(action.action).toBe('approve')
566
+ })
567
+
568
+ it('does an action (active)', async () => {
569
+ const action = await client.do({
570
+ actor: 'user:test-user',
571
+ object: `https://${ns}/Order/order-2`,
572
+ action: 'process',
573
+ })
574
+
575
+ expect(action.status).toBe('active')
576
+ expect(action.startedAt).toBeInstanceOf(Date)
577
+ })
578
+
579
+ it('gets an action by ID', async () => {
580
+ const action = await client.getAction(actionId)
581
+
582
+ expect(action).not.toBeNull()
583
+ expect(action?.action).toBe('approve')
584
+ })
585
+
586
+ it('starts a pending action', async () => {
587
+ const started = await client.startAction(actionId)
588
+
589
+ expect(started.status).toBe('active')
590
+ expect(started.startedAt).toBeInstanceOf(Date)
591
+ })
592
+
593
+ it('completes an action', async () => {
594
+ const completed = await client.completeAction(actionId, { approved: true })
595
+
596
+ expect(completed.status).toBe('completed')
597
+ expect(completed.completedAt).toBeInstanceOf(Date)
598
+ expect(completed.result).toEqual({ approved: true })
599
+ })
600
+
601
+ it('fails an action', async () => {
602
+ const action = await client.do({
603
+ actor: 'system',
604
+ object: `https://${ns}/Task/task-1`,
605
+ action: 'process',
606
+ })
607
+
608
+ const failed = await client.failAction(action.id, 'Connection timeout')
609
+
610
+ expect(failed.status).toBe('failed')
611
+ expect(failed.error).toBe('Connection timeout')
612
+ })
613
+
614
+ it('cancels an action', async () => {
615
+ const action = await client.send({
616
+ actor: 'user:test-user',
617
+ object: `https://${ns}/Report/report-1`,
618
+ action: 'generate',
619
+ })
620
+
621
+ const cancelled = await client.cancelAction(action.id)
622
+
623
+ expect(cancelled.status).toBe('cancelled')
624
+ })
625
+
626
+ it('queries actions by status', async () => {
627
+ const completed = await client.queryActions({ status: 'completed' })
628
+
629
+ expect(completed.every(a => a.status === 'completed')).toBe(true)
630
+ })
631
+
632
+ it('queries actions by actor', async () => {
633
+ const actions = await client.queryActions({ actor: 'user:test-user' })
634
+
635
+ expect(actions.every(a => a.actor === 'user:test-user')).toBe(true)
636
+ })
637
+ })
638
+ }
639
+
640
+ // =========================================================================
641
+ // Artifact Operations
642
+ // =========================================================================
643
+
644
+ if (!skip.artifacts) {
645
+ describe('Artifacts', () => {
646
+ const artifactKey = 'test-artifact-1'
647
+
648
+ afterAll(async () => {
649
+ try { await client.deleteArtifact(artifactKey) } catch { /* ignore */ }
650
+ })
651
+
652
+ it('stores an artifact', async () => {
653
+ const artifact = await client.storeArtifact({
654
+ key: artifactKey,
655
+ type: 'esm',
656
+ source: `https://${ns}/Module/module-1`,
657
+ sourceHash: 'abc123',
658
+ content: 'export const foo = 42',
659
+ })
660
+
661
+ expect(artifact).toBeDefined()
662
+ expect(artifact.key).toBe(artifactKey)
663
+ expect(artifact.type).toBe('esm')
664
+ expect(artifact.content).toBe('export const foo = 42')
665
+ })
666
+
667
+ it('gets an artifact by key', async () => {
668
+ const artifact = await client.getArtifact(artifactKey)
669
+
670
+ expect(artifact).not.toBeNull()
671
+ expect(artifact?.content).toBe('export const foo = 42')
672
+ })
673
+
674
+ it('gets artifact by source', async () => {
675
+ const artifact = await client.getArtifactBySource(
676
+ `https://${ns}/Module/module-1`,
677
+ 'esm'
678
+ )
679
+
680
+ expect(artifact).not.toBeNull()
681
+ expect(artifact?.key).toBe(artifactKey)
682
+ })
683
+
684
+ it('deletes an artifact', async () => {
685
+ await client.storeArtifact({
686
+ key: 'to-delete',
687
+ type: 'ast',
688
+ source: `https://${ns}/File/file-1`,
689
+ sourceHash: 'xyz789',
690
+ content: { type: 'Program', body: [] },
691
+ })
692
+
693
+ const deleted = await client.deleteArtifact('to-delete')
694
+ expect(deleted).toBe(true)
695
+
696
+ const artifact = await client.getArtifact('to-delete')
697
+ expect(artifact).toBeNull()
698
+ })
699
+
700
+ it('stores artifact with TTL', async () => {
701
+ const artifact = await client.storeArtifact({
702
+ key: 'ttl-artifact',
703
+ type: 'html',
704
+ source: `https://${ns}/Page/page-1`,
705
+ sourceHash: 'def456',
706
+ content: '<html></html>',
707
+ ttl: 60000, // 1 minute
708
+ })
709
+
710
+ expect(artifact.expiresAt).toBeInstanceOf(Date)
711
+ expect(artifact.expiresAt!.getTime()).toBeGreaterThan(Date.now())
712
+
713
+ // Cleanup
714
+ await client.deleteArtifact('ttl-artifact')
715
+ })
716
+ })
717
+ }
718
+ })
719
+ }
720
+
721
+ // =============================================================================
722
+ // Re-export for convenience
723
+ // =============================================================================
724
+
725
+ export { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'