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.
- package/agent/.claude/agents/code-generation-handler.md +2 -0
- package/agent/.claude/agents/computation-generation-handler.md +1 -0
- package/agent/.claude/agents/implement-design-handler.md +4 -13
- package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
- package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
- package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
- package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
- package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
- package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
- package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
- package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
- package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
- package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
- package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
- package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
- package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
- package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
- package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
- package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
- package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
- package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
- package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
- package/agent/agentspace/knowledge/usage/README.md +148 -0
- 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
|