@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.3

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 (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.mts +27 -2
  3. package/dist/index.d.ts +27 -2
  4. package/dist/index.js +70 -3
  5. package/dist/index.mjs +74 -12
  6. package/package.json +4 -3
  7. package/package.json.backup +0 -41
  8. package/src/Blueprint.ts +0 -216
  9. package/src/__tests__/Blueprint.test.ts +0 -106
  10. package/src/__tests__/action-context.test.ts +0 -166
  11. package/src/__tests__/actionCreators.test.ts +0 -179
  12. package/src/__tests__/builders.test.ts +0 -336
  13. package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
  14. package/src/__tests__/factories.test.ts +0 -229
  15. package/src/__tests__/loader.test.ts +0 -159
  16. package/src/__tests__/logger.test.ts +0 -70
  17. package/src/__tests__/type-inference.test.ts +0 -160
  18. package/src/__tests__/typed-transitions.test.ts +0 -126
  19. package/src/__tests__/useModuleConfig.test.ts +0 -61
  20. package/src/actionCreators.ts +0 -132
  21. package/src/actions.ts +0 -547
  22. package/src/atoms/index.ts +0 -600
  23. package/src/authoring.ts +0 -92
  24. package/src/browser-player.ts +0 -783
  25. package/src/builders.ts +0 -1342
  26. package/src/components/ExperienceWorkflowBridge.tsx +0 -123
  27. package/src/components/PlayerProvider.tsx +0 -43
  28. package/src/components/atoms/index.tsx +0 -269
  29. package/src/components/index.ts +0 -36
  30. package/src/conditions.ts +0 -692
  31. package/src/config/defineBlueprint.ts +0 -329
  32. package/src/config/defineModel.ts +0 -753
  33. package/src/config/defineWorkspace.ts +0 -24
  34. package/src/core/WorkflowRuntime.ts +0 -153
  35. package/src/factories.ts +0 -425
  36. package/src/grammar/index.ts +0 -173
  37. package/src/hooks/index.ts +0 -106
  38. package/src/hooks/useAuth.ts +0 -288
  39. package/src/hooks/useChannel.ts +0 -304
  40. package/src/hooks/useComputed.ts +0 -154
  41. package/src/hooks/useDomainSubscription.ts +0 -110
  42. package/src/hooks/useDuringAction.ts +0 -99
  43. package/src/hooks/useExperienceState.ts +0 -59
  44. package/src/hooks/useExpressionLibrary.ts +0 -129
  45. package/src/hooks/useForm.ts +0 -352
  46. package/src/hooks/useGeolocation.ts +0 -207
  47. package/src/hooks/useMapView.ts +0 -259
  48. package/src/hooks/useMiddleware.ts +0 -291
  49. package/src/hooks/useModel.ts +0 -363
  50. package/src/hooks/useModule.ts +0 -59
  51. package/src/hooks/useModuleConfig.ts +0 -61
  52. package/src/hooks/useMutation.ts +0 -237
  53. package/src/hooks/useNotification.ts +0 -151
  54. package/src/hooks/useOnChange.ts +0 -30
  55. package/src/hooks/useOnEnter.ts +0 -59
  56. package/src/hooks/useOnEvent.ts +0 -37
  57. package/src/hooks/useOnExit.ts +0 -27
  58. package/src/hooks/useOnTransition.ts +0 -30
  59. package/src/hooks/usePackage.ts +0 -128
  60. package/src/hooks/useParams.ts +0 -33
  61. package/src/hooks/usePlayer.ts +0 -308
  62. package/src/hooks/useQuery.ts +0 -184
  63. package/src/hooks/useRealtimeQuery.ts +0 -222
  64. package/src/hooks/useRole.ts +0 -191
  65. package/src/hooks/useRouteParams.ts +0 -100
  66. package/src/hooks/useRouter.ts +0 -347
  67. package/src/hooks/useServerAction.ts +0 -178
  68. package/src/hooks/useServerState.ts +0 -284
  69. package/src/hooks/useToast.ts +0 -164
  70. package/src/hooks/useTransition.ts +0 -39
  71. package/src/hooks/useView.ts +0 -102
  72. package/src/hooks/useWhileIn.ts +0 -48
  73. package/src/hooks/useWorkflow.ts +0 -63
  74. package/src/index.ts +0 -465
  75. package/src/loader/experience-workflow-loader.ts +0 -192
  76. package/src/loader/index.ts +0 -6
  77. package/src/local/LocalEngine.ts +0 -388
  78. package/src/local/LocalEngineAdapter.ts +0 -175
  79. package/src/local/LocalEngineContext.ts +0 -30
  80. package/src/logger.ts +0 -37
  81. package/src/mixins.ts +0 -1160
  82. package/src/providers/RuntimeContext.ts +0 -20
  83. package/src/providers/WorkflowProvider.tsx +0 -28
  84. package/src/routing/instance-key.ts +0 -107
  85. package/src/server/transition-context.ts +0 -172
  86. package/src/testing/index.ts +0 -9
  87. package/src/testing/useBlueprintTestRunner.ts +0 -91
  88. package/src/testing/useGraphAnalysis.ts +0 -18
  89. package/src/testing/useTestRunner.ts +0 -77
  90. package/src/testing.ts +0 -995
  91. package/src/types/workflow-inference.ts +0 -158
  92. package/src/types.ts +0 -114
  93. package/tsconfig.json +0 -27
  94. package/vitest.config.ts +0 -8
@@ -1,336 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { model, field, state, transition, StateBuilder } from '../builders';
3
- import { setField, logEvent, serverAction } from '../actions';
4
-
5
- describe('StateBuilder.to() — inline transitions', () => {
6
- it('declares a single outgoing transition', () => {
7
- const result = model('test')
8
- .state('draft', state.initial().to('review', 'submit'))
9
- .state('review')
10
- .build();
11
-
12
- expect(result.transitions.submit).toBeDefined();
13
- expect(result.transitions.submit.from).toBe('draft');
14
- expect(result.transitions.submit.to).toBe('review');
15
- });
16
-
17
- it('declares multiple outgoing transitions from one state', () => {
18
- const result = model('test')
19
- .state('review', new StateBuilder()
20
- .to('approved', 'approve')
21
- .to('rejected', 'reject')
22
- )
23
- .build();
24
-
25
- expect(result.transitions.approve).toEqual({ from: 'review', to: 'approved' });
26
- expect(result.transitions.reject).toEqual({ from: 'review', to: 'rejected' });
27
- });
28
-
29
- it('supports transition configuration via callback', () => {
30
- const result = model('test')
31
- .state('draft', state.initial()
32
- .to('review', 'submit', t => t.require('title', 'body').roles('author'))
33
- )
34
- .state('review')
35
- .build();
36
-
37
- expect(result.transitions.submit.requiredFields).toEqual(['title', 'body']);
38
- expect(result.transitions.submit.roles).toEqual(['author']);
39
- });
40
-
41
- it('standalone transitions override inline transitions', () => {
42
- const result = model('test')
43
- .state('draft', state.initial().to('review', 'submit'))
44
- .state('review')
45
- .transition('submit', transition.from('draft').to('review').roles('admin'))
46
- .build();
47
-
48
- expect(result.transitions.submit.roles).toEqual(['admin']);
49
- });
50
-
51
- it('inline transitions work with actions', () => {
52
- const result = model('test')
53
- .state('active', new StateBuilder()
54
- .to('inactive', 'deactivate', t =>
55
- t.do(setField('active', 'false'), logEvent('deactivated'))
56
- )
57
- )
58
- .build();
59
-
60
- expect(result.transitions.deactivate.actions).toHaveLength(2);
61
- expect(result.transitions.deactivate.actions![0].type).toBe('set_field');
62
- expect(result.transitions.deactivate.actions![1].type).toBe('log_event');
63
- });
64
-
65
- it('coexists with standalone transitions on the same model', () => {
66
- const result = model('test')
67
- .state('draft', state.initial().to('review', 'submit'))
68
- .state('review')
69
- .state('published')
70
- .transition('publish', transition.from('review').to('published'))
71
- .build();
72
-
73
- expect(result.transitions.submit.from).toBe('draft');
74
- expect(result.transitions.publish.from).toBe('review');
75
- });
76
- });
77
-
78
- describe('ModelBuilder.line() — auto-named transitions', () => {
79
- it('auto-generates transition names from state pairs', () => {
80
- const result = model('order')
81
- .line(
82
- ['placed', state.initial()],
83
- 'confirmed',
84
- 'shipped',
85
- ['delivered', state.end()],
86
- )
87
- .build();
88
-
89
- expect(Object.keys(result.states).sort()).toEqual(
90
- ['confirmed', 'delivered', 'placed', 'shipped'].sort()
91
- );
92
- expect(result.states.placed.type).toBe('initial');
93
- expect(result.states.delivered.type).toBe('end');
94
- expect(result.transitions.placed_to_confirmed).toEqual({ from: 'placed', to: 'confirmed' });
95
- expect(result.transitions.confirmed_to_shipped).toEqual({ from: 'confirmed', to: 'shipped' });
96
- expect(result.transitions.shipped_to_delivered).toEqual({ from: 'shipped', to: 'delivered' });
97
- });
98
-
99
- it('configures incoming edge via tuple callback', () => {
100
- const result = model('test')
101
- .line(
102
- 'draft',
103
- ['review', t => t.require('title', 'body')],
104
- ['approved', t => t.roles('manager')],
105
- )
106
- .build();
107
-
108
- expect(result.transitions.draft_to_review.requiredFields).toEqual(['title', 'body']);
109
- expect(result.transitions.review_to_approved.roles).toEqual(['manager']);
110
- });
111
-
112
- it('branching via multiple lines', () => {
113
- const result = model('order')
114
- .line(
115
- ['placed', state.initial()],
116
- 'confirmed',
117
- 'shipped',
118
- )
119
- .line('placed', 'cancelled')
120
- .line('confirmed', 'cancelled')
121
- .build();
122
-
123
- expect(Object.keys(result.transitions).sort()).toEqual([
124
- 'confirmed_to_cancelled',
125
- 'confirmed_to_shipped',
126
- 'placed_to_cancelled',
127
- 'placed_to_confirmed',
128
- ].sort());
129
- });
130
-
131
- it('two-state line', () => {
132
- const result = model('test')
133
- .line('active', 'inactive')
134
- .build();
135
-
136
- expect(result.transitions.active_to_inactive).toEqual({ from: 'active', to: 'inactive' });
137
- });
138
-
139
- it('self-loop', () => {
140
- const result = model('test')
141
- .line('active', 'active')
142
- .build();
143
-
144
- expect(result.transitions.active_to_active).toEqual({ from: 'active', to: 'active' });
145
- });
146
-
147
- it('with actions on edges', () => {
148
- const result = model('test')
149
- .line(
150
- 'draft',
151
- ['published', t => t.do(logEvent('published'), setField('publishedAt', 'NOW()'))],
152
- )
153
- .build();
154
-
155
- const tr = result.transitions.draft_to_published;
156
- expect(tr.actions).toHaveLength(2);
157
- expect(tr.actions![0].type).toBe('log_event');
158
- expect(tr.actions![1].type).toBe('set_field');
159
- });
160
-
161
- it('works with field definitions', () => {
162
- const result = model('invoice')
163
- .field('amount', field.currency().required())
164
- .line(
165
- ['draft', state.initial()],
166
- ['sent', t => t.require('amount')],
167
- ['paid', state.end()],
168
- )
169
- .build();
170
-
171
- expect(result.fields.amount.type).toBe('currency');
172
- expect(result.transitions.draft_to_sent.requiredFields).toEqual(['amount']);
173
- });
174
-
175
- it('mixes with .state() and .transition() methods', () => {
176
- const result = model('test')
177
- .state('error', new StateBuilder().onEnter(setField('hasError', 'true')))
178
- .line(
179
- ['draft', state.initial()],
180
- 'review',
181
- ['published', state.end()],
182
- )
183
- .transition('fail', transition.from('review').to('error'))
184
- .build();
185
-
186
- expect(result.states.error.onEnter).toHaveLength(1);
187
- expect(result.states.draft.type).toBe('initial');
188
- expect(result.transitions.draft_to_review).toBeDefined();
189
- expect(result.transitions.fail.from).toBe('review');
190
- });
191
-
192
- it('shared states across lines are unified', () => {
193
- const result = model('auth')
194
- .line(
195
- ['unauthenticated', state.initial()],
196
- 'authenticating',
197
- 'authenticated',
198
- )
199
- .line(
200
- 'unauthenticated',
201
- 'authenticating',
202
- 'authenticated',
203
- )
204
- .build();
205
-
206
- // States appear only once
207
- expect(Object.keys(result.states)).toEqual([
208
- 'unauthenticated', 'authenticating', 'authenticated',
209
- ]);
210
- expect(result.states.unauthenticated.type).toBe('initial');
211
- });
212
- });
213
-
214
- describe('ModelBuilder.line() — named transitions via tuple', () => {
215
- it('overrides auto-name with explicit transition name', () => {
216
- const result = model('test')
217
- .line(
218
- 'draft',
219
- ['review', 'submit'],
220
- ['published', 'approve'],
221
- )
222
- .build();
223
-
224
- expect(result.transitions.submit).toEqual({ from: 'draft', to: 'review' });
225
- expect(result.transitions.approve).toEqual({ from: 'review', to: 'published' });
226
- // Auto-named versions should NOT exist
227
- expect(result.transitions.draft_to_review).toBeUndefined();
228
- expect(result.transitions.review_to_published).toBeUndefined();
229
- });
230
-
231
- it('named transition with configuration', () => {
232
- const result = model('test')
233
- .line(
234
- 'draft',
235
- ['review', 'submit', t => t.require('title')],
236
- ['published', 'approve', t => t.roles('manager')],
237
- )
238
- .build();
239
-
240
- expect(result.transitions.submit.requiredFields).toEqual(['title']);
241
- expect(result.transitions.approve.roles).toEqual(['manager']);
242
- });
243
-
244
- it('mix named and auto-named in same line', () => {
245
- const result = model('test')
246
- .line(
247
- 'draft',
248
- ['review', 'submit'], // named
249
- 'published', // auto-named: review_to_published
250
- )
251
- .build();
252
-
253
- expect(result.transitions.submit).toBeDefined();
254
- expect(result.transitions.review_to_published).toBeDefined();
255
- });
256
-
257
- it('merges from-states for named transitions across lines', () => {
258
- const result = model('order')
259
- .line('placed', ['cancelled', 'cancel'])
260
- .line('confirmed', ['cancelled', 'cancel'])
261
- .build();
262
-
263
- const cancelFrom = result.transitions.cancel.from;
264
- expect(Array.isArray(cancelFrom)).toBe(true);
265
- expect(cancelFrom).toContain('placed');
266
- expect(cancelFrom).toContain('confirmed');
267
- });
268
- });
269
-
270
- describe('ModelBuilder.line() — complete auth example', () => {
271
- it('models auth with auto-named transitions', () => {
272
- const result = model('mod-authentication')
273
- .field('errorMessage', field.string(''))
274
-
275
- // Login flow
276
- .line(
277
- ['unauthenticated', state.initial().onEnter(setField('errorMessage', '""'))],
278
- ['authenticating', t => t.do(serverAction('authenticate', { method: 'login' }))],
279
- 'authenticated',
280
- )
281
-
282
- // Error handling
283
- .line('authenticating', 'error')
284
- .line('error', 'unauthenticated')
285
-
286
- // Logout
287
- .line(
288
- 'authenticated',
289
- ['unauthenticated', t => t.do(serverAction('destroy_session'))],
290
- )
291
- .build();
292
-
293
- expect(result.states.unauthenticated.type).toBe('initial');
294
- expect(result.transitions.unauthenticated_to_authenticating.actions![0].type).toBe('server:authenticate');
295
- expect(result.transitions.authenticated_to_unauthenticated.actions![0].type).toBe('server:destroy_session');
296
- expect(result.transitions.authenticating_to_error).toBeDefined();
297
- expect(result.transitions.error_to_unauthenticated).toBeDefined();
298
- });
299
-
300
- it('models auth with named transitions', () => {
301
- const result = model('mod-authentication')
302
- .field('errorMessage', field.string(''))
303
-
304
- .line(
305
- ['unauthenticated', state.initial().onEnter(setField('errorMessage', '""'))],
306
- ['authenticating', 'login', t => t.do(serverAction('authenticate', { method: 'login' }))],
307
- ['authenticated', 'login_success'],
308
- )
309
- .line(
310
- 'unauthenticated',
311
- ['authenticating', 'signup', t => t.do(serverAction('authenticate', { method: 'signup' }))],
312
- ['authenticated', 'signup_success'],
313
- )
314
- .line('authenticating', ['error', 'login_error'])
315
- .line('authenticating', ['error', 'signup_error'])
316
- .line('error', ['unauthenticated', 'retry'])
317
- .line(
318
- 'authenticated',
319
- ['unauthenticated', 'logout', t => t.do(serverAction('destroy_session'))],
320
- )
321
- .build();
322
-
323
- expect(result.states.unauthenticated.type).toBe('initial');
324
- expect(result.transitions.login.actions![0].type).toBe('server:authenticate');
325
- expect(result.transitions.signup.actions![0].type).toBe('server:authenticate');
326
- expect(result.transitions.logout.actions![0].type).toBe('server:destroy_session');
327
- expect(result.transitions.login_success.from).toBe('authenticating');
328
- expect(result.transitions.signup_success.from).toBe('authenticating');
329
- expect(result.transitions.retry.from).toBe('error');
330
- expect(result.transitions.retry.to).toBe('unauthenticated');
331
-
332
- // login_error and signup_error merge from-states
333
- const loginErrorFrom = result.transitions.login_error.from;
334
- expect(loginErrorFrom).toBe('authenticating');
335
- });
336
- });
@@ -1,106 +0,0 @@
1
- /**
2
- * Tests for BlueprintDependency composition types — routeConfig, slotMapping.
3
- */
4
-
5
- import { describe, it, expect } from 'vitest';
6
- import { defineBlueprint, type BlueprintDependency, type ModuleRouteConfig } from '../config/defineBlueprint';
7
-
8
- describe('BlueprintDependency composition types', () => {
9
- it('accepts routeConfig with prefix', () => {
10
- const dep: BlueprintDependency = {
11
- slug: 'mod-authentication',
12
- version: '>=3.0.0',
13
- routeConfig: {
14
- prefix: '/auth',
15
- },
16
- };
17
- expect(dep.routeConfig?.prefix).toBe('/auth');
18
- });
19
-
20
- it('accepts routeConfig with per-route remapping', () => {
21
- const dep: BlueprintDependency = {
22
- slug: 'mod-authentication',
23
- routeConfig: {
24
- prefix: '/auth',
25
- routes: {
26
- '/login': '/',
27
- '/signup': '/join',
28
- '/profile': false,
29
- },
30
- },
31
- };
32
- expect(dep.routeConfig?.routes?.['/login']).toBe('/');
33
- expect(dep.routeConfig?.routes?.['/profile']).toBe(false);
34
- });
35
-
36
- it('accepts slotMapping', () => {
37
- const dep: BlueprintDependency = {
38
- slug: 'mod-authentication',
39
- slotMapping: {
40
- 'auth:login-providers': 'app:social-login',
41
- 'auth:profile-sections': 'app:settings-tabs',
42
- },
43
- };
44
- expect(dep.slotMapping?.['auth:login-providers']).toBe('app:social-login');
45
- });
46
-
47
- it('accepts config', () => {
48
- const dep: BlueprintDependency = {
49
- slug: 'mod-authentication',
50
- config: {
51
- layout: 'card',
52
- methods: ['email-password', 'google'],
53
- },
54
- };
55
- expect(dep.config?.layout).toBe('card');
56
- });
57
-
58
- it('works in defineBlueprint with full dependency config', () => {
59
- const blueprint = defineBlueprint({
60
- slug: 'employee-salary-tracking',
61
- name: 'Employee Salary Tracking',
62
- version: '1.0.0',
63
- dependencies: [
64
- {
65
- slug: 'mod-authentication',
66
- version: '>=3.0.0',
67
- required: true,
68
- routeConfig: {
69
- prefix: '/auth',
70
- routes: { '/login': '/' },
71
- },
72
- slotMapping: {
73
- 'auth:profile-sections': 'app:settings-tabs',
74
- },
75
- config: {
76
- layout: 'split',
77
- },
78
- },
79
- ],
80
- });
81
-
82
- expect(blueprint.dependencies).toHaveLength(1);
83
- expect(blueprint.dependencies![0].routeConfig?.prefix).toBe('/auth');
84
- expect(blueprint.dependencies![0].slotMapping?.['auth:profile-sections']).toBe('app:settings-tabs');
85
- });
86
-
87
- it('ModuleRouteConfig type works independently', () => {
88
- const config: ModuleRouteConfig = {
89
- prefix: '/account',
90
- routes: {
91
- '/login': '/signin',
92
- '/register': false,
93
- },
94
- };
95
- expect(config.prefix).toBe('/account');
96
- });
97
-
98
- it('backward compatible — all new fields are optional', () => {
99
- const dep: BlueprintDependency = {
100
- slug: 'mod-auth',
101
- };
102
- expect(dep.routeConfig).toBeUndefined();
103
- expect(dep.slotMapping).toBeUndefined();
104
- expect(dep.config).toBeUndefined();
105
- });
106
- });
@@ -1,229 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { model, field, state } from '../builders';
3
- import { approval, escalation, review, crud } from '../factories';
4
-
5
- describe('approval factory', () => {
6
- it('adds approve/reject transitions with defaults', () => {
7
- const def = model('invoice')
8
- .field('amount', field.currency())
9
- .state('draft', state.initial())
10
- .state('pending_approval')
11
- .mixin(approval())
12
- .build();
13
-
14
- expect(def.transitions.approve).toBeDefined();
15
- expect(def.transitions.approve.from).toBe('pending_approval');
16
- expect(def.transitions.approve.to).toBe('approved');
17
- expect(def.transitions.approve.roles).toContain('approver');
18
-
19
- expect(def.transitions.reject).toBeDefined();
20
- expect(def.transitions.reject.from).toBe('pending_approval');
21
- expect(def.transitions.reject.to).toBe('rejected');
22
- });
23
-
24
- it('adds approval-related fields', () => {
25
- const def = model('invoice')
26
- .state('draft', state.initial())
27
- .mixin(approval())
28
- .build();
29
-
30
- expect(def.fields.approvalStatus).toBeDefined();
31
- expect(def.fields.approvedBy).toBeDefined();
32
- expect(def.fields.approvedAt).toBeDefined();
33
- expect(def.fields.rejectionReason).toBeDefined();
34
- });
35
-
36
- it('respects custom config', () => {
37
- const def = model('invoice')
38
- .state('draft', state.initial())
39
- .state('review')
40
- .mixin(approval({
41
- approverRole: 'manager',
42
- field: 'status',
43
- fromState: 'review',
44
- approvedState: 'done',
45
- rejectedState: 'cancelled',
46
- }))
47
- .build();
48
-
49
- expect(def.transitions.approve.from).toBe('review');
50
- expect(def.transitions.approve.to).toBe('done');
51
- expect(def.transitions.approve.roles).toContain('manager');
52
- expect(def.transitions.reject.to).toBe('cancelled');
53
- expect(def.fields.status).toBeDefined();
54
- });
55
-
56
- it('adds approver role', () => {
57
- const def = model('invoice')
58
- .state('draft', state.initial())
59
- .mixin(approval())
60
- .build();
61
-
62
- expect(def.roles!.approver).toBeDefined();
63
- expect(def.roles!.approver.permissions).toContain('approve');
64
- });
65
- });
66
-
67
- describe('escalation factory', () => {
68
- it('adds escalate transition with timeout', () => {
69
- const def = model('ticket')
70
- .state('pending_approval', state.initial())
71
- .mixin(escalation({ timeout: '24h' }))
72
- .build();
73
-
74
- expect(def.transitions.escalate).toBeDefined();
75
- expect(def.transitions.escalate.from).toBe('pending_approval');
76
- expect(def.transitions.escalate.to).toBe('escalated');
77
- expect(def.transitions.escalate.auto).toBe(true);
78
- });
79
-
80
- it('sets timeout on the source state', () => {
81
- const def = model('ticket')
82
- .state('pending_approval', state.initial())
83
- .mixin(escalation({ timeout: '2d', fromState: 'pending_approval' }))
84
- .build();
85
-
86
- expect(def.states.pending_approval.timeout).toBeDefined();
87
- expect(def.states.pending_approval.timeout!.duration).toBe('2d');
88
- expect(def.states.pending_approval.timeout!.fallback?.transition).toBe('escalate');
89
- });
90
-
91
- it('adds escalation fields', () => {
92
- const def = model('ticket')
93
- .state('open', state.initial())
94
- .mixin(escalation({ timeout: '1h', fromState: 'open' }))
95
- .build();
96
-
97
- expect(def.fields.escalatedAt).toBeDefined();
98
- expect(def.fields.escalationLevel).toBeDefined();
99
- });
100
- });
101
-
102
- describe('review factory', () => {
103
- it('adds submit/approve/revise cycle', () => {
104
- const def = model('article')
105
- .field('content', field.string())
106
- .state('draft', state.initial())
107
- .mixin(review())
108
- .build();
109
-
110
- expect(def.transitions.submit_for_review).toBeDefined();
111
- expect(def.transitions.submit_for_review.from).toBe('draft');
112
- expect(def.transitions.submit_for_review.to).toBe('in_review');
113
-
114
- expect(def.transitions.approve_review).toBeDefined();
115
- expect(def.transitions.approve_review.from).toBe('in_review');
116
- expect(def.transitions.approve_review.to).toBe('approved');
117
-
118
- expect(def.transitions.request_revisions).toBeDefined();
119
- expect(def.transitions.request_revisions.from).toBe('in_review');
120
- expect(def.transitions.request_revisions.to).toBe('revisions_requested');
121
-
122
- expect(def.transitions.resubmit).toBeDefined();
123
- expect(def.transitions.resubmit.from).toBe('revisions_requested');
124
- expect(def.transitions.resubmit.to).toBe('in_review');
125
- });
126
-
127
- it('adds review fields', () => {
128
- const def = model('article')
129
- .state('draft', state.initial())
130
- .mixin(review())
131
- .build();
132
-
133
- expect(def.fields.reviewNotes).toBeDefined();
134
- expect(def.fields.reviewedBy).toBeDefined();
135
- expect(def.fields.reviewedAt).toBeDefined();
136
- expect(def.fields.revisionCount).toBeDefined();
137
- });
138
-
139
- it('adds author and reviewer roles', () => {
140
- const def = model('article')
141
- .state('draft', state.initial())
142
- .mixin(review())
143
- .build();
144
-
145
- expect(def.roles!.author).toBeDefined();
146
- expect(def.roles!.reviewer).toBeDefined();
147
- });
148
-
149
- it('respects custom role names', () => {
150
- const def = model('article')
151
- .state('draft', state.initial())
152
- .mixin(review({ authorRole: 'writer', reviewerRole: 'editor' }))
153
- .build();
154
-
155
- expect(def.roles!.writer).toBeDefined();
156
- expect(def.roles!.editor).toBeDefined();
157
- expect(def.transitions.submit_for_review.roles).toContain('writer');
158
- expect(def.transitions.approve_review.roles).toContain('editor');
159
- });
160
- });
161
-
162
- describe('crud factory', () => {
163
- it('adds draft/active/archived lifecycle (soft delete)', () => {
164
- const def = model('product')
165
- .field('name', field.string().required())
166
- .mixin(crud())
167
- .build();
168
-
169
- expect(def.states.draft).toBeDefined();
170
- expect(def.states.draft.type).toBe('initial');
171
- expect(def.states.active).toBeDefined();
172
- expect(def.states.archived).toBeDefined();
173
-
174
- expect(def.transitions.publish).toBeDefined();
175
- expect(def.transitions.update).toBeDefined();
176
- expect(def.transitions.archive).toBeDefined();
177
- expect(def.transitions.restore).toBeDefined();
178
- });
179
-
180
- it('adds hard delete when softDelete=false', () => {
181
- const def = model('temp')
182
- .field('data', field.string())
183
- .mixin(crud({ softDelete: false }))
184
- .build();
185
-
186
- expect(def.states.deleted).toBeDefined();
187
- expect(def.states.deleted.type).toBe('end');
188
- expect(def.transitions.delete_record).toBeDefined();
189
- expect(def.transitions.archive).toBeUndefined();
190
- expect(def.transitions.restore).toBeUndefined();
191
- });
192
-
193
- it('adds CRUD fields', () => {
194
- const def = model('product')
195
- .mixin(crud())
196
- .build();
197
-
198
- expect(def.fields.createdBy).toBeDefined();
199
- expect(def.fields.createdAt).toBeDefined();
200
- expect(def.fields.updatedAt).toBeDefined();
201
- });
202
-
203
- it('does not overwrite existing fields', () => {
204
- const def = model('product')
205
- .field('createdBy', field.string('system'))
206
- .mixin(crud())
207
- .build();
208
-
209
- // Our manually set field should be preserved
210
- expect(def.fields.createdBy.default).toBe('system');
211
- });
212
-
213
- it('composable: approval + escalation', () => {
214
- const def = model('po')
215
- .field('amount', field.currency())
216
- .state('draft', state.initial())
217
- .state('pending_approval')
218
- .mixin(approval({ approverRole: 'manager' }))
219
- .mixin(escalation({ timeout: '48h' }))
220
- .build();
221
-
222
- // Both patterns should coexist
223
- expect(def.transitions.approve).toBeDefined();
224
- expect(def.transitions.reject).toBeDefined();
225
- expect(def.transitions.escalate).toBeDefined();
226
- expect(def.states.escalated).toBeDefined();
227
- expect(def.states.approved).toBeDefined();
228
- });
229
- });