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,870 @@
1
+ # Permission Test Implementation Guide
2
+
3
+ ## Overview
4
+ Permission testing verifies that conditions correctly control access to interactions. Tests should cover both allowed and denied scenarios for different user roles and data states.
5
+
6
+ ### Key Testing Pattern
7
+ When testing permission failures, always verify:
8
+ 1. **Error exists**: `expect(result.error).toBeDefined()`
9
+ 2. **Error type**: `expect((result.error as ConditionError).type).toBe('condition check failed')`
10
+ 3. **Which condition failed**: `expect((result.error as ConditionError).error.data.name).toBe('ConditionName')`
11
+
12
+ This detailed verification helps identify exactly which permission check failed, making debugging easier.
13
+
14
+ ## 🔴 CRITICAL: Permission Testing Principles
15
+
16
+ ### Error Handling Pattern
17
+ ```typescript
18
+ // ❌ WRONG: interaqt doesn't throw exceptions
19
+ try {
20
+ await controller.callInteraction('DeleteStyle', {
21
+ user: viewer,
22
+ payload: { style: { id: styleId } }
23
+ })
24
+ fail('Should have thrown')
25
+ } catch (e) {
26
+ // This will never execute
27
+ }
28
+
29
+ // ✅ CORRECT: Check error in result with detailed verification
30
+ import {ConditionError} from "interaqt"
31
+ const result = await controller.callInteraction('DeleteStyle', {
32
+ user: viewer,
33
+ payload: { style: { id: styleId } }
34
+ })
35
+ expect(result.error).toBeDefined()
36
+ expect((result.error as ConditionError).type).toBe('condition check failed')
37
+ // Verify which specific condition failed
38
+ expect((result.error as ConditionError).error.data.name).toBe('AdminRole')
39
+ ```
40
+
41
+ ### Common Error Types
42
+ - `'no permission'` → Never used (legacy)
43
+ - `'condition check failed'` → What you'll actually see
44
+
45
+ ## Testing Permission Patterns
46
+
47
+ ### 1. Role-Based Permission Test
48
+
49
+ Define permissions:
50
+
51
+ ```typescript
52
+ import { Condition, BoolExp, Conditions, Interaction, Action, Payload, PayloadItem, MatchExp, Controller, ConditionError } from 'interaqt'
53
+
54
+ // Step 1: Define Conditions
55
+ export const AdminRole = Condition.create({
56
+ name: 'AdminRole',
57
+ content: async function(this: Controller, event) {
58
+ return event.user?.role === 'admin'
59
+ }
60
+ })
61
+
62
+ export const OperatorRole = Condition.create({
63
+ name: 'OperatorRole',
64
+ content: async function(this: Controller, event) {
65
+ return event.user?.role === 'operator'
66
+ }
67
+ })
68
+
69
+ export const StyleNotOffline = Condition.create({
70
+ name: 'StyleNotOffline',
71
+ content: async function(this: Controller, event) {
72
+ const styleId = event.payload?.style?.id
73
+ if (!styleId) return false
74
+
75
+ const style = await this.system.storage.findOne('Style',
76
+ MatchExp.atom({ key: 'id', value: ['=', styleId] }),
77
+ undefined,
78
+ ['status']
79
+ )
80
+
81
+ return style?.status !== 'offline'
82
+ }
83
+ })
84
+
85
+ // Step 2: Create Interaction with Permissions
86
+ export const DeleteStyle = Interaction.create({
87
+ name: 'DeleteStyle',
88
+ action: Action.create({ name: 'deleteStyle' }),
89
+ payload: Payload.create({
90
+ items: [
91
+ PayloadItem.create({
92
+ name: 'style',
93
+ base: Style,
94
+ isRef: true
95
+ })
96
+ ]
97
+ }),
98
+ conditions: AdminRole // Only admin can delete
99
+ })
100
+ ```
101
+
102
+ Test implementation:
103
+
104
+ ```typescript
105
+ test('role-based permission', async () => {
106
+ // Step 1: Create test users
107
+ const admin = await system.storage.create('User', {
108
+ name: 'Admin',
109
+ role: 'admin'
110
+ })
111
+
112
+ const operator = await system.storage.create('User', {
113
+ name: 'Operator',
114
+ role: 'operator'
115
+ })
116
+
117
+ const viewer = await system.storage.create('User', {
118
+ name: 'Viewer',
119
+ role: 'viewer'
120
+ })
121
+
122
+ // Step 2: Create test data
123
+ const style = await system.storage.create('Style', {
124
+ label: 'Test Style',
125
+ status: 'published'
126
+ })
127
+
128
+ // Step 3: Test admin (allowed)
129
+ const adminResult = await controller.callInteraction('DeleteStyle', {
130
+ user: admin,
131
+ payload: { style: { id: style.id } }
132
+ })
133
+ expect(adminResult.error).toBeUndefined()
134
+
135
+ // Step 4: Test operator (denied)
136
+ const operatorResult = await controller.callInteraction('DeleteStyle', {
137
+ user: operator,
138
+ payload: { style: { id: style.id } }
139
+ })
140
+ expect(operatorResult.error).toBeDefined()
141
+ expect((operatorResult.error as ConditionError).type).toBe('condition check failed')
142
+ expect((operatorResult.error as ConditionError).error.data.name).toBe('AdminRole')
143
+
144
+ // Step 5: Test viewer (denied)
145
+ const viewerResult = await controller.callInteraction('DeleteStyle', {
146
+ user: viewer,
147
+ payload: { style: { id: style.id } }
148
+ })
149
+ expect(viewerResult.error).toBeDefined()
150
+ expect((viewerResult.error as ConditionError).type).toBe('condition check failed')
151
+ expect((viewerResult.error as ConditionError).error.data.name).toBe('AdminRole')
152
+ })
153
+ ```
154
+
155
+ ### 2. Combined Permission Test
156
+
157
+ ```typescript
158
+ // Define combined permission
159
+ export const UpdateStyle = Interaction.create({
160
+ name: 'UpdateStyle',
161
+ action: Action.create({ name: 'updateStyle' }),
162
+ payload: Payload.create({
163
+ items: [
164
+ PayloadItem.create({
165
+ name: 'style',
166
+ base: Style,
167
+ isRef: true
168
+ })
169
+ ]
170
+ }),
171
+ // Admin OR Operator AND style not offline
172
+ conditions: Conditions.create({
173
+ content: BoolExp.atom(AdminRole)
174
+ .or(BoolExp.atom(OperatorRole))
175
+ .and(BoolExp.atom(StyleNotOffline))
176
+ })
177
+ })
178
+
179
+ // Test implementation
180
+ test('combined permissions with BoolExp', async () => {
181
+ // Create users
182
+ const admin = await system.storage.create('User', {
183
+ name: 'Admin',
184
+ role: 'admin'
185
+ })
186
+
187
+ const operator = await system.storage.create('User', {
188
+ name: 'Operator',
189
+ role: 'operator'
190
+ })
191
+
192
+ const viewer = await system.storage.create('User', {
193
+ name: 'Viewer',
194
+ role: 'viewer'
195
+ })
196
+
197
+ // Create styles
198
+ const publishedStyle = await system.storage.create('Style', {
199
+ label: 'Published Style',
200
+ status: 'published'
201
+ })
202
+
203
+ const offlineStyle = await system.storage.create('Style', {
204
+ label: 'Offline Style',
205
+ status: 'offline'
206
+ })
207
+
208
+ // Test admin with published style (allowed)
209
+ const result1 = await controller.callInteraction('UpdateStyle', {
210
+ user: admin,
211
+ payload: { style: { id: publishedStyle.id } }
212
+ })
213
+ expect(result1.error).toBeUndefined()
214
+
215
+ // Test operator with published style (allowed)
216
+ const result2 = await controller.callInteraction('UpdateStyle', {
217
+ user: operator,
218
+ payload: { style: { id: publishedStyle.id } }
219
+ })
220
+ expect(result2.error).toBeUndefined()
221
+
222
+ // Test viewer with published style (denied)
223
+ const result3 = await controller.callInteraction('UpdateStyle', {
224
+ user: viewer,
225
+ payload: { style: { id: publishedStyle.id } }
226
+ })
227
+ expect(result3.error).toBeDefined()
228
+ expect((result3.error as ConditionError).type).toBe('condition check failed')
229
+ // With combined conditions, the first failing condition is reported
230
+ expect((result3.error as ConditionError).error.data.name).toBeDefined()
231
+
232
+ // Test admin with offline style (denied - even admin can't update offline)
233
+ const result4 = await controller.callInteraction('UpdateStyle', {
234
+ user: admin,
235
+ payload: { style: { id: offlineStyle.id } }
236
+ })
237
+ expect(result4.error).toBeDefined()
238
+ expect((result4.error as ConditionError).type).toBe('condition check failed')
239
+ expect((result4.error as ConditionError).error.data.name).toBe('StyleNotOffline')
240
+ })
241
+ ```
242
+
243
+ ### 3. Resource-Based Permission Test
244
+
245
+ ```typescript
246
+ // Define owner check
247
+ export const OwnerOnly = Condition.create({
248
+ name: 'OwnerOnly',
249
+ content: async function(this: Controller, event) {
250
+ const styleId = event.payload?.style?.id
251
+ if (!styleId) return false
252
+
253
+ const style = await this.system.storage.findOne('Style',
254
+ MatchExp.atom({ key: 'id', value: ['=', styleId] }),
255
+ undefined,
256
+ ['creator'] // Must specify attributeQuery!
257
+ )
258
+
259
+ return style?.creator?.id === event.user?.id
260
+ }
261
+ })
262
+
263
+ // Interaction that allows owner or admin
264
+ export const DeleteOwnStyle = Interaction.create({
265
+ name: 'DeleteOwnStyle',
266
+ action: Action.create({ name: 'deleteOwnStyle' }),
267
+ payload: Payload.create({
268
+ items: [
269
+ PayloadItem.create({
270
+ name: 'style',
271
+ base: Style,
272
+ isRef: true
273
+ })
274
+ ]
275
+ }),
276
+ conditions: Conditions.create({
277
+ content: BoolExp.atom(OwnerOnly).or(BoolExp.atom(AdminRole))
278
+ })
279
+ })
280
+
281
+ // Test
282
+ test('resource ownership permission', async () => {
283
+ // Create users
284
+ const owner = await system.storage.create('User', {
285
+ name: 'Owner',
286
+ role: 'user'
287
+ })
288
+
289
+ const otherUser = await system.storage.create('User', {
290
+ name: 'Other User',
291
+ role: 'user'
292
+ })
293
+
294
+ const admin = await system.storage.create('User', {
295
+ name: 'Admin',
296
+ role: 'admin'
297
+ })
298
+
299
+ // Create style with owner
300
+ const style = await system.storage.create('Style', {
301
+ label: 'My Style',
302
+ creator: { id: owner.id }
303
+ })
304
+
305
+ // Owner can delete (allowed)
306
+ const ownerResult = await controller.callInteraction('DeleteOwnStyle', {
307
+ user: owner,
308
+ payload: { style: { id: style.id } }
309
+ })
310
+ expect(ownerResult.error).toBeUndefined()
311
+
312
+ // Other user cannot delete (denied)
313
+ const otherResult = await controller.callInteraction('DeleteOwnStyle', {
314
+ user: otherUser,
315
+ payload: { style: { id: style.id } }
316
+ })
317
+ expect(otherResult.error).toBeDefined()
318
+ expect((otherResult.error as ConditionError).type).toBe('condition check failed')
319
+ // Should fail on OwnerOnly condition
320
+ expect((otherResult.error as ConditionError).error.data.name).toBe('OwnerOnly')
321
+
322
+ // Admin can delete any style (allowed)
323
+ const adminResult = await controller.callInteraction('DeleteOwnStyle', {
324
+ user: admin,
325
+ payload: { style: { id: style.id } }
326
+ })
327
+ expect(adminResult.error).toBeUndefined()
328
+ })
329
+ ```
330
+
331
+ ### 4. Data Retrieval Permission Test
332
+
333
+ Test permissions for data retrieval interactions using GetAction:
334
+
335
+ ```typescript
336
+ import { GetAction, Query, QueryItem, Condition, Controller, MatchExp, ConditionError } from 'interaqt'
337
+
338
+ // Check query-based permissions
339
+ export const CanViewPrivateData = Condition.create({
340
+ name: 'CanViewPrivateData',
341
+ content: async function(this: Controller, event) {
342
+ if (event.user?.role === 'admin') return true
343
+
344
+ const queryMatch = event.query?.match
345
+ if (!queryMatch) return true // No filter means public data
346
+
347
+ // Users can only query their own data
348
+ if (queryMatch.key === 'owner.id') {
349
+ return queryMatch.value?.[1] === event.user?.id
350
+ }
351
+
352
+ // Allow public status queries
353
+ if (queryMatch.key === 'status') {
354
+ const status = queryMatch.value?.[1]
355
+ return status === 'public' || status === 'published'
356
+ }
357
+
358
+ return false
359
+ }
360
+ })
361
+
362
+ // Department-based access control
363
+ export const DepartmentDataAccess = Condition.create({
364
+ name: 'DepartmentDataAccess',
365
+ content: async function(this: Controller, event) {
366
+ const userDept = event.user?.department
367
+ if (!userDept) return false
368
+
369
+ const queryMatch = event.query?.match
370
+ if (queryMatch?.key === 'department') {
371
+ return queryMatch.value?.[1] === userDept
372
+ }
373
+
374
+ return false
375
+ }
376
+ })
377
+
378
+ // Apply conditions to data retrieval
379
+ export const GetUserDocuments = Interaction.create({
380
+ name: 'GetUserDocuments',
381
+ action: GetAction,
382
+ data: Document,
383
+ conditions: CanViewPrivateData
384
+ })
385
+
386
+ // Test implementation
387
+ test('data retrieval permissions based on query', async () => {
388
+ const admin = await system.storage.create('User', {
389
+ role: 'admin'
390
+ })
391
+
392
+ const user1 = await system.storage.create('User', {
393
+ role: 'user'
394
+ })
395
+
396
+ const user2 = await system.storage.create('User', {
397
+ role: 'user'
398
+ })
399
+
400
+ // Create documents
401
+ await system.storage.create('Document', {
402
+ title: 'Private Doc',
403
+ status: 'private',
404
+ owner: { id: user1.id }
405
+ })
406
+
407
+ // Admin can view private documents
408
+ const adminResult = await controller.callInteraction('GetUserDocuments', {
409
+ user: admin,
410
+ query: {
411
+ match: MatchExp.atom({ key: 'status', value: ['=', 'private'] }),
412
+ attributeQuery: ['id', 'title', 'status']
413
+ }
414
+ })
415
+ expect(adminResult.error).toBeUndefined()
416
+
417
+ // User can view own documents
418
+ const ownResult = await controller.callInteraction('GetUserDocuments', {
419
+ user: user1,
420
+ query: {
421
+ match: MatchExp.atom({ key: 'owner.id', value: ['=', user1.id] }),
422
+ attributeQuery: ['id', 'title']
423
+ }
424
+ })
425
+ expect(ownResult.error).toBeUndefined()
426
+
427
+ // User cannot view others' documents
428
+ const othersResult = await controller.callInteraction('GetUserDocuments', {
429
+ user: user1,
430
+ query: {
431
+ match: MatchExp.atom({ key: 'owner.id', value: ['=', user2.id] }),
432
+ attributeQuery: ['id', 'title']
433
+ }
434
+ })
435
+ expect(othersResult.error).toBeDefined()
436
+ expect((othersResult.error as ConditionError).type).toBe('condition check failed')
437
+ expect((othersResult.error as ConditionError).error.data.name).toBe('CanViewPrivateData')
438
+ })
439
+
440
+ // Pagination limits based on user role
441
+ export const EnforcePaginationLimits = Condition.create({
442
+ name: 'EnforcePaginationLimits',
443
+ content: async function(this: Controller, event) {
444
+ const modifier = event.query?.modifier
445
+ const maxLimits = { admin: 1000, premium: 500, user: 100 }
446
+ const userLimit = maxLimits[event.user?.role] || 50
447
+
448
+ if (modifier?.limit && modifier.limit > userLimit) {
449
+ event.error = `Limit exceeds maximum allowed (${userLimit})`
450
+ return false
451
+ }
452
+
453
+ return true
454
+ }
455
+ })
456
+ ```
457
+
458
+ ### 5. Payload Condition Test
459
+
460
+ Define payload conditions:
461
+
462
+ ```typescript
463
+ // Check if style is published
464
+ export const CheckPublishedStyle = Condition.create({
465
+ name: 'CheckPublishedStyle',
466
+ content: async function(this: Controller, event) {
467
+ const styleId = event.payload?.style?.id
468
+ if (!styleId) return false
469
+
470
+ const style = await this.system.storage.findOne('Style',
471
+ MatchExp.atom({ key: 'id', value: ['=', styleId] }),
472
+ undefined,
473
+ ['status']
474
+ )
475
+
476
+ return style && style.status === 'published'
477
+ }
478
+ })
479
+
480
+ // Apply to interaction
481
+ export const ShareStyle = Interaction.create({
482
+ name: 'ShareStyle',
483
+ action: Action.create({ name: 'shareStyle' }),
484
+ payload: Payload.create({
485
+ items: [
486
+ PayloadItem.create({
487
+ name: 'style',
488
+ base: Style,
489
+ isRef: true
490
+ })
491
+ ]
492
+ }),
493
+ conditions: Conditions.create({
494
+ content: BoolExp.atom(CheckPublishedStyle).and(BoolExp.atom(OperatorRole))
495
+ })
496
+ })
497
+
498
+ // Test payload validation
499
+ test('payload validation in conditions', async () => {
500
+ const operator = await system.storage.create('User', {
501
+ name: 'Operator',
502
+ role: 'operator'
503
+ })
504
+
505
+ const publishedStyle = await system.storage.create('Style', {
506
+ label: 'Published',
507
+ status: 'published'
508
+ })
509
+
510
+ const draftStyle = await system.storage.create('Style', {
511
+ label: 'Draft',
512
+ status: 'draft'
513
+ })
514
+
515
+ // Published style can be shared (allowed)
516
+ const result1 = await controller.callInteraction('ShareStyle', {
517
+ user: operator,
518
+ payload: { style: { id: publishedStyle.id } }
519
+ })
520
+ expect(result1.error).toBeUndefined()
521
+
522
+ // Draft style cannot be shared (denied)
523
+ const result2 = await controller.callInteraction('ShareStyle', {
524
+ user: operator,
525
+ payload: { style: { id: draftStyle.id } }
526
+ })
527
+ expect(result2.error).toBeDefined()
528
+ expect((result2.error as ConditionError).type).toBe('condition check failed')
529
+ // Should fail on CheckPublishedStyle condition
530
+ expect((result2.error as ConditionError).error.data.name).toBe('CheckPublishedStyle')
531
+ })
532
+ ```
533
+
534
+ ## Best Practices for Permission Testing
535
+
536
+ ### 1. Test All Branches
537
+ ```typescript
538
+ test('comprehensive permission coverage', async () => {
539
+ // Test all roles
540
+ const roles = ['admin', 'operator', 'viewer', 'user']
541
+
542
+ for (const role of roles) {
543
+ const user = await system.storage.create('User', {
544
+ name: `${role} user`,
545
+ role: role
546
+ })
547
+
548
+ const result = await controller.callInteraction('AdminOnlyAction', {
549
+ user: user
550
+ })
551
+
552
+ if (role === 'admin') {
553
+ expect(result.error).toBeUndefined()
554
+ } else {
555
+ expect(result.error).toBeDefined()
556
+ expect((result.error as ConditionError).type).toBe('condition check failed')
557
+ expect((result.error as ConditionError).error.data.name).toBe('AdminRole')
558
+ }
559
+ }
560
+ })
561
+ ```
562
+
563
+ ### 2. Test Edge Cases
564
+ ```typescript
565
+ test('edge cases in permissions', async () => {
566
+ // Test with null user
567
+ const result1 = await controller.callInteraction('RequireAuth', {
568
+ user: null
569
+ })
570
+ expect(result1.error).toBeDefined()
571
+
572
+ // Test with missing payload data
573
+ const user = await system.storage.create('User', { role: 'admin' })
574
+ const result2 = await controller.callInteraction('UpdateStyle', {
575
+ user: user,
576
+ payload: {} // Missing style
577
+ })
578
+ expect(result2.error).toBeDefined()
579
+
580
+ // Test with non-existent resource
581
+ const result3 = await controller.callInteraction('UpdateStyle', {
582
+ user: user,
583
+ payload: { style: { id: 'non-existent-id' } }
584
+ })
585
+ expect(result3.error).toBeDefined()
586
+ })
587
+ ```
588
+
589
+ ### 3. Test Complex Conditions
590
+ ```typescript
591
+ test('complex permission logic', async () => {
592
+ // Define time-based condition
593
+ const BusinessHours = Condition.create({
594
+ name: 'BusinessHours',
595
+ content: async function(this: Controller, event) {
596
+ const hour = new Date().getHours()
597
+ return hour >= 9 && hour < 17
598
+ }
599
+ })
600
+
601
+ // Active user check
602
+ const ActiveUser = Condition.create({
603
+ name: 'ActiveUser',
604
+ content: async function(this: Controller, event) {
605
+ return event.user?.status === 'active'
606
+ }
607
+ })
608
+
609
+ // Complex interaction
610
+ const SensitiveAction = Interaction.create({
611
+ name: 'SensitiveAction',
612
+ action: Action.create({ name: 'sensitive' }),
613
+ conditions: Conditions.create({
614
+ content: BoolExp.atom(AdminRole)
615
+ .and(BoolExp.atom(ActiveUser))
616
+ .and(BoolExp.atom(BusinessHours))
617
+ })
618
+ })
619
+
620
+ // Test all combinations
621
+ const activeAdmin = await system.storage.create('User', {
622
+ role: 'admin',
623
+ status: 'active'
624
+ })
625
+
626
+ const inactiveAdmin = await system.storage.create('User', {
627
+ role: 'admin',
628
+ status: 'inactive'
629
+ })
630
+
631
+ // Mock time if needed for consistent tests
632
+ // ... test logic
633
+ })
634
+ ```
635
+
636
+ ### 4. Verify Detailed Error Information
637
+ ```typescript
638
+ test('verify detailed condition error information', async () => {
639
+ // When a condition fails, verify all error details
640
+ const result = await controller.callInteraction('AdminOnlyAction', {
641
+ user: normalUser
642
+ })
643
+
644
+ // Basic error checks
645
+ expect(result.error).toBeDefined()
646
+ expect((result.error as ConditionError).type).toBe('condition check failed')
647
+
648
+ // Detailed error verification - identify which condition failed
649
+ expect((result.error as ConditionError).error.data.name).toBe('AdminRole')
650
+
651
+ // For combined conditions, test each failure scenario
652
+ const complexResult = await controller.callInteraction('ComplexAction', {
653
+ user: unverifiedAdmin // Admin but not verified
654
+ })
655
+ expect(complexResult.error).toBeDefined()
656
+ expect((complexResult.error as ConditionError).type).toBe('condition check failed')
657
+ // Should report the specific condition that failed
658
+ expect((complexResult.error as ConditionError).error.data.name).toBe('EmailVerified')
659
+ })
660
+
661
+ test('meaningful error messages', async () => {
662
+ // Define condition with custom error
663
+ const CustomError = Condition.create({
664
+ name: 'CustomError',
665
+ content: async function(this: Controller, event) {
666
+ if (!event.user) {
667
+ event.error = 'User authentication required'
668
+ return false
669
+ }
670
+ if (event.user.credits < 10) {
671
+ event.error = 'Insufficient credits (minimum: 10)'
672
+ return false
673
+ }
674
+ return true
675
+ }
676
+ })
677
+
678
+ // Test error messages
679
+ const poorUser = await system.storage.create('User', {
680
+ credits: 5
681
+ })
682
+
683
+ const result = await controller.callInteraction('PremiumAction', {
684
+ user: poorUser
685
+ })
686
+
687
+ expect(result.error).toBeDefined()
688
+ expect((result.error as ConditionError).type).toBe('condition check failed')
689
+ expect((result.error as ConditionError).error.data.name).toBe('CustomError')
690
+ expect((result.error as ConditionError).message).toContain('Insufficient credits')
691
+ })
692
+ ```
693
+
694
+ ### 5. Test State-Dependent Permissions
695
+ ```typescript
696
+ test('state-dependent permissions', async () => {
697
+ // User must have verified email
698
+ const VerifiedEmail = Condition.create({
699
+ name: 'VerifiedEmail',
700
+ content: async function(this: Controller, event) {
701
+ return event.user?.emailVerified === true
702
+ }
703
+ })
704
+
705
+ // User must not be banned
706
+ const NotBanned = Condition.create({
707
+ name: 'NotBanned',
708
+ content: async function(this: Controller, event) {
709
+ return event.user?.banned !== true
710
+ }
711
+ })
712
+
713
+ // Apply multiple state checks
714
+ const PostComment = Interaction.create({
715
+ name: 'PostComment',
716
+ action: Action.create({ name: 'postComment' }),
717
+ conditions: Conditions.create({
718
+ content: BoolExp.atom(VerifiedEmail).and(BoolExp.atom(NotBanned))
719
+ })
720
+ })
721
+
722
+ // Test various user states
723
+ const verifiedUser = await system.storage.create('User', {
724
+ emailVerified: true,
725
+ banned: false
726
+ })
727
+
728
+ const unverifiedUser = await system.storage.create('User', {
729
+ emailVerified: false,
730
+ banned: false
731
+ })
732
+
733
+ const bannedUser = await system.storage.create('User', {
734
+ emailVerified: true,
735
+ banned: true
736
+ })
737
+
738
+ // Only verified, non-banned user can post
739
+ const result1 = await controller.callInteraction('PostComment', {
740
+ user: verifiedUser
741
+ })
742
+ expect(result1.error).toBeUndefined()
743
+
744
+ const result2 = await controller.callInteraction('PostComment', {
745
+ user: unverifiedUser
746
+ })
747
+ expect(result2.error).toBeDefined()
748
+
749
+ const result3 = await controller.callInteraction('PostComment', {
750
+ user: bannedUser
751
+ })
752
+ expect(result3.error).toBeDefined()
753
+ })
754
+ ```
755
+
756
+ ### 6. Test Conditional Updates
757
+ ```typescript
758
+ test('conditional state updates', async () => {
759
+ // User can only delete if not deleted
760
+ const NotDeleted = Condition.create({
761
+ name: 'NotDeleted',
762
+ content: async function(this: Controller, event) {
763
+ const styleId = event.payload?.style?.id
764
+ if (!styleId) return false
765
+
766
+ const style = await this.system.storage.findOne('Style',
767
+ MatchExp.atom({ key: 'id', value: ['=', styleId] }),
768
+ undefined,
769
+ ['isDeleted']
770
+ )
771
+
772
+ return style && !style.isDeleted
773
+ }
774
+ })
775
+
776
+ const DeleteStyle = Interaction.create({
777
+ name: 'DeleteStyle',
778
+ action: Action.create({ name: 'deleteStyle' }),
779
+ conditions: Conditions.create({
780
+ content: BoolExp.atom(AdminRole).and(BoolExp.atom(NotDeleted))
781
+ })
782
+ })
783
+
784
+ const admin = await system.storage.create('User', { role: 'admin' })
785
+ const style = await system.storage.create('Style', {
786
+ label: 'Test',
787
+ isDeleted: false
788
+ })
789
+
790
+ // First delete succeeds
791
+ const result1 = await controller.callInteraction('DeleteStyle', {
792
+ user: admin,
793
+ payload: { style: { id: style.id } }
794
+ })
795
+ expect(result1.error).toBeUndefined()
796
+
797
+ // Update style to deleted
798
+ await system.storage.update('Style', style.id, { isDeleted: true })
799
+
800
+ // Second delete fails
801
+ const result2 = await controller.callInteraction('DeleteStyle', {
802
+ user: admin,
803
+ payload: { style: { id: style.id } }
804
+ })
805
+ expect(result2.error).toBeDefined()
806
+ })
807
+ ```
808
+
809
+ ## Testing Checklist
810
+ - [ ] Test all user roles (admin, operator, viewer, etc.)
811
+ - [ ] Test allowed and denied scenarios
812
+ - [ ] Test with missing/invalid payload data
813
+ - [ ] Test resource state conditions
814
+ - [ ] Test combined permissions (AND/OR logic)
815
+ - [ ] Test edge cases (null user, non-existent resources)
816
+ - [ ] Verify error types are 'condition check failed'
817
+ - [ ] Verify specific failed condition name with `error.error.data.name`
818
+ - [ ] Test custom error messages if used
819
+ - [ ] Cover all branches in condition logic
820
+ - [ ] Test time/state dependent conditions
821
+
822
+ ## Common Testing Mistakes
823
+
824
+ ### 1. Missing attributeQuery
825
+ ```typescript
826
+ // ❌ WRONG: Without attributeQuery, only returns { id }
827
+ const style = await system.storage.findOne('Style',
828
+ MatchExp.atom({ key: 'id', value: ['=', styleId] })
829
+ )
830
+ // style.creator is undefined!
831
+
832
+ // ✅ CORRECT: Specify needed fields
833
+ const style = await system.storage.findOne('Style',
834
+ MatchExp.atom({ key: 'id', value: ['=', styleId] }),
835
+ undefined,
836
+ ['id', 'creator', 'status'] // Include all needed fields
837
+ )
838
+ ```
839
+
840
+ ### 2. Wrong Error Expectations
841
+ ```typescript
842
+ // ❌ WRONG: Expecting wrong error type or incomplete verification
843
+ expect((result.error as ConditionError).type).toBe('no permission')
844
+
845
+ // ❌ INCOMPLETE: Only checking error type
846
+ expect(result.error).toBeDefined()
847
+ expect((result.error as ConditionError).type).toBe('condition check failed')
848
+
849
+ // ✅ CORRECT: Complete error verification including which condition failed
850
+ expect(result.error).toBeDefined()
851
+ expect((result.error as ConditionError).type).toBe('condition check failed')
852
+ expect((result.error as ConditionError).error.data.name).toBe('AdminRole') // Verify specific condition
853
+ ```
854
+
855
+ ### 3. Incomplete Test Coverage
856
+ ```typescript
857
+ // ❌ WRONG: Only testing happy path
858
+ test('admin can delete', async () => {
859
+ // Only tests admin success
860
+ })
861
+
862
+ // ✅ CORRECT: Test all scenarios
863
+ test('delete permissions', async () => {
864
+ // Test admin: allowed
865
+ // Test operator: denied
866
+ // Test viewer: denied
867
+ // Test null user: denied
868
+ // Test with deleted style: denied
869
+ })
870
+ ```