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,1201 @@
1
+ # Testing
2
+
3
+ In the interaqt framework, testing is part of the design philosophy. The reactive programming model makes testing more intuitive and reliable. This chapter will detail testing strategies, patterns, and best practices.
4
+
5
+ ## Testing API Quick Reference
6
+
7
+ ### ⚠️ Common API Mistakes to Avoid
8
+
9
+ Many LLMs generate incorrect API usage. Here's the correct way to use interaqt testing APIs:
10
+
11
+ ```typescript
12
+ // ❌ WRONG: These APIs do NOT exist
13
+ controller.run() // ❌ No such method
14
+ storage.findByProperty('Entity', 'prop') // ❌ No such method
15
+ controller.execute() // ❌ No such method
16
+ controller.dispatch() // ❌ No such method
17
+
18
+ // ✅ CORRECT: Use these APIs instead
19
+ controller.callInteraction('InteractionName', args) // ✅ Call interactions
20
+ storage.findOne('Entity', MatchExp) // ✅ Find single record
21
+ storage.find('Entity', MatchExp) // ✅ Find multiple records
22
+ storage.create('Entity', data) // ✅ Create record
23
+ ```
24
+
25
+ ### Complete Test Template
26
+
27
+ ```typescript
28
+ import { describe, test, expect, beforeEach } from 'vitest'
29
+ import { Controller, MonoSystem, KlassByName, PGLiteDB, MatchExp } from 'interaqt'
30
+ import { entities, relations, interactions, activities } from '../backend'
31
+ // If you need UUID, install and import it:
32
+ // npm install uuid @types/uuid
33
+ // import { v4 as uuid } from 'uuid'
34
+
35
+ describe('Feature Tests', () => {
36
+ let system: MonoSystem
37
+ let controller: Controller
38
+
39
+ beforeEach(async () => {
40
+ // ✅ Correct setup
41
+ system = new MonoSystem(new PGLiteDB())
42
+ system.conceptClass = KlassByName
43
+
44
+ // Note: When creating relations, DO NOT specify name property
45
+ // The framework automatically generates relation names:
46
+ // - User + Post → UserPost
47
+ // - Post + Comment → PostComment
48
+
49
+ controller = new Controller({
50
+ system,
51
+ entities,
52
+ relations, // Relations with auto-generated names
53
+ activities, // Activities
54
+ interactions, // Interactions
55
+ dict: [], // Global dictionaries (NOT computations)
56
+ recordMutationSideEffects: [] // Side effects
57
+ })
58
+
59
+ await controller.setup(true)
60
+ })
61
+
62
+ test('interaction test example', async () => {
63
+ // ✅ CORRECT: Use callInteraction
64
+ const result = await controller.callInteraction('CreateUser', {
65
+ user: { id: 'system', role: 'admin' }, // Must include user object
66
+ payload: {
67
+ username: 'testuser',
68
+ email: 'test@example.com',
69
+ role: 'user'
70
+ }
71
+ })
72
+
73
+ // Check for errors
74
+ expect(result.error).toBeUndefined()
75
+
76
+ // ✅ CORRECT: Use storage.findOne with MatchExp
77
+ const user = await system.storage.findOne(
78
+ 'User',
79
+ MatchExp.atom({ key: 'username', value: ['=', 'testuser'] }),
80
+ undefined,
81
+ ['id', 'username', 'email', 'role', 'status']
82
+ )
83
+
84
+ expect(user).toBeTruthy()
85
+ expect(user.email).toBe('test@example.com')
86
+ })
87
+
88
+ test('finding records examples', async () => {
89
+ // ✅ Find by single field
90
+ const user = await system.storage.findOne(
91
+ 'User',
92
+ MatchExp.atom({ key: 'id', value: ['=', userId] }),
93
+ undefined,
94
+ ['id', 'username', 'email', 'status', 'role']
95
+ )
96
+
97
+ // ✅ Find with multiple conditions
98
+ const activeUsers = await system.storage.find(
99
+ 'User',
100
+ MatchExp.atom({ key: 'status', value: ['=', 'active'] })
101
+ .and({ key: 'role', value: ['=', 'user'] }),
102
+ undefined,
103
+ ['id', 'username', 'email', 'lastLoginDate']
104
+ )
105
+
106
+ // ✅ Find with complex conditions
107
+ const posts = await system.storage.find(
108
+ 'Post',
109
+ MatchExp.atom({ key: 'author.id', value: ['=', userId] })
110
+ .and({ key: 'status', value: ['in', ['published', 'draft']] }),
111
+ undefined,
112
+ ['id', 'title', 'content', 'status', 'createdAt', 'author']
113
+ )
114
+ })
115
+
116
+ test('creating and updating records', async () => {
117
+ // ✅ Create record directly (ONLY for test setup)
118
+ // ⚠️ WARNING: storage.create bypasses ALL validation!
119
+ // NEVER use it to test business logic or validation
120
+ const user = await system.storage.create('User', {
121
+ username: 'testuser',
122
+ email: 'test@example.com',
123
+ role: 'user'
124
+ })
125
+
126
+ // ✅ Update record (also bypasses validation - use only for test setup)
127
+ await system.storage.update(
128
+ 'User',
129
+ MatchExp.atom({ key: 'id', value: ['=', user.id] }),
130
+ { status: 'active' }
131
+ )
132
+
133
+ // ✅ Delete record
134
+ await system.storage.delete(
135
+ 'User',
136
+ MatchExp.atom({ key: 'id', value: ['=', user.id] })
137
+ )
138
+ })
139
+ })
140
+ ```
141
+
142
+ ### Key API Methods
143
+
144
+ #### 1. Controller APIs
145
+
146
+ ```typescript
147
+ // Call an interaction (the ONLY way to execute business logic)
148
+ const result = await controller.callInteraction(interactionName: string, args: {
149
+ user: { id: string, [key: string]: any }, // Required user object
150
+ payload?: { [key: string]: any } // Optional payload
151
+ })
152
+
153
+ // Call activity interaction
154
+ const result = await controller.callActivityInteraction(
155
+ activityName: string,
156
+ interactionName: string,
157
+ activityId: string,
158
+ args: InteractionEventArgs
159
+ )
160
+ ```
161
+
162
+ #### 2. Storage APIs
163
+
164
+ ⚠️ **WARNING: Storage APIs bypass ALL validation and business logic!**
165
+ - Use `storage.create/update/delete` ONLY for test data setup
166
+ - NEVER use them to test validation or business logic
167
+ - ALL business logic tests must use `callInteraction`
168
+
169
+ 🔴 **CRITICAL: Always specify attributeQuery when using find/findOne!**
170
+ - Without `attributeQuery`, only the `id` field is returned
171
+ - This is the most common cause of test failures
172
+ - Always explicitly list all fields you need to verify
173
+
174
+ ```typescript
175
+ // Create a record (ONLY for test setup - bypasses ALL validation!)
176
+ const record = await system.storage.create(entityName: string, data: object)
177
+
178
+ // Find one record (safe for reading data)
179
+ const record = await system.storage.findOne(
180
+ entityName: string,
181
+ matchExp: MatchExp,
182
+ modifier?: Modifier,
183
+ attributeQuery?: AttributeQuery
184
+ )
185
+
186
+ // Find multiple records (safe for reading data)
187
+ const records = await system.storage.find(
188
+ entityName: string,
189
+ matchExp: MatchExp,
190
+ modifier?: Modifier,
191
+ attributeQuery?: AttributeQuery
192
+ )
193
+
194
+ // Update records (ONLY for test setup - bypasses ALL validation!)
195
+ await system.storage.update(
196
+ entityName: string,
197
+ matchExp: MatchExp,
198
+ data: object
199
+ )
200
+
201
+ // Delete records (ONLY for test cleanup - bypasses ALL business logic!)
202
+ await system.storage.delete(
203
+ entityName: string,
204
+ matchExp: MatchExp
205
+ )
206
+ ```
207
+
208
+ #### 3. MatchExp Usage
209
+
210
+ ```typescript
211
+ // Simple equality
212
+ MatchExp.atom({ key: 'field', value: ['=', value] })
213
+
214
+ // Multiple conditions (AND)
215
+ MatchExp.atom({ key: 'status', value: ['=', 'active'] })
216
+ .and({ key: 'role', value: ['=', 'admin'] })
217
+
218
+ // OR conditions
219
+ MatchExp.atom({ key: 'role', value: ['=', 'admin'] })
220
+ .or({ key: 'role', value: ['=', 'moderator'] })
221
+
222
+ // Complex operators
223
+ MatchExp.atom({ key: 'age', value: ['>', 18] })
224
+ MatchExp.atom({ key: 'name', value: ['like', '%john%'] })
225
+ MatchExp.atom({ key: 'status', value: ['in', ['active', 'pending']] })
226
+ MatchExp.atom({ key: 'score', value: ['between', [60, 100]] })
227
+
228
+ // Nested field access
229
+ MatchExp.atom({ key: 'user.profile.city', value: ['=', 'Beijing'] })
230
+ ```
231
+
232
+ ### Error Handling in Tests
233
+
234
+ ```typescript
235
+ test('should handle errors correctly', async () => {
236
+ // ✅ CORRECT: Check error field in result
237
+ const result = await controller.callInteraction('SomeInteraction', {
238
+ user: { id: 'user1' },
239
+ payload: { invalid: 'data' }
240
+ })
241
+
242
+ expect(result.error).toBeDefined()
243
+ expect(result.error.message).toContain('expected error message')
244
+
245
+ // ❌ WRONG: interaqt doesn't throw exceptions
246
+ // try {
247
+ // await controller.callInteraction(...)
248
+ // } catch (e) {
249
+ // // This won't work
250
+ // }
251
+ })
252
+ ```
253
+
254
+ # 12. How to Perform Testing
255
+
256
+ Testing is a crucial component for ensuring the quality of interaqt applications. The framework provides comprehensive testing support, including unit testing, integration testing, and end-to-end testing. This chapter will detail how to write effective tests for reactive applications.
257
+
258
+ ## ⚠️ CRITICAL: interaqt Testing Philosophy
259
+
260
+ **In the interaqt framework, ALL data is derived from interaction events.** This fundamental principle changes how we approach testing:
261
+
262
+ 1. **Focus on Interaction Testing**: Since all Entity and Relation data are created, modified, and deleted through Interactions, comprehensive Interaction testing naturally covers all data operations.
263
+
264
+ 2. **No Separate Entity/Relation Tests**: You should NOT write separate unit tests for Entity CRUD operations or Relation creation/deletion. These are implementation details that are automatically tested when you test the Interactions that use them.
265
+
266
+ 3. **Coverage Through Interactions**: If your test coverage is below 100% after testing all Interactions, it indicates:
267
+ - Missing Interaction definitions in your design
268
+ - Insufficient edge case testing for existing Interactions
269
+ - Unused code that should be removed
270
+
271
+ 4. **Test What Matters**: Test the business logic and user scenarios through Interactions, not the framework mechanics.
272
+
273
+ 5. **Storage APIs are LOW-LEVEL**:
274
+ - `storage.create()`, `storage.update()`, `storage.delete()` bypass ALL validation and business logic
275
+ - Use them ONLY for test data setup (creating prerequisite records)
276
+ - NEVER use them to test validation failures - they will always succeed!
277
+ - ALL business logic testing must go through `callInteraction()`
278
+
279
+ ## 12.1 Testing Reactive Computations
280
+
281
+ ### 12.1.1 Count Computation Testing
282
+
283
+ ```typescript
284
+ // tests/computations/count.spec.ts
285
+ describe('Count Computation', () => {
286
+ test('should update user count automatically', async () => {
287
+ const system = new MonoSystem(new PGLiteDB());
288
+
289
+ const userEntity = Entity.create({
290
+ name: 'User',
291
+ properties: [
292
+ Property.create({ name: 'username', type: 'string' }),
293
+ Property.create({ name: 'isActive', type: 'boolean' })
294
+ ]
295
+ });
296
+
297
+ const totalUsersDict = Dictionary.create({
298
+ name: 'totalUsers',
299
+ type: 'number',
300
+ collection: false,
301
+ defaultValue: () => 0,
302
+ computation: Count.create({
303
+ record: userEntity
304
+ })
305
+ });
306
+
307
+ const activeUsersDict = Dictionary.create({
308
+ name: 'activeUsers',
309
+ type: 'number',
310
+ collection: false,
311
+ defaultValue: () => 0,
312
+ computation: Count.create({
313
+ record: userEntity
314
+ })
315
+ });
316
+
317
+ const controller = new Controller({
318
+
319
+
320
+ system: system,
321
+
322
+
323
+ entities: [userEntity],
324
+
325
+
326
+ relations: [],
327
+
328
+
329
+ activities: [],
330
+
331
+
332
+ interactions: [],
333
+
334
+
335
+ dict: [totalUsersDict, activeUsersDict],
336
+
337
+
338
+ recordMutationSideEffects: []
339
+
340
+
341
+ });
342
+ await controller.setup(true);
343
+
344
+ // Check initial state
345
+ let totalUsers = await system.storage.get('state', 'totalUsers');
346
+ let activeUsers = await system.storage.get('state', 'activeUsers');
347
+ expect(totalUsers).toBe(0);
348
+ expect(activeUsers).toBe(0);
349
+
350
+ // Create active user
351
+ await system.storage.create('User', {
352
+ username: 'alice',
353
+ isActive: true
354
+ });
355
+
356
+ // Verify count update
357
+ totalUsers = await system.storage.get('state', 'totalUsers');
358
+ activeUsers = await system.storage.get('state', 'activeUsers');
359
+ expect(totalUsers).toBe(1);
360
+ expect(activeUsers).toBe(1);
361
+
362
+ // Create inactive user
363
+ await system.storage.create('User', {
364
+ username: 'bob',
365
+ isActive: false
366
+ });
367
+
368
+ // Verify count update
369
+ totalUsers = await system.storage.get('state', 'totalUsers');
370
+ activeUsers = await system.storage.get('state', 'activeUsers');
371
+ expect(totalUsers).toBe(2);
372
+ expect(activeUsers).toBe(1);
373
+ });
374
+ });
375
+ ```
376
+
377
+ ### 12.1.2 Complex Computation Testing
378
+
379
+ ```typescript
380
+ // tests/computations/transform.spec.ts
381
+ describe('Transform Computation', () => {
382
+ test('should calculate user statistics correctly', async () => {
383
+ const system = new MonoSystem(new PGLiteDB());
384
+
385
+ const userEntity = Entity.create({
386
+ name: 'User',
387
+ properties: [
388
+ Property.create({ name: 'username', type: 'string' }),
389
+ Property.create({ name: 'age', type: 'number' }),
390
+ Property.create({ name: 'score', type: 'number' })
391
+ ]
392
+ });
393
+
394
+ const userStatsDict = Dictionary.create({
395
+ name: 'userStats',
396
+ type: 'object',
397
+ collection: false,
398
+ defaultValue: () => ({}),
399
+ computation: Transform.create({
400
+ record: userEntity,
401
+ attributeQuery: ['age', 'score'],
402
+ callback: (users: any[]) => {
403
+ if (users.length === 0) {
404
+ return {
405
+ totalUsers: 0,
406
+ averageAge: 0,
407
+ averageScore: 0,
408
+ maxScore: 0,
409
+ minScore: 0
410
+ };
411
+ }
412
+
413
+ const totalAge = users.reduce((sum, user) => sum + user.age, 0);
414
+ const totalScore = users.reduce((sum, user) => sum + user.score, 0);
415
+ const scores = users.map(user => user.score);
416
+
417
+ return {
418
+ totalUsers: users.length,
419
+ averageAge: totalAge / users.length,
420
+ averageScore: totalScore / users.length,
421
+ maxScore: Math.max(...scores),
422
+ minScore: Math.min(...scores)
423
+ };
424
+ }
425
+ })
426
+ });
427
+
428
+ const controller = new Controller({
429
+
430
+
431
+ system: system,
432
+
433
+
434
+ entities: [userEntity],
435
+
436
+
437
+ relations: [],
438
+
439
+
440
+ activities: [],
441
+
442
+
443
+ interactions: [],
444
+
445
+
446
+ dict: [userStatsDict],
447
+
448
+
449
+ recordMutationSideEffects: []
450
+
451
+
452
+ });
453
+ await controller.setup(true);
454
+
455
+ // Create test users
456
+ await system.storage.create('User', {
457
+ username: 'alice',
458
+ age: 25,
459
+ score: 85
460
+ });
461
+
462
+ await system.storage.create('User', {
463
+ username: 'bob',
464
+ age: 30,
465
+ score: 92
466
+ });
467
+
468
+ await system.storage.create('User', {
469
+ username: 'charlie',
470
+ age: 35,
471
+ score: 78
472
+ });
473
+
474
+ // Verify statistical calculation
475
+ const stats = await system.storage.get('state', 'userStats');
476
+ expect(stats.totalUsers).toBe(3);
477
+ expect(stats.averageAge).toBe(30);
478
+ expect(stats.averageScore).toBeCloseTo(85);
479
+ expect(stats.maxScore).toBe(92);
480
+ expect(stats.minScore).toBe(78);
481
+ });
482
+ });
483
+ ```
484
+
485
+ ## 12.2 Testing Interactions and Activities
486
+
487
+ ### 12.2.1 Interaction Testing
488
+
489
+ ```typescript
490
+ // tests/interactions/userActions.spec.ts
491
+ describe('User Interactions', () => {
492
+ test('should handle user registration interaction', async () => {
493
+ const system = new MonoSystem(new PGLiteDB());
494
+
495
+ const userEntity = Entity.create({
496
+ name: 'User',
497
+ properties: [
498
+ Property.create({ name: 'username', type: 'string' }),
499
+ Property.create({ name: 'email', type: 'string' }),
500
+ Property.create({ name: 'isActive', type: 'boolean', defaultValue: () => true })
501
+ ]
502
+ });
503
+
504
+ const registerInteraction = Interaction.create({
505
+ name: 'register',
506
+ action: Action.create({ name: 'register' }),
507
+ payload: Payload.create({
508
+ items: [
509
+ PayloadItem.create({
510
+ name: 'userData',
511
+ base: userEntity
512
+ })
513
+ ]
514
+ })
515
+ });
516
+
517
+ const controller = new Controller({
518
+ system,
519
+ entities: [userEntity],
520
+ relations: [],
521
+ activities: [], // activities
522
+ interactions: [registerInteraction], // interactions
523
+ dict: []
524
+ });
525
+ await controller.setup(true);
526
+
527
+ // Execute registration interaction
528
+ const result = await controller.callInteraction(registerInteraction.name, {
529
+ user: { id: 'test-user' }, // Add user object
530
+ payload: {
531
+ userData: {
532
+ username: 'newuser',
533
+ email: 'newuser@example.com'
534
+ }
535
+ }
536
+ });
537
+
538
+ // Verify interaction result
539
+ expect(result).toBeTruthy();
540
+
541
+ // Verify user creation
542
+ const user = await system.storage.findOne(
543
+ 'User',
544
+ MatchExp.atom({ key: 'username', value: ['=', 'newuser'] }),
545
+ undefined,
546
+ ['id', 'username', 'email', 'isActive'] // Specify fields to verify
547
+ );
548
+
549
+ expect(user).toBeTruthy();
550
+ expect(user.username).toBe('newuser');
551
+ expect(user.email).toBe('newuser@example.com');
552
+ expect(user.isActive).toBe(true);
553
+ });
554
+ });
555
+ ```
556
+
557
+ ### 12.2.2 Activity Testing
558
+
559
+ ```typescript
560
+ // tests/activities/approvalProcess.spec.ts
561
+ describe('Approval Process Activity', () => {
562
+ test('should handle complete approval workflow', async () => {
563
+ const system = new MonoSystem(new PGLiteDB());
564
+
565
+ // Create request entity
566
+ const requestEntity = Entity.create({
567
+ name: 'Request',
568
+ properties: [
569
+ Property.create({ name: 'title', type: 'string' }),
570
+ Property.create({ name: 'status', type: 'string' }),
571
+ Property.create({ name: 'submitterId', type: 'string' })
572
+ ]
573
+ });
574
+
575
+ // Create activity states
576
+ const submittedState = StateNode.create({ name: 'submitted' });
577
+ const reviewingState = StateNode.create({ name: 'reviewing' });
578
+ const approvedState = StateNode.create({ name: 'approved' });
579
+ const rejectedState = StateNode.create({ name: 'rejected' });
580
+
581
+ // Create interactions
582
+ const submitInteraction = Interaction.create({
583
+ name: 'submit',
584
+ action: Action.create({ name: 'submit' })
585
+ });
586
+
587
+ const approveInteraction = Interaction.create({
588
+ name: 'approve',
589
+ action: Action.create({ name: 'approve' })
590
+ });
591
+
592
+ const rejectInteraction = Interaction.create({
593
+ name: 'reject',
594
+ action: Action.create({ name: 'reject' })
595
+ });
596
+
597
+ // Create state transfers
598
+ const submitTransfer = StateTransfer.create({
599
+ trigger: submitInteraction,
600
+ current: submittedState,
601
+ next: reviewingState
602
+ });
603
+
604
+ const approveTransfer = StateTransfer.create({
605
+ trigger: approveInteraction,
606
+ current: reviewingState,
607
+ next: approvedState
608
+ });
609
+
610
+ const rejectTransfer = StateTransfer.create({
611
+ trigger: rejectInteraction,
612
+ current: reviewingState,
613
+ next: rejectedState
614
+ });
615
+
616
+ // Create activity
617
+ const approvalActivity = Activity.create({
618
+ name: 'ApprovalProcess',
619
+ states: [submittedState, reviewingState, approvedState, rejectedState],
620
+ transfers: [submitTransfer, approveTransfer, rejectTransfer],
621
+ defaultState: submittedState
622
+ });
623
+
624
+ const controller = new Controller({
625
+ system,
626
+ entities: [requestEntity],
627
+ relations: [],
628
+ activities: [approvalActivity], // activities
629
+ interactions: [submitInteraction, approveInteraction, rejectInteraction], // interactions
630
+ dict: []
631
+ });
632
+ await controller.setup(true);
633
+
634
+ // Create request
635
+ const request = await system.storage.create('Request', {
636
+ title: 'Test Request',
637
+ status: 'submitted',
638
+ submitterId: 'user123'
639
+ });
640
+
641
+ // Execute submit interaction
642
+ await controller.callInteraction(submitInteraction.name, {
643
+ user: { id: 'user123' }, // Add user object
644
+ payload: {
645
+ requestId: request.id
646
+ }
647
+ });
648
+
649
+ // Verify state transition
650
+ let updatedRequest = await system.storage.findOne(
651
+ 'Request',
652
+ MatchExp.atom({ key: 'id', value: ['=', request.id] }),
653
+ undefined,
654
+ ['id', 'title', 'status', 'submitterId'] // Specify fields
655
+ );
656
+ expect(updatedRequest.status).toBe('reviewing');
657
+
658
+ // Execute approve interaction
659
+ await controller.callInteraction(approveInteraction.name, {
660
+ user: { id: 'admin-user' }, // Add user object
661
+ payload: {
662
+ requestId: request.id
663
+ }
664
+ });
665
+
666
+ // Verify final state
667
+ updatedRequest = await system.storage.findOne(
668
+ 'Request',
669
+ MatchExp.atom({ key: 'id', value: ['=', request.id] }),
670
+ undefined,
671
+ ['id', 'title', 'status', 'submitterId'] // Specify fields
672
+ );
673
+ expect(updatedRequest.status).toBe('approved');
674
+ });
675
+ });
676
+ ```
677
+
678
+ ## 12.3 Testing Permissions and Attributives
679
+
680
+ > **Important: Correct Error Handling Approach**
681
+ >
682
+ > The interaqt framework automatically catches all errors (including Attributive validation failures, insufficient permissions, etc.) and returns error information through the `error` field in the return value. The framework **does not throw uncaught exceptions**.
683
+ >
684
+ > Therefore, when writing tests:
685
+ > - ✅ **Correct approach**: Check the `error` field in the return value
686
+ > - ❌ **Wrong approach**: Use try-catch to catch exceptions
687
+ >
688
+ > ```javascript
689
+ > // ✅ Correct testing approach
690
+ > const result = await controller.callInteraction('SomeInteraction', {...});
691
+ > expect(result.error).toBeTruthy();
692
+ > expect(result.error.message).toContain('permission denied');
693
+ >
694
+ > // ❌ Wrong testing approach
695
+ > try {
696
+ > await controller.callInteraction('SomeInteraction', {...});
697
+ > fail('Should have thrown error');
698
+ > } catch (e) {
699
+ > // This code will never execute as the framework doesn't throw exceptions
700
+ > }
701
+ > ```
702
+
703
+ ### 12.3.1 Permission Testing Basics
704
+
705
+ Permission testing is an important component of interaqt application testing, requiring verification of access permissions for different users in different scenarios:
706
+
707
+ ```typescript
708
+ import { describe, test, expect, beforeEach } from 'vitest';
709
+ import { Controller, MonoSystem, KlassByName, PGLiteDB } from 'interaqt';
710
+
711
+ describe('Permission Testing', () => {
712
+ let system: MonoSystem;
713
+ let controller: Controller;
714
+
715
+ beforeEach(async () => {
716
+ system = new MonoSystem(new PGLiteDB());
717
+ system.conceptClass = KlassByName;
718
+
719
+ controller = new Controller({
720
+ system,
721
+ entities,
722
+ relations,
723
+ activities,
724
+ interactions,
725
+ dict: []
726
+ });
727
+
728
+ await controller.setup(true);
729
+ });
730
+
731
+
732
+ });
733
+ ```
734
+
735
+ ### 12.3.2 Basic Role Permission Testing
736
+
737
+ ```typescript
738
+ describe('Basic Role Permission Testing', () => {
739
+ test('admin permission test', async () => {
740
+ // Create admin user
741
+ const admin = await system.storage.create('User', {
742
+ name: 'Admin User',
743
+ role: 'admin',
744
+ email: 'admin@example.com'
745
+ });
746
+
747
+ // Test that admin can perform privileged operations
748
+ const result = await controller.callInteraction('CreateDormitory', {
749
+ user: admin,
750
+ payload: {
751
+ name: 'Admin Created Dormitory',
752
+ building: 'Admin Building',
753
+ roomNumber: '001',
754
+ capacity: 4,
755
+ description: 'Test dormitory'
756
+ }
757
+ });
758
+
759
+ expect(result.error).toBeUndefined();
760
+
761
+ // Verify dormitory was actually created
762
+ const { MatchExp } = controller.globals;
763
+ const dormitory = await system.storage.findOne('Dormitory',
764
+ MatchExp.atom({ key: 'name', value: ['=', 'Admin Created Dormitory'] }),
765
+ undefined,
766
+ ['id', 'name', 'building', 'roomNumber', 'capacity', 'description'] // Specify fields
767
+ );
768
+ expect(dormitory).toBeTruthy();
769
+ });
770
+
771
+ test('regular user permission restriction test', async () => {
772
+ const student = await system.storage.create('User', {
773
+ name: 'Regular Student',
774
+ role: 'student',
775
+ email: 'student@example.com'
776
+ });
777
+
778
+ // Regular student should not be able to create dormitory
779
+ const result = await controller.callInteraction('CreateDormitory', {
780
+ user: student,
781
+ payload: {
782
+ name: 'Student Attempted Dormitory',
783
+ building: 'Student Building',
784
+ roomNumber: '002',
785
+ capacity: 4,
786
+ description: 'Unauthorized test'
787
+ }
788
+ });
789
+
790
+ expect(result.error).toBeTruthy();
791
+ expect(result.error.message).toContain('Admin'); // Permission error should mention requirement
792
+ });
793
+ });
794
+ ```
795
+
796
+ ### 12.3.3 Complex Permission Logic Testing
797
+
798
+ ```typescript
799
+ describe('Complex Permission Logic Testing', () => {
800
+ test('dormitory leader permission test', async () => {
801
+ // Setup test scenario
802
+ const leader = await system.storage.create('User', {
803
+ name: 'Dormitory Leader',
804
+ role: 'student',
805
+ email: 'leader@example.com'
806
+ });
807
+
808
+ const member = await system.storage.create('User', {
809
+ name: 'Regular Member',
810
+ role: 'student',
811
+ email: 'member@example.com'
812
+ });
813
+
814
+ // Create dormitory and member relations
815
+ const dormitory = await system.storage.create('Dormitory', {
816
+ name: 'Permission Test Dormitory',
817
+ building: 'Permission Test Building',
818
+ roomNumber: '999',
819
+ capacity: 4
820
+ });
821
+
822
+ const leaderMember = await system.storage.create('DormitoryMember', {
823
+ user: leader,
824
+ dormitory: dormitory,
825
+ role: 'leader',
826
+ status: 'active',
827
+ bedNumber: 1,
828
+ joinedAt: new Date().toISOString()
829
+ });
830
+
831
+ const normalMember = await system.storage.create('DormitoryMember', {
832
+ user: member,
833
+ dormitory: dormitory,
834
+ role: 'member',
835
+ status: 'active',
836
+ bedNumber: 2,
837
+ joinedAt: new Date().toISOString()
838
+ });
839
+
840
+ // Test that leader can record scores
841
+ const leaderResult = await controller.callInteraction('RecordScore', {
842
+ user: leader,
843
+ payload: {
844
+ memberId: normalMember,
845
+ points: 10,
846
+ reason: 'Cleaning duties',
847
+ category: 'hygiene'
848
+ }
849
+ });
850
+ expect(leaderResult.error).toBeUndefined();
851
+
852
+ // Test that regular member cannot record scores
853
+ const memberResult = await controller.callInteraction('RecordScore', {
854
+ user: member,
855
+ payload: {
856
+ memberId: leaderMember,
857
+ points: 10,
858
+ reason: 'Attempted score recording',
859
+ category: 'hygiene'
860
+ }
861
+ });
862
+ expect(memberResult.error).toBeTruthy();
863
+ });
864
+ });
865
+ ```
866
+
867
+ ### 12.3.4 Payload-level Permission Testing
868
+
869
+ ```typescript
870
+ describe('Payload-level Permission Testing', () => {
871
+ test('can only operate on own dormitory data', async () => {
872
+ // Create two dormitory leaders
873
+ const leader1 = await system.storage.create('User', {
874
+ name: 'Leader 1',
875
+ role: 'student',
876
+ email: 'leader1@example.com'
877
+ });
878
+
879
+ const leader2 = await system.storage.create('User', {
880
+ name: 'Leader 2',
881
+ role: 'student',
882
+ email: 'leader2@example.com'
883
+ });
884
+
885
+ // Create two dormitories
886
+ const dormitory1 = await system.storage.create('Dormitory', {
887
+ name: 'Dormitory 1',
888
+ building: 'Test Building',
889
+ roomNumber: '201',
890
+ capacity: 4
891
+ });
892
+
893
+ const dormitory2 = await system.storage.create('Dormitory', {
894
+ name: 'Dormitory 2',
895
+ building: 'Test Building',
896
+ roomNumber: '202',
897
+ capacity: 4
898
+ });
899
+
900
+ // Establish member relations
901
+ const member1 = await system.storage.create('DormitoryMember', {
902
+ user: leader1,
903
+ dormitory: dormitory1,
904
+ role: 'leader',
905
+ status: 'active',
906
+ bedNumber: 1,
907
+ joinedAt: new Date().toISOString()
908
+ });
909
+
910
+ const member2 = await system.storage.create('DormitoryMember', {
911
+ user: leader2,
912
+ dormitory: dormitory2,
913
+ role: 'leader',
914
+ status: 'active',
915
+ bedNumber: 1,
916
+ joinedAt: new Date().toISOString()
917
+ });
918
+
919
+ // Leader 1 should be able to operate on own dormitory members
920
+ const validResult = await controller.callInteraction('RecordScore', {
921
+ user: leader1,
922
+ payload: {
923
+ memberId: member1,
924
+ points: 10,
925
+ reason: 'Cleanliness',
926
+ category: 'hygiene'
927
+ }
928
+ });
929
+ expect(validResult.error).toBeUndefined();
930
+
931
+ // Leader 1 should not be able to operate on other dormitory members
932
+ const invalidResult = await controller.callInteraction('RecordScore', {
933
+ user: leader1,
934
+ payload: {
935
+ memberId: member2,
936
+ points: 10,
937
+ reason: 'Cross-dormitory operation attempt',
938
+ category: 'hygiene'
939
+ }
940
+ });
941
+ expect(invalidResult.error).toBeTruthy();
942
+ });
943
+ });
944
+ ```
945
+
946
+ ### 12.3.5 Permission Edge Case Testing
947
+
948
+ ```typescript
949
+ describe('Permission Edge Case Testing', () => {
950
+ test('application restriction when dormitory is full', async () => {
951
+ const student = await system.storage.create('User', {
952
+ name: 'Applicant Student',
953
+ role: 'student',
954
+ email: 'applicant@example.com'
955
+ });
956
+
957
+ // Create full dormitory
958
+ const fullDormitory = await system.storage.create('Dormitory', {
959
+ name: 'Full Dormitory',
960
+ building: 'Test Building',
961
+ roomNumber: '301',
962
+ capacity: 2
963
+ });
964
+
965
+ // Add members until full
966
+ for (let i = 0; i < 2; i++) {
967
+ const user = await system.storage.create('User', {
968
+ name: `Member ${i + 1}`,
969
+ role: 'student',
970
+ email: `member${i + 1}@example.com`
971
+ });
972
+
973
+ await system.storage.create('DormitoryMember', {
974
+ user: user,
975
+ dormitory: fullDormitory,
976
+ role: 'member',
977
+ status: 'active',
978
+ bedNumber: i + 1,
979
+ joinedAt: new Date().toISOString()
980
+ });
981
+ }
982
+
983
+ // Try to apply to full dormitory
984
+ const result = await controller.callInteraction('ApplyForDormitory', {
985
+ user: student,
986
+ payload: {
987
+ dormitoryId: fullDormitory,
988
+ message: 'Hope to join this dormitory'
989
+ }
990
+ });
991
+
992
+ expect(result.error).toBeTruthy();
993
+ expect(result.error.message).toContain('DormitoryNotFull');
994
+ });
995
+
996
+ test('duplicate application restriction', async () => {
997
+ const student = await system.storage.create('User', {
998
+ name: 'Student with Dormitory',
999
+ role: 'student',
1000
+ email: 'hasdorm@example.com'
1001
+ });
1002
+
1003
+ // Create dormitories
1004
+ const dormitory1 = await system.storage.create('Dormitory', {
1005
+ name: 'Current Dormitory',
1006
+ building: 'Test Building',
1007
+ roomNumber: '401',
1008
+ capacity: 4
1009
+ });
1010
+
1011
+ const dormitory2 = await system.storage.create('Dormitory', {
1012
+ name: 'Target Dormitory',
1013
+ building: 'Test Building',
1014
+ roomNumber: '402',
1015
+ capacity: 4
1016
+ });
1017
+
1018
+ // Student already in dormitory1
1019
+ await system.storage.create('DormitoryMember', {
1020
+ user: student,
1021
+ dormitory: dormitory1,
1022
+ role: 'member',
1023
+ status: 'active',
1024
+ bedNumber: 1,
1025
+ joinedAt: new Date().toISOString()
1026
+ });
1027
+
1028
+ // Try to apply to dormitory2
1029
+ const result = await controller.callInteraction('ApplyForDormitory', {
1030
+ user: student,
1031
+ payload: {
1032
+ dormitoryId: dormitory2,
1033
+ message: 'Want to change dormitory'
1034
+ }
1035
+ });
1036
+
1037
+ expect(result.error).toBeTruthy();
1038
+ expect(result.error.message).toContain('NoActiveDormitory');
1039
+ });
1040
+ });
1041
+ ```
1042
+
1043
+ ### 12.3.6 State Machine Permission Testing
1044
+
1045
+ ```typescript
1046
+ describe('State Machine Permission Testing', () => {
1047
+ test('state machine computeTarget function coverage test', async () => {
1048
+ // Create admin and target user
1049
+ const admin = await system.storage.create('User', {
1050
+ name: 'State Machine Test Admin',
1051
+ role: 'admin',
1052
+ email: 'statemachine@test.com'
1053
+ });
1054
+
1055
+ const targetUser = await system.storage.create('User', {
1056
+ name: 'Student to be Kicked',
1057
+ role: 'student',
1058
+ email: 'target@test.com',
1059
+ studentId: 'TARGET001'
1060
+ });
1061
+
1062
+ // Create dormitory and member
1063
+ const dormitory = await system.storage.create('Dormitory', {
1064
+ name: 'State Machine Test Dormitory',
1065
+ building: 'State Machine Test Building',
1066
+ roomNumber: '999',
1067
+ capacity: 4
1068
+ });
1069
+
1070
+ const targetMember = await system.storage.create('DormitoryMember', {
1071
+ user: targetUser,
1072
+ dormitory: dormitory,
1073
+ role: 'member',
1074
+ status: 'active',
1075
+ score: -60,
1076
+ bedNumber: 1,
1077
+ joinedAt: new Date().toISOString()
1078
+ });
1079
+
1080
+ // Create kick request
1081
+ const kickRequest = await system.storage.create('KickRequest', {
1082
+ targetMember: targetMember,
1083
+ requester: admin,
1084
+ reason: 'Violated dormitory rules, score too low',
1085
+ status: 'pending',
1086
+ createdAt: new Date().toISOString()
1087
+ });
1088
+
1089
+ // Execute ApproveKickRequest interaction, trigger state machine
1090
+ const result = await controller.callInteraction('ApproveKickRequest', {
1091
+ user: admin,
1092
+ payload: {
1093
+ kickRequestId: kickRequest,
1094
+ adminComment: 'Admin approved kick request'
1095
+ }
1096
+ });
1097
+
1098
+ expect(result.error).toBeUndefined();
1099
+
1100
+ // Verify state machine successfully executed state transition
1101
+ const { MatchExp } = controller.globals;
1102
+ const updatedMember = await system.storage.findOne('DormitoryMember',
1103
+ MatchExp.atom({ key: 'id', value: ['=', targetMember.id] }),
1104
+ undefined,
1105
+ ['status']
1106
+ );
1107
+
1108
+ expect(updatedMember.status).toBe('kicked');
1109
+ });
1110
+ });
1111
+ ```
1112
+
1113
+ ### 12.3.7 Permission Debugging and Error Handling Testing
1114
+
1115
+ ```typescript
1116
+ describe('Permission Debugging and Error Handling', () => {
1117
+ test('should provide clear permission error messages', async () => {
1118
+ const student = await system.storage.create('User', {
1119
+ name: 'Regular Student',
1120
+ role: 'student',
1121
+ email: 'student@example.com'
1122
+ });
1123
+
1124
+ const result = await controller.callInteraction('CreateDormitory', {
1125
+ user: student,
1126
+ payload: {
1127
+ name: 'Test Dormitory',
1128
+ building: 'Test Building',
1129
+ roomNumber: '101',
1130
+ capacity: 4,
1131
+ description: 'Test dormitory'
1132
+ }
1133
+ });
1134
+
1135
+ expect(result.error).toBeTruthy();
1136
+ expect(result.error.message).toContain('Admin');
1137
+ });
1138
+
1139
+ test('permission checks should handle database query errors', async () => {
1140
+ const student = await system.storage.create('User', {
1141
+ name: 'Test Student',
1142
+ role: 'student',
1143
+ email: 'test@example.com'
1144
+ });
1145
+
1146
+ // Pass invalid ID to trigger query error
1147
+ const result = await controller.callInteraction('RecordScore', {
1148
+ user: student,
1149
+ payload: {
1150
+ memberId: { id: 'invalid-member-id' },
1151
+ points: 10,
1152
+ reason: 'Test error handling',
1153
+ category: 'hygiene'
1154
+ }
1155
+ });
1156
+
1157
+ expect(result.error).toBeTruthy();
1158
+ });
1159
+ });
1160
+ ```
1161
+
1162
+ ## 12.4 Testing Best Practices
1163
+
1164
+ ### 12.4.1 Test Organization (Interaction-Focused)
1165
+
1166
+ ```typescript
1167
+ // Organize tests by Interactions, not by data structures
1168
+ describe('User Management Interactions', () => {
1169
+ describe('CreateUser Interaction', () => {
1170
+ test('should create user with valid data', () => {});
1171
+ test('should reject invalid email format', () => {});
1172
+ test('should enforce unique username constraint', () => {});
1173
+ test('should update userCount computation', () => {});
1174
+ test('should require admin permission', () => {});
1175
+ });
1176
+
1177
+ describe('CreateFriendship Interaction', () => {
1178
+ test('should establish friendship between users', () => {});
1179
+ test('should update both users friend count', () => {});
1180
+ test('should handle symmetric relationship correctly', () => {});
1181
+ test('should prevent duplicate friendships', () => {});
1182
+ test('should require both users consent', () => {});
1183
+ });
1184
+
1185
+ describe('UpdateUserScore Interaction', () => {
1186
+ test('should update user score correctly', () => {});
1187
+ test('should trigger score-based computations', () => {});
1188
+ test('should validate score range', () => {});
1189
+ test('should require moderator permission', () => {});
1190
+ test('should create audit log entry', () => {});
1191
+ });
1192
+
1193
+ describe('Edge Cases and Error Scenarios', () => {
1194
+ test('should handle concurrent friend requests', () => {});
1195
+ test('should handle database connection failures gracefully', () => {});
1196
+ test('should provide meaningful error messages', () => {});
1197
+ });
1198
+ });
1199
+ ```
1200
+
1201
+ Testing is a crucial aspect of building reliable interaqt applications. By focusing on comprehensive Interaction testing, developers can ensure their reactive applications work correctly and maintain quality as they evolve. Remember: in interaqt, all data flows from Interactions, so testing Interactions thoroughly is sufficient to achieve complete test coverage. Skip entity and relation unit tests - they're automatically covered when you test the Interactions that use them. Proper test organization, edge case coverage, and permission testing make tests maintainable and effective for long-term development.