interaqt 0.3.0 → 0.3.1

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 (35) hide show
  1. package/agent/.claude/agents/code-generation-handler.md +2 -0
  2. package/agent/.claude/agents/computation-generation-handler.md +1 -0
  3. package/agent/.claude/agents/implement-design-handler.md +4 -13
  4. package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
  5. package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
  6. package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
  7. package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
  8. package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
  9. package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
  10. package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
  11. package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
  12. package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
  13. package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
  14. package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
  15. package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
  16. package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
  17. package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
  18. package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
  19. package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
  20. package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
  21. package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
  22. package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
  23. package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
  24. package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
  25. package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
  26. package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
  27. package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
  28. package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
  29. package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
  30. package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
  31. package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
  32. package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
  33. package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
  34. package/agent/agentspace/knowledge/usage/README.md +148 -0
  35. package/package.json +1 -1
@@ -0,0 +1,674 @@
1
+ # Test Implementation Guide
2
+
3
+ ## Overview
4
+ Testing in interaqt focuses on interactions as the primary way to verify business logic. Since all data creation, updates, and deletions flow through interactions, comprehensive interaction testing provides complete coverage.
5
+
6
+ ## 🔴 CRITICAL: Testing Philosophy
7
+
8
+ ### Core Principles
9
+ 1. **Test Through Interactions Only**: All business logic testing must use `callInteraction()`
10
+ 2. **Storage APIs Bypass Validation**: `storage.create/update/delete` are ONLY for test setup
11
+ 3. **No Entity/Relation Unit Tests**: These are implementation details tested through interactions
12
+ 4. **Error Handling**: interaqt returns errors in result.error, never throws exceptions
13
+
14
+ ### Common Mistakes
15
+ ```typescript
16
+ // ❌ WRONG: These APIs don't exist
17
+ controller.run()
18
+ controller.execute()
19
+ storage.findByProperty()
20
+
21
+ // ❌ WRONG: Direct storage manipulation for business logic
22
+ await storage.create('Style', { ... }) // Bypasses ALL validation!
23
+
24
+ // ❌ WRONG: Try-catch for errors
25
+ try {
26
+ await controller.callInteraction(...)
27
+ } catch (e) {
28
+ // interaqt doesn't throw exceptions
29
+ }
30
+
31
+ // ✅ CORRECT: Test through interactions
32
+ const result = await controller.callInteraction('CreateStyle', { ... })
33
+ expect(result.error).toBeUndefined()
34
+ ```
35
+
36
+ ## callInteraction Return Value
37
+
38
+ The `controller.callInteraction()` method returns a `InteractionCallResponse` object with the following structure:
39
+
40
+ ```typescript
41
+ type InteractionCallResponse = {
42
+ // Contains error information if the interaction failed
43
+ error?: unknown
44
+
45
+ // For GET interactions: contains the retrieved data
46
+ data?: unknown
47
+
48
+ // The interaction event that was processed
49
+ event?: InteractionEvent
50
+
51
+ // Record mutations (create/update/delete) that occurred
52
+ effects?: RecordMutationEvent[]
53
+
54
+ // Results from side effects defined in the interaction
55
+ sideEffects?: {
56
+ [effectName: string]: {
57
+ result?: unknown
58
+ error?: unknown
59
+ }
60
+ }
61
+
62
+ // Additional context (e.g., activityId for activity interactions)
63
+ context?: {
64
+ [key: string]: unknown
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### Common Usage Patterns
70
+
71
+ #### 🔴 IMPORTANT: Accessing Created/Updated Data from Effects
72
+
73
+ When calling interactions that create, update, or delete data, the `result.effects` array contains detailed information about all record mutations:
74
+
75
+ ```typescript
76
+ // Example: Getting created record ID and data from effects
77
+ test('should create a style and get the created record from effects', async () => {
78
+ const result = await controller.callInteraction('CreateStyle', {
79
+ user: adminUser,
80
+ payload: { label: 'Modern', slug: 'modern' }
81
+ })
82
+
83
+ expect(result.error).toBeUndefined()
84
+
85
+ // Access the created record from effects
86
+ const createEffect = result.effects?.find(e =>
87
+ e.type === 'create' && e.recordName === 'Style'
88
+ )
89
+
90
+ // The created record contains all fields with their values
91
+ expect(createEffect).toBeDefined()
92
+ expect(createEffect?.record?.id).toBeDefined() // Auto-generated ID
93
+ expect(createEffect?.record?.label).toBe('Modern')
94
+ expect(createEffect?.record?.slug).toBe('modern')
95
+ expect(createEffect?.record?.status).toBe('draft') // Default value
96
+
97
+ // You can use the ID from effects for subsequent operations
98
+ const styleId = createEffect?.record?.id
99
+ })
100
+
101
+ // Example: Tracking multiple mutations in one interaction
102
+ test('should track all mutations when creating related data', async () => {
103
+ const result = await controller.callInteraction('CreatePostWithTags', {
104
+ user: authorUser,
105
+ payload: {
106
+ title: 'Test Post',
107
+ tags: ['tech', 'news']
108
+ }
109
+ })
110
+
111
+ // Check all effects
112
+ const postCreate = result.effects?.find(e =>
113
+ e.type === 'create' && e.recordName === 'Post'
114
+ )
115
+ const tagCreates = result.effects?.filter(e =>
116
+ e.type === 'create' && e.recordName === 'Tag'
117
+ )
118
+ const relationCreates = result.effects?.filter(e =>
119
+ e.type === 'create' && e.recordName === 'PostTagRelation'
120
+ )
121
+
122
+ expect(postCreate).toBeDefined()
123
+ expect(tagCreates).toHaveLength(2)
124
+ expect(relationCreates).toHaveLength(2)
125
+ })
126
+
127
+ // Example: Accessing old and new values in updates
128
+ test('should track old and new values in update effects', async () => {
129
+ const updateResult = await controller.callInteraction('UpdateStyle', {
130
+ user: adminUser,
131
+ payload: {
132
+ id: existingStyle.id,
133
+ label: 'Updated Label',
134
+ status: 'active'
135
+ }
136
+ })
137
+
138
+ const updateEffect = updateResult.effects?.find(e =>
139
+ e.type === 'update' && e.recordName === 'Style'
140
+ )
141
+
142
+ // Access both old and new values
143
+ expect(updateEffect?.oldRecord?.label).toBe('Original Label')
144
+ expect(updateEffect?.record?.label).toBe('Updated Label')
145
+ expect(updateEffect?.oldRecord?.status).toBe('draft')
146
+ expect(updateEffect?.record?.status).toBe('active')
147
+ })
148
+ ```
149
+
150
+ **RecordMutationEvent Structure:**
151
+ ```typescript
152
+ type RecordMutationEvent = {
153
+ recordName: string // Entity/Relation name (e.g., 'Style', 'User')
154
+ type: 'create' | 'update' | 'delete'
155
+ keys?: string[] // Updated field names (for updates)
156
+ record?: { // New/current record data
157
+ id: string
158
+ [key: string]: any
159
+ }
160
+ oldRecord?: { // Previous record data (for updates)
161
+ id: string
162
+ [key: string]: any
163
+ }
164
+ }
165
+ ```
166
+
167
+ **When to Use Effects vs Storage Queries:**
168
+ - **Use `result.effects`** when you need immediate access to the created/updated data without an additional database query
169
+ - **Use storage APIs** when you need to verify the final state after all computations have run
170
+ - **Effects are useful for**: Getting auto-generated IDs, tracking all mutations in complex interactions, debugging what changed
171
+ - **Storage queries are better for**: Verifying computed properties, checking related data, confirming final state
172
+
173
+ #### 🔴 IMPORTANT: Use Storage APIs for Verification
174
+ When testing interactions, **directly use storage.find/findOne to verify results**. DO NOT create query interactions just for testing purposes:
175
+
176
+ ```typescript
177
+ // ✅ CORRECT: Use storage APIs to verify interaction results
178
+ test('should create and update style', async () => {
179
+ // Execute business logic through interaction
180
+ const createResult = await controller.callInteraction('CreateStyle', {
181
+ user: adminUser,
182
+ payload: { label: 'Test Style', slug: 'test-style' }
183
+ })
184
+ expect(createResult.error).toBeUndefined()
185
+
186
+ // Directly verify data with storage API
187
+ const style = await system.storage.findOne('Style',
188
+ MatchExp.atom({ key: 'slug', value: ['=', 'test-style'] }),
189
+ undefined,
190
+ ['id', 'label', 'status', 'createdAt']
191
+ )
192
+ expect(style.label).toBe('Test Style')
193
+ expect(style.status).toBe('draft')
194
+ })
195
+
196
+ // ❌ WRONG: Creating query interactions just for testing
197
+ const GetStyleBySlug = Interaction.create({ // Don't create this just for tests!
198
+ name: 'GetStyleBySlug',
199
+ action: Action.create({ name: 'get' }),
200
+ // ...
201
+ })
202
+ ```
203
+
204
+ **Why?**
205
+ - Storage APIs provide direct, efficient access to verify test outcomes
206
+ - Creating query interactions adds unnecessary complexity
207
+ - Tests should verify business logic, not test helper interactions
208
+ - Only create query interactions if they're actual business requirements
209
+
210
+ ```typescript
211
+ // 1. Basic success check
212
+ const result = await controller.callInteraction('CreateStyle', {...})
213
+ if (result.error) {
214
+ console.error('Interaction failed:', result.error)
215
+ return
216
+ }
217
+
218
+ // 2. Getting data from query interactions
219
+ const queryResult = await controller.callInteraction('GetStyles', {
220
+ user: currentUser,
221
+ query: {
222
+ match: MatchExp.atom({ key: 'status', value: ['=', 'active'] }),
223
+ modifier: { limit: 10 }
224
+ }
225
+ })
226
+ expect(queryResult.error).toBeUndefined()
227
+ expect(queryResult.data).toHaveLength(10)
228
+
229
+ // 3. Checking side effects
230
+ const publishResult = await controller.callInteraction('PublishStyle', {...})
231
+ expect(publishResult.error).toBeUndefined()
232
+ expect(publishResult.sideEffects?.emailNotification?.result).toBe('sent')
233
+
234
+ // 4. Activity interactions return activityId
235
+ const activityResult = await controller.callActivityInteraction(
236
+ 'ApprovalWorkflow',
237
+ 'StartApproval',
238
+ undefined,
239
+ {...}
240
+ )
241
+ const activityId = activityResult.context?.activityId
242
+ ```
243
+
244
+
245
+ ### Multiple References Example
246
+ ```typescript
247
+ // For arrays when isCollection: true
248
+ const AssignStyles = Interaction.create({
249
+ payload: Payload.create({
250
+ items: [
251
+ PayloadItem.create({
252
+ name: 'styles',
253
+ base: Style,
254
+ isRef: true,
255
+ isCollection: true
256
+ })
257
+ ]
258
+ })
259
+ })
260
+
261
+ // ✅ CORRECT: Array of objects with id
262
+ await controller.callInteraction('AssignStyles', {
263
+ user: adminUser,
264
+ payload: {
265
+ styles: [
266
+ { id: style1Id },
267
+ { id: style2Id },
268
+ { id: style3Id }
269
+ ]
270
+ }
271
+ })
272
+ ```
273
+
274
+ **Why this matters**: The framework uses the `isRef` flag to determine whether to create a new entity or reference an existing one. When `isRef: true`, it expects an entity reference format.
275
+
276
+ ### Common Usage Patterns
277
+
278
+ ## Error Checking
279
+
280
+ The interaqt framework wraps all exceptions in the return value, so you NEVER need try-catch blocks:
281
+
282
+ ### Error Types
283
+
284
+ ```typescript
285
+ // 1. Permission errors
286
+ const result = await controller.callInteraction('DeleteStyle', {
287
+ user: viewerUser, // viewer role cannot delete
288
+ payload: { id: style.id }
289
+ })
290
+ expect(result.error).toBeDefined()
291
+ expect((result.error as any).type).toBe('check user failed')
292
+
293
+ // 2. Validation errors (payload attributive checks)
294
+ const result = await controller.callInteraction('PublishStyle', {
295
+ user: adminUser,
296
+ payload: {
297
+ id: offlineStyle.id // Cannot publish offline styles
298
+ }
299
+ })
300
+ expect(result.error).toBeDefined()
301
+ expect((result.error as any).type).toBe('id not match attributive')
302
+
303
+ // 3. Missing required fields
304
+ const result = await controller.callInteraction('CreateStyle', {
305
+ user: adminUser,
306
+ payload: {
307
+ // Missing required 'label' field
308
+ slug: 'test-style'
309
+ }
310
+ })
311
+ expect(result.error).toBeDefined()
312
+ expect((result.error as any).type).toBe('payload label missing')
313
+
314
+ // 4. Business rule violations (condition checks)
315
+ const result = await controller.callInteraction('CreateStyle', {
316
+ user: adminUser,
317
+ payload: {
318
+ label: 'Duplicate',
319
+ slug: existingSlug // Slug must be unique
320
+ }
321
+ })
322
+ expect(result.error).toBeDefined()
323
+ expect((result.error as any).type).toBe('condition check failed')
324
+ ```
325
+
326
+ ### Error Handling Best Practices
327
+
328
+ ```typescript
329
+ test('should handle all error cases', async () => {
330
+ const result = await controller.callInteraction('UpdateStyle', {...})
331
+
332
+ // Always check error first
333
+ if (result.error) {
334
+ // For tests, use expect to verify expected errors
335
+ expect(result.error).toBeDefined()
336
+ expect((result.error as any).type).toBe('expected error type')
337
+ return
338
+ }
339
+
340
+ // Only access other properties after confirming no error
341
+ expect(result.effects).toHaveLength(1)
342
+ expect(result.sideEffects?.audit?.result).toBeTruthy()
343
+ })
344
+ ```
345
+
346
+ ### Debugging Tips
347
+
348
+ When an interaction fails unexpectedly, use `console.log` to inspect the full error object:
349
+
350
+ ```typescript
351
+ const result = await controller.callInteraction('CreateStyle', {...})
352
+ if (result.error) {
353
+ // Print full error details for debugging
354
+ console.log('Interaction error:', result.error)
355
+
356
+ // The error object often contains helpful details:
357
+ // - type: Error type (e.g., 'check user failed', 'payload validation failed')
358
+ // - message: Detailed error message
359
+ // - context: Additional context about what failed
360
+ }
361
+ ```
362
+
363
+ This is especially useful when:
364
+ - Writing new tests and encountering unexpected failures
365
+ - Debugging permission/condition check failures
366
+ - Understanding payload validation errors
367
+
368
+ The interaqt framework implements its own Error subclasses with a nested structure. Errors are wrapped layer by layer, with each layer adding context about where and why the error occurred.
369
+
370
+ ## 🔴 CRITICAL: Timestamp Handling - Always Use Seconds!
371
+ **The database does NOT support millisecond precision for timestamps**. You MUST convert to seconds:
372
+
373
+ ```typescript
374
+ // ❌ WRONG: Using milliseconds directly in test data
375
+ const testData = await system.storage.create('Post', {
376
+ title: 'Test',
377
+ createdAt: Date.now() // ERROR! Database doesn't support milliseconds!
378
+ })
379
+
380
+ // ✅ CORRECT: Convert to seconds for all timestamps
381
+ const testData = await system.storage.create('Post', {
382
+ title: 'Test',
383
+ createdAt: Math.floor(Date.now()/1000) // Correct! Unix timestamp in seconds
384
+ })
385
+
386
+ // ✅ CORRECT: When verifying timestamps in tests
387
+ test('should set correct timestamp', async () => {
388
+ const beforeTime = Math.floor(Date.now()/1000)
389
+
390
+ const result = await controller.callInteraction('CreatePost', {...})
391
+
392
+ const afterTime = Math.floor(Date.now()/1000)
393
+ const post = await system.storage.findOne('Post', ...)
394
+
395
+ expect(post.createdAt).toBeGreaterThanOrEqual(beforeTime)
396
+ expect(post.createdAt).toBeLessThanOrEqual(afterTime)
397
+ })
398
+ ```
399
+
400
+ **Remember**: Always use `Math.floor(Date.now()/1000)` for timestamps in:
401
+ - Test data setup
402
+ - Timestamp verifications
403
+ - Mock data creation
404
+ - Any timestamp-related assertions
405
+
406
+ ## 🔴 CRITICAL: User Authentication Handling
407
+ **interaqt does NOT handle user authentication**. This is a fundamental principle:
408
+ - The framework assumes user identity has already been authenticated through external means (JWT, Session, OAuth, etc.)
409
+ - **DO NOT** create user registration, login, logout interactions
410
+ - **DO NOT** implement authentication logic within the interaqt system
411
+ - In tests, directly create user objects with required properties (id, role, etc.)
412
+ - When calling interactions, pass pre-authenticated user objects
413
+
414
+ **⚠️ IMPORTANT: You MUST Still Define User Entity**
415
+ Even though interaqt doesn't handle authentication, you still need to:
416
+ 1. **Define a User entity** in your application with necessary properties
417
+ 2. **Create test users directly in storage** for testing purposes
418
+ 3. **Pass these user objects** when calling interactions
419
+
420
+ Example of User entity definition and test usage:
421
+ ```typescript
422
+ // ✅ CORRECT: Define User entity (in entities/User.ts)
423
+ export const User = Entity.create({
424
+ name: 'User',
425
+ properties: [
426
+ Property.create({ name: 'name', type: 'string' }),
427
+ Property.create({ name: 'email', type: 'string' }),
428
+ Property.create({ name: 'role', type: 'string' }),
429
+ // Add other properties your application needs
430
+ // But NO password or authentication-related fields
431
+ ]
432
+ })
433
+
434
+ // ✅ CORRECT: Create test users directly in test setup
435
+ const adminUser = await system.storage.create('User', {
436
+ id: 'admin-123',
437
+ name: 'Admin User',
438
+ role: 'admin',
439
+ email: 'admin@test.com'
440
+ })
441
+
442
+ // ✅ CORRECT: Use pre-authenticated user in interactions
443
+ await controller.callInteraction('CreatePost', {
444
+ user: adminUser, // Already authenticated user
445
+ payload: { ... }
446
+ })
447
+
448
+ // ❌ WRONG: Don't create authentication interactions
449
+ const LoginInteraction = Interaction.create({ // DON'T DO THIS
450
+ name: 'Login',
451
+ // ...
452
+ })
453
+ ```
454
+
455
+ ### User-related Interaction Guidelines
456
+ - **DO NOT generate** CreateUser/DeleteUser interactions - user creation/deletion is handled by external user systems
457
+ - **DO generate** UpdateUserProfile, UpdateUserSettings etc. if explicitly required by business requirements
458
+ - User entity exists for reference and relation purposes, not for authentication management
459
+
460
+ ## 🔴 CRITICAL: Always Specify attributeQuery
461
+
462
+ When using `findOne` or `find`, you MUST specify which fields to retrieve:
463
+
464
+ ```typescript
465
+ // ❌ WRONG: Only returns { id: '...' }
466
+ const user = await system.storage.findOne('User',
467
+ MatchExp.atom({ key: 'email', value: ['=', 'test@example.com'] })
468
+ )
469
+ console.log(user.name) // undefined!
470
+
471
+ // ❌ WRONG: Don't use '*' - it's not supported
472
+ const user = await system.storage.findOne('User',
473
+ MatchExp.atom({ key: 'email', value: ['=', 'test@example.com'] }),
474
+ undefined,
475
+ ['*'] // WRONG! Should not use star selector in tests.
476
+ )
477
+
478
+ // ✅ CORRECT: Explicitly list all needed fields
479
+ const user = await system.storage.findOne('User',
480
+ MatchExp.atom({ key: 'email', value: ['=', 'test@example.com'] }),
481
+ undefined, // modifier
482
+ ['id', 'name', 'email', 'role', 'status'] // attributeQuery
483
+ )
484
+ console.log(user.name) // 'Test User' ✓
485
+ ```
486
+
487
+ ### Querying Relations with attributeQuery
488
+
489
+ When querying entities with relations, you can specify related entity fields using nested arrays:
490
+
491
+ ```typescript
492
+ // ✅ CORRECT: Query style with author information
493
+ const style = await system.storage.findOne('Style',
494
+ MatchExp.atom({ key: 'slug', value: ['=', 'modern-style'] }),
495
+ undefined,
496
+ [
497
+ 'id',
498
+ 'label',
499
+ 'status',
500
+ ['author', {
501
+ attributeQuery: ['id', 'name', 'email', 'role']
502
+ }]
503
+ ]
504
+ )
505
+ console.log(style.author.name) // 'John Doe' ✓
506
+
507
+ // ✅ CORRECT: Query with multiple relations
508
+ const post = await system.storage.findOne('Post',
509
+ MatchExp.atom({ key: 'id', value: ['=', postId] }),
510
+ undefined,
511
+ [
512
+ 'id',
513
+ 'title',
514
+ 'content',
515
+ 'publishedAt',
516
+ ['author', {
517
+ attributeQuery: ['id', 'name', 'avatar']
518
+ }],
519
+ ['category', {
520
+ attributeQuery: ['id', 'name', 'slug']
521
+ }]
522
+ ]
523
+ )
524
+
525
+ // ❌ WRONG: Don't use '*' for relations either
526
+ const style = await system.storage.findOne('Style',
527
+ MatchExp.atom({ key: 'id', value: ['=', styleId] }),
528
+ undefined,
529
+ [
530
+ '*', // WRONG!
531
+ ['author', { attributeQuery: ['*'] }] // WRONG!
532
+ ]
533
+ )
534
+ ```
535
+
536
+ ### Querying Relations for Related Entities
537
+
538
+ When querying relations themselves (not entities with relations), the `source` and `target` properties should be treated like related entities in your queries:
539
+
540
+ **🔴 CRITICAL: source/target in Relations are Related Entities**
541
+
542
+ ```typescript
543
+ // ✅ CORRECT: Use relation instance name and query by source entity's properties
544
+ const userPostRelations = await system.storage.find(
545
+ UserPostRelation.name, // Use relation instance's name property
546
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] }), // Use dot notation for source
547
+ undefined,
548
+ [
549
+ 'id',
550
+ 'createdAt', // Relation's own property
551
+ ['source', { attributeQuery: ['id', 'name', 'email'] }], // Nested query for source entity
552
+ ['target', { attributeQuery: ['id', 'title', 'status'] }] // Nested query for target entity
553
+ ]
554
+ )
555
+
556
+ // ✅ CORRECT: Use relation instance name and query by target entity's properties
557
+ const postAuthorRelation = await system.storage.findOneRelationByName(
558
+ PostAuthorRelation.name, // Use relation instance's name property
559
+ MatchExp.atom({ key: 'target.status', value: ['=', 'published'] }), // Query by target's field
560
+ undefined,
561
+ [
562
+ 'id',
563
+ ['source', { attributeQuery: ['id', 'title'] }],
564
+ ['target', { attributeQuery: ['id', 'name', 'role'] }]
565
+ ]
566
+ )
567
+
568
+ // ✅ CORRECT: Complex query with both source and target conditions
569
+ const activeUserPublishedPostRelations = await system.storage.findRelationByName(
570
+ UserPostRelation.name, // Use relation instance's name property
571
+ MatchExp.atom({ key: 'source.status', value: ['=', 'active'] })
572
+ .and({ key: 'target.publishedAt', value: ['not', null] }),
573
+ { limit: 10 },
574
+ [
575
+ 'id',
576
+ 'priority', // Relation property
577
+ ['source', { attributeQuery: ['id', 'name'] }],
578
+ ['target', { attributeQuery: ['id', 'title', 'publishedAt'] }]
579
+ ]
580
+ )
581
+
582
+ // ❌ WRONG: Don't hardcode relation names or compare source/target directly
583
+ const relations = await system.storage.findRelationByName(
584
+ 'UserPostRelation', // WRONG! Don't hardcode, use UserPostRelation.name
585
+ MatchExp.atom({ key: 'source', value: ['=', userId] }), // WRONG! Can't compare entity object
586
+ undefined,
587
+ ['id', 'source', 'target'] // WRONG! Missing nested attributeQuery
588
+ )
589
+
590
+ // ❌ WRONG: Don't hardcode names and don't forget nested attributeQuery for source/target
591
+ const relations = await system.storage.findRelationByName(
592
+ 'UserPostRelation', // WRONG! Use UserPostRelation.name instead
593
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] }),
594
+ undefined,
595
+ ['id', 'source', 'target'] // WRONG! This won't fetch entity data
596
+ )
597
+ ```
598
+
599
+ **Key Points:**
600
+ 1. **In matchExpression**: Use dot notation to access source/target properties (e.g., `source.id`, `target.status`)
601
+ 2. **In attributeQuery**: Use nested array format to fetch source/target data (e.g., `['source', { attributeQuery: [...] }]`)
602
+ 3. **source and target are entities**: They follow the same query rules as any related entity
603
+ 4. **Relation properties**: Can be queried directly without nesting (e.g., `'createdAt'`, `'priority'`)
604
+
605
+ ### Important Notes
606
+ - **Always explicitly list fields**: This ensures predictable results.
607
+ - **Only requested fields are returned**: Any field not in attributeQuery will be undefined
608
+
609
+ ## 🔴 CRITICAL: Accessing Relation Names
610
+
611
+ When you need to query relations in tests, **always use the relation instance's `.name` property** to get the automatically generated name:
612
+
613
+ ```typescript
614
+ // ❌ WRONG: Don't assume or hardcode relation names
615
+ const relation = await system.storage.findOneRelationByName(
616
+ 'User_styles_Style', // WRONG! Don't guess the name format
617
+ MatchExp.atom({ key: 'source.id', value: ['=', user.id] })
618
+ )
619
+
620
+ // ❌ WRONG: Don't construct relation names manually
621
+ const relationName = `${Style.name}_author_${User.name}` // WRONG!
622
+
623
+ // ✅ CORRECT: Use the relation instance's name property
624
+ import { UserStyleRelation } from '../backend/relations'
625
+
626
+ const relation = await system.storage.findOneRelationByName(
627
+ UserStyleRelation.name, // CORRECT! Use the actual relation's name
628
+ MatchExp.atom({ key: 'source.id', value: ['=', user.id] }),
629
+ undefined,
630
+ ['id', 'source', 'target']
631
+ )
632
+
633
+ // ✅ CORRECT: For multiple relations, use their respective names
634
+ const styleAuthorRelation = await system.storage.findRelationByName(
635
+ StyleAuthorRelation.name,
636
+ MatchExp.atom({ key: 'target.id', value: ['=', userId] })
637
+ )
638
+
639
+ const userFavoriteRelation = await system.storage.findRelationByName(
640
+ UserFavoriteStyleRelation.name,
641
+ MatchExp.atom({ key: 'source.id', value: ['=', userId] })
642
+ )
643
+ ```
644
+
645
+ **Why this matters**:
646
+ - Relation names are automatically generated by the framework based on entity names and properties
647
+ - The format may change or vary based on relation configuration
648
+ - Using the `.name` property ensures your tests remain correct even if the naming convention changes
649
+ - It makes tests more maintainable and less brittle
650
+
651
+ ## Best Practices
652
+
653
+ ### DO
654
+ - Test all business scenarios through interactions
655
+ - Use descriptive test names following test case IDs
656
+ - Verify both success and error paths
657
+ - Check computed values update correctly
658
+ - Test edge cases and boundary conditions
659
+
660
+ ### DON'T
661
+ - Don't use storage APIs for business logic testing
662
+ - Don't test framework mechanics (entity/relation creation)
663
+ - Don't use try-catch for error handling
664
+ - Don't forget attributeQuery in find operations
665
+ - Don't test implementation details
666
+
667
+ ## Validation Checklist
668
+ - [ ] All tests use callInteraction for business logic
669
+ - [ ] Storage APIs only used for test setup
670
+ - [ ] All findOne/find calls include attributeQuery
671
+ - [ ] Error checking uses result.error pattern
672
+ - [ ] Test covers success and failure scenarios
673
+ - [ ] Computed values verified after interactions
674
+ - [ ] No try-catch blocks for error handling